diff --git a/ChangeLog.md b/ChangeLog.md index c55c3d9..97f1d23 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,6 +2,12 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. +## [6.1.0] - 2024-MM-DD + +### Added + +* [#56](https://github.com/sebastianbergmann/exporter/pull/56): The export of objects can now be customized using `ObjectExporter` objects + ## [6.0.1] - 2024-03-02 ### Changed @@ -112,6 +118,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt * Remove HHVM-specific code that is no longer needed +[6.1.0]: https://github.com/sebastianbergmann/exporter/compare/6.0.1...main [6.0.1]: https://github.com/sebastianbergmann/exporter/compare/6.0.0...6.0.1 [6.0.0]: https://github.com/sebastianbergmann/exporter/compare/5.1...6.0.0 [5.1.2]: https://github.com/sebastianbergmann/exporter/compare/5.1.1...5.1.2 diff --git a/src/Exporter.php b/src/Exporter.php index cf7efc1..9fea46b 100644 --- a/src/Exporter.php +++ b/src/Exporter.php @@ -38,6 +38,13 @@ final readonly class Exporter { + private ?ObjectExporter $objectExporter; + + public function __construct(?ObjectExporter $objectExporter = null) + { + $this->objectExporter = $objectExporter; + } + /** * Exports a value as a string. * @@ -334,7 +341,18 @@ private function exportObject(object $value, RecursionContext $processed, int $i $processed->add($value); - $array = $this->toArray($value); + 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); @@ -352,6 +370,6 @@ private function exportObject(object $value, RecursionContext $processed, int $i $buffer = "\n" . $buffer . $whitespace; } - return $class . ' Object #' . spl_object_id($value) . ' (' . $buffer . ')'; + return $buffer; } } diff --git a/src/ObjectExporter.php b/src/ObjectExporter.php new file mode 100644 index 0000000..535aa53 --- /dev/null +++ b/src/ObjectExporter.php @@ -0,0 +1,17 @@ + + * + * 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; +} diff --git a/src/ObjectExporterChain.php b/src/ObjectExporterChain.php new file mode 100644 index 0000000..2efd7f3 --- /dev/null +++ b/src/ObjectExporterChain.php @@ -0,0 +1,51 @@ + + * + * 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 + */ + private array $exporter; + + /** + * @psalm-param non-empty-list $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; + } +} diff --git a/src/exception/Exception.php b/src/exception/Exception.php new file mode 100644 index 0000000..279613b --- /dev/null +++ b/src/exception/Exception.php @@ -0,0 +1,16 @@ + + * + * 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 +{ +} diff --git a/src/exception/ObjectNotSupportedException.php b/src/exception/ObjectNotSupportedException.php new file mode 100644 index 0000000..4e0bf96 --- /dev/null +++ b/src/exception/ObjectNotSupportedException.php @@ -0,0 +1,16 @@ + + * + * 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 +{ +} diff --git a/tests/ExporterTest.php b/tests/ExporterTest.php index b78e008..7775884 100644 --- a/tests/ExporterTest.php +++ b/tests/ExporterTest.php @@ -502,6 +502,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); diff --git a/tests/ObjectExporterChainTest.php b/tests/ObjectExporterChainTest.php new file mode 100644 index 0000000..bb901fc --- /dev/null +++ b/tests/ObjectExporterChainTest.php @@ -0,0 +1,64 @@ + + * + * 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)); + } +}