denpamusic/php-levin

View on GitHub
src/Console.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php

declare(strict_types=1);

namespace Denpa\Levin;

use ArrayAccess;
use Denpa\Levin\Notifications\NotificationInterface;
use Denpa\Levin\Section\Section;
use Denpa\Levin\Traits\ConsoleColor;
use Denpa\Levin\Types\BoostSerializable;
use Denpa\Levin\Types\Bytearray;
use Denpa\Levin\Types\Bytestring;
use Denpa\Levin\Types\TypeInterface;
use ReflectionClass;

class Console
{
    use ConsoleColor;

    /**
     * @var int
     */
    public $level = 0;

    /**
     * @var array Contains array of serializable types.
     */
    protected $types = [];

    /**
     * @var resource
     */
    protected $target = STDOUT;

    /**
     * @var array
     */
    protected $dumpers = [
        'is_string'                     => 'line',
        'is_numeric'                    => 'line',
        'is_array'                      => 'dumpArrayable',
        'is_a.'.CommandInterface::class => 'dumpCommand',
        'is_a.'.BucketInterface::class  => 'dumpBucket',
        'is_a.'.Section::class          => 'dumpSection',
        'is_a.'.Bytearray::class        => 'dumpBytearray',
        'is_a.'.Bytestring::class       => 'dumpBytestring',
        'is_a.'.ArrayAccess::class      => 'dumpArrayable',
        'is_a.'.TypeInterface::class    => 'dumpType',
    ];

    /**
     * @return void
     */
    public function __construct()
    {
        $this->loadTypes();
    }

    /**
     * @param mixed $message
     * @param mixed $args,...
     *
     * @return self
     */
    public function line($message = '', ...$args): self
    {
        fwrite(
            $this->target,
            $message === '' ?
                PHP_EOL : $this->colorize(sprintf((string) $message, ...$args))
        );

        return $this;
    }

    /**
     * @param mixed $object
     *
     * @return self
     */
    public function dump($object): self
    {
        foreach ($this->dumpers as $dumper => $method) {
            $args = explode('.', $dumper);
            $testFn = array_shift($args);

            if ($testFn($object, ...$args)) {
                return $this->$method($object);
            }
        }

        return $this;
    }

    /**
     * @return self
     */
    public function startBlock(): self
    {
        $this->level++;

        return $this;
    }

    /**
     * @return self
     */
    public function endBlock(): self
    {
        $this->level--;

        return $this;
    }

    /**
     * @return self
     */
    public function indent(): self
    {
        if ($this->level > 0) {
            $this->line(str_repeat('  ', $this->level));
        }

        return $this;
    }

    /**
     * Alias for inserting end-of-line.
     *
     * @return self
     */
    public function eol(): self
    {
        return $this->line();
    }

    /**
     * @param resource $target
     *
     * @return self
     */
    public function target($target = STDOUT): self
    {
        $this->target = $target;

        return $this;
    }

    /**
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return self
     */
    protected function dumpBucket(Bucket $bucket): self
    {
        return $this
            ->resetColors()
            ->color('red')
            ->line(
                '<%s bucket, payload: %d bytes>'.PHP_EOL,
                $bucket->isRequest() ? 'request' : 'response',
                $bucket->getCb()->toInt()
            )
            ->resetColors()
            ->dumpBucketHead($bucket)
            ->dumpBucketPayload($bucket);
    }

    /**
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return self
     */
    protected function dumpBucketHead(Bucket $bucket): self
    {
        return $this->line('[head]    =>')
            ->eol()
            ->startBlock()
            ->dump([
                'signature'        => $bucket->getSignature(),
                'cb'               => $bucket->getCb(),
                'return_data'      => $bucket->getReturnData(),
                'command'          => $bucket->getCommand(),
                'return_code'      => $bucket->getReturnCode(),
                'flags'            => $bucket->getFlags(),
                'protocol_version' => $bucket->getProtocolVersion(),
            ])
            ->endBlock()
            ->eol();
    }

    /**
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return self
     */
    protected function dumpBucketPayload(Bucket $bucket): self
    {
        return $this
            ->line('[payload] => ')
            ->startBlock()
            ->dump($bucket->getPayload())
            ->endBlock()
            ->eol();
    }

