Skip to content

Commit

Permalink
Make export of objects customizable
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Mar 31, 2024
1 parent 8f75583 commit f6bc492
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 6 deletions.
4 changes: 4 additions & 0 deletions ChangeLog.md
Expand Up @@ -4,6 +4,10 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt

## [6.1.0] - 2024-MM-DD

### Added

* [#56](https://github.com/sebastianbergmann/exporter/pull/56): The export of objects can now be customized using a chain of (see `ObjectExporterChain`) `ObjectExporter` objects

## [6.0.1] - 2024-03-02

### Changed
Expand Down
30 changes: 24 additions & 6 deletions src/Exporter.php
Expand Up @@ -37,6 +37,13 @@

final readonly class Exporter
{
private ?ObjectExporterChain $objectExporter;

public function __construct(?ObjectExporterChain $objectExporter = null)
{
$this->objectExporter = $objectExporter;
}

/**
* Exports a value as a string.
*
Expand Down Expand Up @@ -329,7 +336,7 @@ private function exportArray(array &$value, RecursionContext $processed, int $in
return 'Array &' . (string) $key . ' [' . $values . ']';
}

private function exportObject(mixed $value, RecursionContext $processed, int $indentation): string
private function exportObject(object $value, RecursionContext $processed, int $indentation): string
{
$class = $value::class;

Expand All @@ -339,13 +346,24 @@ private function exportObject(mixed $value, RecursionContext $processed, int $in

$processed->add($value);

$array = $this->toArray($value);
$values = '';
if ($this->objectExporter !== null && $this->objectExporter->handles($value)) {
$buffer = $this->objectExporter->export($value, $this, $indentation);
} else {
$buffer = $this->defaultObjectExport($value, $processed, $indentation);
}

return $class . ' Object #' . spl_object_id($value) . ' (' . $buffer . ')';
}

private function defaultObjectExport(object $object, RecursionContext $processed, int $indentation): string
{
$array = $this->toArray($object);
$buffer = '';
$whitespace = str_repeat(' ', 4 * $indentation);

if (count($array) > 0) {
foreach ($array as $k => $v) {
$values .=
$buffer .=
$whitespace
. ' ' .
$this->recursiveExport($k, $indentation)
Expand All @@ -354,9 +372,9 @@ private function exportObject(mixed $value, RecursionContext $processed, int $in
. ",\n";
}

$values = "\n" . $values . $whitespace;
$buffer = "\n" . $buffer . $whitespace;
}

return $class . ' Object #' . spl_object_id($value) . ' (' . $values . ')';
return $buffer;
}
}
17 changes: 17 additions & 0 deletions src/ObjectExporter.php
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;

interface ObjectExporter
{
public function handles(object $object): bool;

public function export(object $object, Exporter $exporter, int $indentation): string;
}
51 changes: 51 additions & 0 deletions src/ObjectExporterChain.php
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;

final class ObjectExporterChain implements ObjectExporter
{
/**
* @psalm-var non-empty-list<ObjectExporter>
*/
private array $exporter;

/**
* @psalm-param non-empty-list<ObjectExporter> $exporter
*/
public function __construct(array $exporter)
{
$this->exporter = $exporter;
}

public function handles(object $object): bool
{
foreach ($this->exporter as $exporter) {
if ($exporter->handles($object)) {
return true;
}
}

return false;
}

/**
* @throws ObjectNotSupportedException
*/
public function export(object $object, Exporter $exporter, int $indentation): string
{
foreach ($this->exporter as $objectExporter) {
if ($objectExporter->handles($object)) {
return $objectExporter->export($object, $exporter, $indentation);
}
}

throw new ObjectNotSupportedException;
}
}
16 changes: 16 additions & 0 deletions src/exception/Exception.php
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;

use Throwable;

interface Exception extends Throwable
{
}
16 changes: 16 additions & 0 deletions src/exception/ObjectNotSupportedException.php
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;

use RuntimeException;

final class ObjectNotSupportedException extends RuntimeException implements Exception
{
}
23 changes: 23 additions & 0 deletions tests/ExporterTest.php
Expand Up @@ -26,12 +26,14 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use SebastianBergmann\RecursionContext\Context;
use SplObjectStorage;
use stdClass;

#[CoversClass(Exporter::class)]
#[UsesClass(ObjectExporterChain::class)]
#[Small]
final class ExporterTest extends TestCase
{
Expand Down Expand Up @@ -447,6 +449,27 @@ public function testShortenedRecursiveOccurredRecursion(): void
$this->assertEquals('*RECURSION*', (new Exporter)->shortenedRecursiveExport($value, $context));
}

public function testExportOfObjectsCanBeCustomized(): void
{
$objectExporter = $this->createStub(ObjectExporter::class);
$objectExporter->method('handles')->willReturn(true);
$objectExporter->method('export')->willReturn('custom object export');

$exporter = new Exporter(new ObjectExporterChain([$objectExporter]));

$this->assertStringMatchesFormat(
<<<'EOT'
Array &0 [
0 => stdClass Object #%d (custom object export),
1 => stdClass Object #%d (custom object export),
]
EOT
,
$exporter->export([new stdClass, new stdClass]),
);

}

private function trimNewline(string $string): string
{
return preg_replace('/[ ]*\n/', "\n", $string);
Expand Down
64 changes: 64 additions & 0 deletions tests/ObjectExporterChainTest.php
@@ -0,0 +1,64 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use stdClass;

#[CoversClass(ObjectExporterChain::class)]
#[UsesClass(Exporter::class)]
#[Small]
final class ObjectExporterChainTest extends TestCase
{
public function testCanBeQueriedWhetherChainedExporterHandlesAnObject(): void
{
$firstExporter = $this->createStub(ObjectExporter::class);
$firstExporter->method('handles')->willReturn(false);

$secondExporter = $this->createStub(ObjectExporter::class);
$secondExporter->method('handles')->willReturn(true);

$chain = new ObjectExporterChain([$firstExporter]);
$this->assertFalse($chain->handles(new stdClass));

$chain = new ObjectExporterChain([$firstExporter, $secondExporter]);
$this->assertTrue($chain->handles(new stdClass));
}

public function testDelegatesExportingToFirstExporterThatHandlesAnObject(): void
{
$firstExporter = $this->createStub(ObjectExporter::class);
$firstExporter->method('handles')->willReturn(false);
$firstExporter->method('export')->willThrowException(new ObjectNotSupportedException);

$secondExporter = $this->createStub(ObjectExporter::class);
$secondExporter->method('handles')->willReturn(true);
$secondExporter->method('export')->willReturn('string');

$chain = new ObjectExporterChain([$firstExporter, $secondExporter]);

$this->assertSame('string', $chain->export(new stdClass, new Exporter, 0));
}

public function testCannotExportObjectWhenNoExporterHandlesIt(): void
{
$firstExporter = $this->createStub(ObjectExporter::class);
$firstExporter->method('handles')->willReturn(false);

$chain = new ObjectExporterChain([$firstExporter]);

$this->expectException(ObjectNotSupportedException::class);

$this->assertSame('string', $chain->export(new stdClass, new Exporter, 0));
}
}

0 comments on commit f6bc492

Please sign in to comment.