Skip to content

Commit

Permalink
Stream & Composite handlers (#95)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexey Rogachev <arogachev90@gmail.com>
Co-authored-by: Sergei Predvoditelev <sergei@predvoditelev.ru>
  • Loading branch information
3 people committed Oct 8, 2023
1 parent bed164d commit 34b8c47
Show file tree
Hide file tree
Showing 14 changed files with 481 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Expand Up @@ -28,4 +28,4 @@ jobs:
os: >-
['ubuntu-latest', 'windows-latest']
php: >-
['8.0', '8.1']
['8.0', '8.1', '8.2']
2 changes: 1 addition & 1 deletion .github/workflows/composer-require-checker.yml
Expand Up @@ -30,4 +30,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.0', '8.1']
['8.0', '8.1', '8.2']
2 changes: 1 addition & 1 deletion .github/workflows/mutation.yml
Expand Up @@ -26,6 +26,6 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.1']
['8.2']
secrets:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
2 changes: 1 addition & 1 deletion .github/workflows/static.yml
Expand Up @@ -28,4 +28,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.0', '8.1']
['8.0', '8.1', '8.2']
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

## 1.7.0 under development

- New #95: Add `StreamHandler` and `CompositeHandler` (@xepozz)
- Enh #94: Add a middleware handler to control dumps' output destination (@xepozz)
- New #94: Add `dump` function (@xepozz)

Expand Down
13 changes: 13 additions & 0 deletions README.md
Expand Up @@ -100,6 +100,19 @@ In the above `asJson()` will give you nicely formatted code. You can remove form

`$depth` argument allows you to set maximum recursion depth.

## Output destination

Choose one of existing classes or create a new one to control the destination where "dumps" will be sent to:
- [EchoHandler](./src/Handler/EchoHandler.php)
- Uses `echo` to write to stdout stream.
- Used by default.
- [StreamHandler](./src/Handler/StreamHandler.php)
- Uses `ext-sockets` to sent dumps encoded with `json_encode` to a UDP socket.
- [CompositeHandler](./src/Handler/CompositeHandler.php)
- Helpful class to sent dumps to multiple handlers in a row, for example `EchoHandler` and `StreamHandler`.

Output handlers are set via `VarDumper::setDefaultHandler()` method.

## Limitations

Current limitations are:
Expand Down
6 changes: 6 additions & 0 deletions composer-require-checker.json
@@ -0,0 +1,6 @@
{
"symbol-whitelist": [
"Socket",
"socket_write"
]
}
3 changes: 3 additions & 0 deletions composer.json
Expand Up @@ -33,6 +33,9 @@
"spatie/phpunit-watcher": "^1.23",
"vimeo/psalm": "^4.30|^5.3"
},
"suggest": {
"ext-sockets": "Send dumps to a server through UDP/TCP protocols"
},
"autoload": {
"psr-4": {
"Yiisoft\\VarDumper\\": "src"
Expand Down
30 changes: 30 additions & 0 deletions src/Handler/CompositeHandler.php
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Yiisoft\VarDumper\Handler;

use Yiisoft\VarDumper\HandlerInterface;

/**
* `CompositeHandler` allows to use multiple handlers at once.
* It iterates over all handlers and calls their {@see HandlerInterface::handle()} method.
* For example, you may use it to output data to both {@see StreamHandler} and {@see EchoHandler} at once.
*/
final class CompositeHandler implements HandlerInterface
{
/**
* @param HandlerInterface[] $handlers
*/
public function __construct(
private array $handlers
) {
}

public function handle(mixed $variable, int $depth, bool $highlight = false): void
{
foreach ($this->handlers as $handler) {
$handler->handle($variable, $depth, $highlight);
}
}
}
138 changes: 138 additions & 0 deletions src/Handler/StreamHandler.php
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

namespace Yiisoft\VarDumper\Handler;

use InvalidArgumentException;
use RuntimeException;
use Socket;
use Yiisoft\VarDumper\HandlerInterface;

use function fsockopen;
use function fwrite;
use function get_debug_type;
use function is_resource;
use function is_string;

/**
* Uses stream ({@link https://www.php.net/manual/en/intro.stream.php}) for writing variable's data. Requires "sockets"
* PHP extension when {@see StreamHandler::$uri} is a {@see Socket} instance.
*/
final class StreamHandler implements HandlerInterface
{
/**
* @var callable|null
*/
private mixed $encoder = null;
/**
* @var resource|Socket|null
*/
private mixed $stream = null;

/**
* @var resource|Socket|string
*/
private mixed $uri;

private const SOCKET_PROTOCOLS = ['udp', 'udg', 'tcp', 'unix'];

/**
* @param mixed|resource|string $uri
*/
public function __construct(
mixed $uri = 'udp://127.0.0.1:8890'
) {
if (!is_string($uri) && !is_resource($uri) && !$uri instanceof Socket) {
throw new InvalidArgumentException(
sprintf(
'Argument $uri must be either a string, a resource or a Socket instance, "%s" given.',
get_debug_type($uri)
)
);
}
$this->uri = $uri;
}

public function __destruct()
{
if (!is_string($this->uri) || !is_resource($this->stream)) {
return;
}
fclose($this->stream);
}

/**
* Encodes {@param $variable} with {@see self::$encoder} and sends the result to the stream.
*/
public function handle(mixed $variable, int $depth, bool $highlight = false): void
{
$data = ($this->encoder ?? '\json_encode')($variable);
if (!is_string($data)) {
throw new RuntimeException(
sprintf(
'Encoder must return a string, "%s" returned.',
get_debug_type($data)
)
);
}

if (!is_resource($this->stream) && !$this->stream instanceof Socket) {
$this->initializeStream();
}

if (!$this->writeToStream($data)) {
$this->initializeStream();

if (!$this->writeToStream($data)) {
throw new RuntimeException('Cannot write a stream.');
}
}
}

/**
* @param callable(mixed $variable): string $encoder Encoder that will be used to encode variable before sending it to the stream.
*/
public function withEncoder(callable $encoder): static
{
$new = clone $this;
$new->encoder = $encoder;
return $new;
}

private function initializeStream(): void
{
if (!is_string($this->uri)) {
$this->stream = $this->uri;
} else {
$uriHasSocketProtocol = false;
foreach (self::SOCKET_PROTOCOLS as $protocol) {
if (str_starts_with($this->uri, "$protocol://")) {
$uriHasSocketProtocol = true;
break;
}
}

$this->stream = $uriHasSocketProtocol ? fsockopen($this->uri) : fopen($this->uri, 'wb+');
}

if (!is_resource($this->stream) && !$this->stream instanceof Socket) {
throw new RuntimeException('Cannot initialize a stream.');
}
}

private function writeToStream(string $data): bool
{
if ($this->stream === null) {
return false;
}

if ($this->stream instanceof Socket) {
socket_write($this->stream, $data, strlen($data));

return true;
}

return @fwrite($this->stream, $data) !== false;
}
}
26 changes: 26 additions & 0 deletions tests/Handler/CompositeHandlerTest.php
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Yiisoft\VarDumper\Tests\Handler;

use PHPUnit\Framework\TestCase;
use Yiisoft\VarDumper\Handler\CompositeHandler;
use Yiisoft\VarDumper\Tests\Support\InMemoryHandler;

final class CompositeHandlerTest extends TestCase
{
public function testComposite(): void
{
$compositeHandler = new CompositeHandler([
$inMemoryHandler1 = new InMemoryHandler(),
$inMemoryHandler2 = new InMemoryHandler(),
]);
$variable = 'test';

$compositeHandler->handle($variable, 1, true);

$this->assertEquals([[$variable, 1, true]], $inMemoryHandler1->getVariables());
$this->assertEquals($inMemoryHandler1->getVariables(), $inMemoryHandler2->getVariables());
}
}
33 changes: 33 additions & 0 deletions tests/Handler/EchoHandlerTest.php
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Yiisoft\VarDumper\Tests\Handler;

use PHPUnit\Framework\TestCase;
use Yiisoft\VarDumper\Handler\EchoHandler;

final class EchoHandlerTest extends TestCase
{
public function testEcho(): void
{
$handler = new EchoHandler();

$handler->handle('test', 1);

$this->expectOutputString("'test'");
}

public function testHighlight(): void
{
$handler = new EchoHandler();

$handler->handle('test', 1, true);

$this->expectOutputString(
<<<HTML
<code><span style="color: #000000">\n<span style="color: #0000BB"></span><span style="color: #DD0000">'test'</span>\n</span>\n</code>
HTML
);
}
}

0 comments on commit 34b8c47

Please sign in to comment.