    /**
     * @param Denpa\Levin\CommandInterface $command
     *
     * @return self
     */
    protected function dumpCommand(CommandInterface $command): self
    {
        $type = $command instanceof NotificationInterface
            ? 'notification' : 'request';

        return $this
            ->resetColors()
            ->line('(')
            ->color('bright-yellow')
            ->line('%s %d', $type, $command->getCommandCode())
            ->resetColors()
            ->line(') ')
            ->background('white')
            ->color('black')
            ->line(classname(get_class($command)))
            ->resetColors();
    }

    /**
     * @param mixed $arrayable
     *
     * @return self
     */
    protected function dumpArrayable($arrayable): self
    {
        $keyLength = $this->normalizeKeyLength($arrayable);

        foreach ($arrayable as $key => $value) {
            $this
                ->indent()
                ->resetColors()
                ->line('%s => ', str_pad("[$key]", $keyLength));

            if ($value instanceof ArrayAccess || is_array($value)) {
                $this
                    ->startBlock()
                    ->dump($value)
                    ->endBlock();
                continue;
            }

            $this->dump($value)->eol();
        }

        return $this;
    }

    /**
     * @param \Denpa\Levin\Types\TypeInterface $type
     *
     * @return self
     */
    protected function dumpType(TypeInterface $type): self
    {
        $name = strtolower(classname(get_class($type)));

        return $this
            ->resetColors()
            ->color('red')
            ->line('<%s> ', $name)
            ->resetColors()
            ->line($this->splitHex($type).' (')
            ->color('bright-yellow')
            ->line($type->toInt())
            ->resetColors()
            ->line(')');
    }

    /**
     * @param \Denpa\Levin\Types\Bytestring $bytestring
     *
     * @return self
     */
    protected function dumpBytestring(Bytestring $bytestring): self
    {
        $this
            ->resetColors()
            ->color('red')
            ->line('<bytestring, %d bytes> ', count($bytestring))
            ->resetColors();

        if (count($bytestring) > 0) {
            $plaintext = preg_replace(
                '/[^a-z0-9!"#$%&\'()*+,.\/:;<=>?@\[\] ^_`{|}~-]+/i',
                '.',
                $bytestring->getValue()
            );

            $this
                ->line($bytestring->toHex().' (')
                ->color('bright-yellow')
                ->line('%s', $plaintext)
                ->resetColors()
                ->line(')');
        }

        return $this;
    }

    /**
     * @param Denpa\Levin\Types\Bytearray $bytearray
     *
     * @return self
     */
    protected function dumpBytearray(Bytearray $bytearray): self
    {
        $type = $bytearray->getType()->toInt();
        $type = $this->types[$type] ?? $type;

        if (count($bytearray) > 0) {
            $this->eol()->indent();
        }

        return $this
            ->resetColors()
            ->color('red')
            ->line(
                '<bytearray, %d entries of type %s>'.PHP_EOL,
                count($bytearray),
                $type
            )
            ->resetColors()
            ->dumpArrayable($bytearray);
    }

    /**
     * @param \Denpa\Levin\Section\Section $section
     *
     * @return self
     */
    protected function dumpSection(Section $section): self
    {
        return $this
            ->eol()
            ->resetColors()
            ->color('red')
            ->indent()
            ->line('<section, %d entries>'.PHP_EOL, count($section))
            ->resetColors()
            ->dumpArrayable($section);
    }

    /**
     * @return void
     */
    protected function loadTypes(): void
    {
        $serializable = new ReflectionClass(BoostSerializable::class);

        foreach ($serializable->getConstants() as $name => $value) {
            $parts = explode('_', $name);

            if (!in_array('TYPE', $parts)) {
                continue;
            }

            $this->types[$value] = strtolower(end($parts));
        }
    }

    /**
     * @param mixed $arrayable
     *
     * @return int
     */
    protected function normalizeKeyLength($arrayable): int
    {
        $keys = (is_object($arrayable) && method_exists($arrayable, 'keys')) ?
            $arrayable->keys() : array_keys($arrayable);

        return count($keys) == 0 ? 0 : max(array_map('strlen', $keys)) + 2;
    }

    /**
     * @param \Denpa\Levin\Types\TypeInterface $hex
     * @param int                              $length
     *
     * @return string
     */
    protected function splitHex(TypeInterface $type, int $length = 2): string
    {
        $arr = str_split($type->toHex(), $length);

        return implode(' ', ($type->isBigEndian() ? $arr : array_reverse($arr)));
    }
}