Introduce ObjectExporter interface and ObjectExporterChain for customizing how objects are exported
This commit is contained in:
parent
cfaa77c750
commit
3f33ba4c67
|
|
@ -11,7 +11,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: 8.1.x-dev
|
||||
COMPOSER_ROOT_VERSION: 8.2.x-dev
|
||||
PHP_VERSION: 8.4
|
||||
|
||||
permissions:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
|
||||
|
||||
## [8.2.0] - 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
`ObjectExporter` interface and `ObjectExporterChain` for customizing how objects are exported (a chain can be passed to `Exporter`'s constructor and is consulted before the default property-by-property export is applied)
|
||||
|
||||
## [8.1.1] - 2026-07-13
|
||||
|
||||
### Fixed
|
||||
|
|
@ -61,6 +67,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
|
|||
|
||||
* This component is no longer supported on PHP 8.2
|
||||
|
||||
[8.2.0]: https://github.com/sebastianbergmann/exporter/compare/8.1.1...main
|
||||
[8.1.1]: https://github.com/sebastianbergmann/exporter/compare/8.1.0...8.1.1
|
||||
[8.1.0]: https://github.com/sebastianbergmann/exporter/compare/8.0.3...8.1.0
|
||||
[8.0.3]: https://github.com/sebastianbergmann/exporter/compare/8.0.2...8.0.3
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
"sebastian/recursion-context": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^13.2.4"
|
||||
"phpunit/phpunit": "^13.3"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
|
@ -58,7 +59,7 @@
|
|||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "8.1-dev"
|
||||
"dev-main": "8.2-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,15 +61,17 @@ final readonly class Exporter
|
|||
* @var positive-int
|
||||
*/
|
||||
private int $maxLengthForStrings;
|
||||
private ?ObjectExporterChain $objectExporter;
|
||||
|
||||
/**
|
||||
* @param non-negative-int $shortenArraysLongerThan
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*/
|
||||
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40)
|
||||
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40, ?ObjectExporterChain $objectExporter = null)
|
||||
{
|
||||
$this->shortenArraysLongerThan = $shortenArraysLongerThan;
|
||||
$this->maxLengthForStrings = $maxLengthForStrings;
|
||||
$this->objectExporter = $objectExporter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -524,6 +526,10 @@ final readonly class Exporter
|
|||
|
||||
$processed->add($value);
|
||||
|
||||
if ($this->objectExporter !== null && $this->objectExporter->handles($value)) {
|
||||
return $this->objectExporter->export($value, $this, $indentation);
|
||||
}
|
||||
|
||||
$array = $this->toArray($value);
|
||||
$buffer = '';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<?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;
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(object $object, Exporter $exporter, int $indentation): string;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?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 function array_any;
|
||||
|
||||
final readonly class ObjectExporterChain implements ObjectExporter
|
||||
{
|
||||
/**
|
||||
* @var non-empty-list<ObjectExporter>
|
||||
*/
|
||||
private array $exporters;
|
||||
|
||||
/**
|
||||
* @param non-empty-list<ObjectExporter> $exporters
|
||||
*/
|
||||
public function __construct(array $exporters)
|
||||
{
|
||||
$this->exporters = $exporters;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return array_any(
|
||||
$this->exporters,
|
||||
static fn (ObjectExporter $exporter) => $exporter->handles($object),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
||||
{
|
||||
foreach ($this->exporters as $objectExporter) {
|
||||
if ($objectExporter->handles($object)) {
|
||||
return $objectExporter->export($object, $exporter, $indentation);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ObjectNotSupportedException;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
|
|||
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 ReflectionClass;
|
||||
use SebastianBergmann\RecursionContext\Context;
|
||||
|
|
@ -37,6 +38,7 @@ use SplObjectStorage;
|
|||
use stdClass;
|
||||
|
||||
#[CoversClass(Exporter::class)]
|
||||
#[UsesClass(ObjectExporterChain::class)]
|
||||
#[Small]
|
||||
final class ExporterTest extends TestCase
|
||||
{
|
||||
|
|
@ -649,6 +651,114 @@ EOF;
|
|||
$this->assertTrue($reflector->isUninitializedLazyObject($object));
|
||||
}
|
||||
|
||||
public function testObjectCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass (indentation: 0)',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInArrayCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass (indentation: 1),
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([new stdClass]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInObjectCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
$object->nested = new ExampleClass('bar');
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
stdClass Object #%d (
|
||||
'nested' => SebastianBergmann\Exporter\ExampleClass handled by custom exporter,
|
||||
)
|
||||
EOT,
|
||||
$exporter->export($object),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectHandledByCustomObjectExporterIsOnlyExportedOnce(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass (indentation: 1),
|
||||
1 => stdClass Object #%d,
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([$object, $object]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testDefaultExportIsUsedWhenNoCustomObjectExporterHandlesObject(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'stdClass Object #%d ()',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
private function trimNewline(string $string): string
|
||||
{
|
||||
return (string) preg_replace('/[ ]*\n/', "\n", $string);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
<?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 testHandlesObjectWhenAtLeastOneComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertTrue($chain->handles(new stdClass));
|
||||
}
|
||||
|
||||
public function testDoesNotHandleObjectWhenNoComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertFalse($chain->handles(new stdClass));
|
||||
}
|
||||
|
||||
public function testDelegatesExportingToFirstComposedObjectExporterThatHandlesObject(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(stdClass::class),
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass handled by custom exporter',
|
||||
$chain->export(new stdClass, new Exporter, 0),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
ExampleClass::class . ' (indentation: 0)',
|
||||
$chain->export(new ExampleClass('foo'), new Exporter, 0),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCannotExportObjectWhenNoComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->expectException(ObjectNotSupportedException::class);
|
||||
|
||||
$chain->export(new stdClass, new Exporter, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?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 function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesEveryObject implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s (indentation: %d)',
|
||||
$object::class,
|
||||
$indentation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?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 readonly class ObjectExporterThatHandlesNoObject implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
||||
{
|
||||
throw new ObjectNotSupportedException;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?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 function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesObjectsOfSpecificType implements ObjectExporter
|
||||
{
|
||||
/**
|
||||
* @var class-string
|
||||
*/
|
||||
private string $type;
|
||||
|
||||
/**
|
||||
* @param class-string $type
|
||||
*/
|
||||
public function __construct(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof $this->type;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s handled by custom exporter',
|
||||
$object::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue