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
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
COMPOSER_ROOT_VERSION: 8.1.x-dev
|
COMPOSER_ROOT_VERSION: 8.2.x-dev
|
||||||
PHP_VERSION: 8.4
|
PHP_VERSION: 8.4
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
|
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
|
## [8.1.1] - 2026-07-13
|
||||||
|
|
||||||
### Fixed
|
### 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
|
* 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.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.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
|
[8.0.3]: https://github.com/sebastianbergmann/exporter/compare/8.0.2...8.0.3
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"optimize-autoloader": true,
|
"optimize-autoloader": true,
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
},
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.4",
|
"php": ">=8.4",
|
||||||
|
|
@ -44,7 +45,7 @@
|
||||||
"sebastian/recursion-context": "^8.0"
|
"sebastian/recursion-context": "^8.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^13.2.4"
|
"phpunit/phpunit": "^13.3"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
|
@ -58,7 +59,7 @@
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "8.1-dev"
|
"dev-main": "8.2-dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,15 +61,17 @@ final readonly class Exporter
|
||||||
* @var positive-int
|
* @var positive-int
|
||||||
*/
|
*/
|
||||||
private int $maxLengthForStrings;
|
private int $maxLengthForStrings;
|
||||||
|
private ?ObjectExporterChain $objectExporter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param non-negative-int $shortenArraysLongerThan
|
* @param non-negative-int $shortenArraysLongerThan
|
||||||
* @param positive-int $maxLengthForStrings
|
* @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->shortenArraysLongerThan = $shortenArraysLongerThan;
|
||||||
$this->maxLengthForStrings = $maxLengthForStrings;
|
$this->maxLengthForStrings = $maxLengthForStrings;
|
||||||
|
$this->objectExporter = $objectExporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -524,6 +526,10 @@ final readonly class Exporter
|
||||||
|
|
||||||
$processed->add($value);
|
$processed->add($value);
|
||||||
|
|
||||||
|
if ($this->objectExporter !== null && $this->objectExporter->handles($value)) {
|
||||||
|
return $this->objectExporter->export($value, $this, $indentation);
|
||||||
|
}
|
||||||
|
|
||||||
$array = $this->toArray($value);
|
$array = $this->toArray($value);
|
||||||
$buffer = '';
|
$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\DataProvider;
|
||||||
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
||||||
use PHPUnit\Framework\Attributes\Small;
|
use PHPUnit\Framework\Attributes\Small;
|
||||||
|
use PHPUnit\Framework\Attributes\UsesClass;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
use SebastianBergmann\RecursionContext\Context;
|
use SebastianBergmann\RecursionContext\Context;
|
||||||
|
|
@ -37,6 +38,7 @@ use SplObjectStorage;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
#[CoversClass(Exporter::class)]
|
#[CoversClass(Exporter::class)]
|
||||||
|
#[UsesClass(ObjectExporterChain::class)]
|
||||||
#[Small]
|
#[Small]
|
||||||
final class ExporterTest extends TestCase
|
final class ExporterTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -649,6 +651,114 @@ EOF;
|
||||||
$this->assertTrue($reflector->isUninitializedLazyObject($object));
|
$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
|
private function trimNewline(string $string): string
|
||||||
{
|
{
|
||||||
return (string) preg_replace('/[ ]*\n/', "\n", $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