Compare commits

..

No commits in common. "642e2bbda79545ad3ed7e0b0e5d1a4b7cec8b7c0" and "5f9dc382bff11d789c813d42f211735019611751" have entirely different histories.

14 changed files with 108 additions and 848 deletions

View File

@ -6,9 +6,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
### Added
* `ObjectExporter` interface, `ObjectExporterChain`, and `ExportContext` for customizing how objects are exported (an object exporter can be passed to `Exporter`'s constructor and is consulted before the default representation of an object is used)
* `Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` use the representation provided by an object exporter as well; it is collapsed to a single line and shortened when it is longer than the configured maximum length
* `Exporter::hasCustomRepresentationFor()` for checking whether an object exporter provides the representation for an object
`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

276
README.md
View File

@ -25,19 +25,19 @@ composer require --dev sebastian/exporter
Exporting:
```php
<?php declare(strict_types=1);
<?php
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
/*
Exception Object #4 (
'message' => '',
'string' => '',
'code' => 0,
'file' => '/home/sebastianbergmann/test.php',
'line' => 34,
'previous' => null,
Exception Object &0000000078de0f0d000000002003a261 (
'message' => ''
'string' => ''
'code' => 0
'file' => '/home/sebastianbergmann/test.php'
'line' => 34
'previous' => null
)
*/
@ -49,7 +49,7 @@ print $exporter->export(new Exception);
Exporting simple types:
```php
<?php declare(strict_types=1);
<?php
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
@ -85,66 +85,64 @@ print $exporter->export(chr(0) . chr(1) . chr(2) . chr(3) . chr(4) . chr(5));
Exporting complex types:
```php
<?php declare(strict_types=1);
<?php
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
/*
Array &0 [
0 => Array &1 [
0 => 1,
1 => 2,
2 => 3,
],
1 => Array &2 [
0 => '',
1 => 0,
2 => false,
],
]
*/
print $exporter->export([[1, 2, 3], ['', 0, false]]);
/*
Array &0 [
'self' => Array &1 [
'self' => Array &1,
],
]
*/
$array = [];
$array['self'] = &$array;
print $exporter->export($array);
/*
stdClass Object #4 (
'self' => stdClass Object #4,
Array &0 (
0 => Array &1 (
0 => 1
1 => 2
2 => 3
)
1 => Array &2 (
0 => ''
1 => 0
2 => false
)
)
*/
$obj = new stdClass;
$obj->self = $obj;
print $exporter->export(array(array(1,2,3), array("",0,FALSE)));
/*
Array &0 (
'self' => Array &1 (
'self' => Array &1
)
)
*/
$array = array();
$array['self'] = &$array;
print $exporter->export($array);
/*
stdClass Object &0000000003a66dcc0000000025e723e2 (
'self' => stdClass Object &0000000003a66dcc0000000025e723e2
)
*/
$obj = new stdClass();
$obj->self = $obj;
print $exporter->export($obj);
```
Compact exports:
```php
<?php declare(strict_types=1);
<?php
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
// []
print $exporter->shortenedExport([]);
// Array ()
print $exporter->shortenedExport(array());
// [...]
print $exporter->shortenedExport([1, 2, 3, 4, 5]);
// Array (...)
print $exporter->shortenedExport(array(1,2,3,4,5));
// stdClass Object ()
print $exporter->shortenedExport(new stdClass);
@ -152,7 +150,7 @@ print $exporter->shortenedExport(new stdClass);
// Exception Object (...)
print $exporter->shortenedExport(new Exception);
// 'this\nis\na\nsuper\nlong\nstr...nspace'
// this\nis\na\nsuper\nlong\nstring\nt...\nspace
print $exporter->shortenedExport(
<<<LONG_STRING
this
@ -175,181 +173,3 @@ space
LONG_STRING
);
```
## Customizing How Objects Are Exported
By default, an object is exported property by property. Implement the `ObjectExporter` interface to control how objects of a type are represented:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\ExportContext;
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\Exporter\ObjectExporter;
final class MoneyExporter implements ObjectExporter
{
public function handles(object $object): bool
{
return $object instanceof Money;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
assert($object instanceof Money);
return sprintf('Money (%d %s)', $object->amount(), $object->currency());
}
}
```
An object exporter is registered by passing it to `Exporter`'s constructor. It is consulted before the default representation of an object is used:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter(objectExporter: new MoneyExporter);
// Money (1999 EUR)
print $exporter->export(new Money(1999, 'EUR'));
/*
stdClass Object #6 (
'net' => Money (1999 EUR),
'gross' => Money (2379 EUR),
)
*/
$price = new stdClass;
$price->net = new Money(1999, 'EUR');
$price->gross = new Money(2379, 'EUR');
print $exporter->export($price);
```
`ObjectExporterChain` composes multiple object exporters into a single one. They are consulted in the order in which they are composed into the chain and the first one whose `handles()` method returns `true` is asked for the representation:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\Exporter\ObjectExporterChain;
$exporter = new Exporter(
objectExporter: new ObjectExporterChain(
[
new BasketExporter,
new MoneyExporter,
],
),
);
```
Enums are objects and are therefore passed to the object exporters as well. When no object exporter handles an object, the default representation is used for it.
### Repeated Occurrences
When the same object occurs more than once, the default representation is only used for the first occurrence; subsequent occurrences are replaced with a reference to the object:
```php
/*
Array &0 [
0 => Money Object #8 (
'amount' => 1999,
'currency' => 'EUR',
),
1 => Money Object #8,
]
*/
$money = new Money(1999, 'EUR');
print (new Exporter)->export([$money, $money]);
```
An object exporter is responsible for the entire representation of the object it handles. Every occurrence of such an object is therefore exported by it:
```php
/*
Array &0 [
0 => Money (1999 EUR),
1 => Money (1999 EUR),
]
*/
print $exporter->export([$money, $money]);
```
The exception is an object that is (indirectly) nested in itself: it cannot be exported this way without recursing infinitely and is replaced with a reference to the object.
### Exporting Nested Values
An object exporter may use the `Exporter` it is given to export values that are nested in the object it handles. The `ExportContext` it is given must be passed on when it does. Otherwise, the export of these nested values starts over with an empty context and, for instance, assigns references to arrays that are already in use elsewhere in the same export:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\ExportContext;
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\Exporter\ObjectExporter;
final class BasketExporter implements ObjectExporter
{
public function handles(object $object): bool
{
return $object instanceof Basket;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
assert($object instanceof Basket);
return 'Basket ' . $exporter->export($object->items(), $indentation, $context);
}
}
```
```php
/*
Array &0 [
'basket' => Basket Array &1 [
0 => Money (1999 EUR),
],
]
*/
print $exporter->export(['basket' => new Basket([new Money(1999, 'EUR')])]);
```
### Compact Exports
`Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` consult object exporters as well. The representation an object exporter provides is collapsed to a single line and shortened when it is longer than the configured maximum length:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter(objectExporter: new MoneyExporter);
// Money (1999 EUR)
print $exporter->shortenedExport(new Money(1999, 'EUR'));
```
Bear in mind that these representations are used where a short, single-line, and stable string is required. The representation of a data set is built using `Exporter::shortenedRecursiveExport()`, for instance, and is part of the name of a test that uses a data provider. An object exporter should therefore provide a representation that is compact and that does not change from one export to the next.
### Custom Representations
`Exporter::hasCustomRepresentationFor()` tells whether an object exporter provides the representation for an object. This is meant for code that renders objects itself and wants to use the representation an object exporter provides when there is one:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter(objectExporter: new MoneyExporter);
// true
var_dump($exporter->hasCustomRepresentationFor(new Money(1999, 'EUR')));
// false
var_dump($exporter->hasCustomRepresentationFor(new stdClass));
```
Bear in mind that an object that is (indirectly) nested in itself is replaced with a reference to the object instead of being exported by an object exporter again.

View File

@ -1,82 +0,0 @@
<?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 SebastianBergmann\RecursionContext\Context as RecursionContext;
use SplObjectStorage;
/**
* Carries the state that is shared by all steps of a single export operation.
*
* An implementation of ObjectExporter that exports values nested in the object
* it handles must pass the instance of this class it is given to the Exporter
* it delegates to. Otherwise, the export of these nested values starts over
* with an empty context and, for instance, assigns references to arrays that
* are already in use elsewhere in the same export.
*/
final class ExportContext
{
private RecursionContext $recursionContext;
/**
* @var SplObjectStorage<object, null>
*/
private SplObjectStorage $exportedByObjectExporter;
public function __construct()
{
$this->recursionContext = new RecursionContext;
$this->exportedByObjectExporter = new SplObjectStorage;
}
/**
* @template T of array|object
*
* @param T $value
*
* @param-out T $value
*/
public function add(array|object &$value): int
{
return $this->recursionContext->add($value);
}
/**
* @template T of array|object
*
* @param T $value
*
* @param-out T $value
*/
public function contains(array|object &$value): false|int
{
return $this->recursionContext->contains($value);
}
/**
* Whether the export of an object by an ObjectExporter is in progress.
*
* This is the case when the object is (indirectly) nested in itself.
*/
public function isBeingExportedByObjectExporter(object $object): bool
{
return $this->exportedByObjectExporter->offsetExists($object);
}
public function beginExportByObjectExporter(object $object): void
{
$this->exportedByObjectExporter->offsetSet($object);
}
public function endExportByObjectExporter(object $object): void
{
$this->exportedByObjectExporter->offsetUnset($object);
}
}

View File

@ -61,13 +61,13 @@ final readonly class Exporter
* @var positive-int
*/
private int $maxLengthForStrings;
private ?ObjectExporter $objectExporter;
private ?ObjectExporterChain $objectExporter;
/**
* @param non-negative-int $shortenArraysLongerThan
* @param positive-int $maxLengthForStrings
*/
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40, ?ObjectExporter $objectExporter = null)
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40, ?ObjectExporterChain $objectExporter = null)
{
$this->shortenArraysLongerThan = $shortenArraysLongerThan;
$this->maxLengthForStrings = $maxLengthForStrings;
@ -86,23 +86,15 @@ final readonly class Exporter
* - Strings are always quoted with single quotes
* - Carriage returns and newlines are normalized to \n
* - Recursion and repeated rendering is treated properly
*
* An implementation of ObjectExporter must pass the ExportContext it is
* given to this method when it exports values that are nested in the
* object it handles.
*
* @throws ObjectNotSupportedException
*/
public function export(mixed $value, int $indentation = 0, ?ExportContext $context = null): string
public function export(mixed $value, int $indentation = 0): string
{
return $this->recursiveExport($value, $indentation, $context);
return $this->recursiveExport($value, $indentation);
}
/**
* @param array<mixed> $data
* @param positive-int $maxLengthForStrings
*
* @throws ObjectNotSupportedException
*/
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string
{
@ -136,13 +128,7 @@ final readonly class Exporter
* Newlines are replaced by the visible string '\n'.
* Contents of arrays and objects (if any) are replaced by '...'.
*
* The representation a custom object exporter provides for an object is
* used, but it is collapsed to a single line and shortened when it is
* longer than $maxLengthForStrings.
*
* @param positive-int $maxLengthForStrings
*
* @throws ObjectNotSupportedException
*/
public function shortenedExport(mixed $value, int $maxLengthForStrings = 40): string
{
@ -151,20 +137,13 @@ final readonly class Exporter
}
if (is_string($value)) {
return $this->shorten($this->exportString($value), $maxLengthForStrings);
}
$string = str_replace("\n", '', $this->exportString($value));
if ($this->objectExporter !== null &&
is_object($value) &&
$this->objectExporter->handles($value)) {
$context = new ExportContext;
if (mb_strlen($string) > $maxLengthForStrings) {
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
}
$context->beginExportByObjectExporter($value);
return $this->shorten(
$this->objectExporter->export($value, $this, 0, $context),
$maxLengthForStrings,
);
return $string;
}
if ($value instanceof BackedEnum) {
@ -202,26 +181,6 @@ final readonly class Exporter
return $this->export($value);
}
/**
* Returns whether a custom object exporter provides the representation
* for an object.
*
* This is intended for code that renders objects itself and wants to use
* the representation a custom object exporter provides when there is one.
*
* Bear in mind that an object that is (indirectly) nested in itself is
* replaced with a reference to the object instead of being exported by a
* custom object exporter again.
*/
public function hasCustomRepresentationFor(object $object): bool
{
if ($this->objectExporter === null) {
return false;
}
return $this->objectExporter->handles($object);
}
/**
* Converts an object to an array containing all of its private, protected
* and public properties.
@ -355,8 +314,6 @@ final readonly class Exporter
/**
* @param array<mixed> $data
* @param positive-int $maxLengthForStrings
*
* @throws ObjectNotSupportedException
*/
private function shortenedCountedRecursiveExport(array &$data, RecursionContext $processed, int &$counter, int $maxLengthForStrings): string
{
@ -393,10 +350,7 @@ final readonly class Exporter
return implode(', ', $result);
}
/**
* @throws ObjectNotSupportedException
*/
private function recursiveExport(mixed &$value, int $indentation = 0, ?ExportContext $context = null): string
private function recursiveExport(mixed &$value, int $indentation = 0, ?RecursionContext $processed = null): string
{
if ($value === null) {
return 'null';
@ -423,42 +377,44 @@ final readonly class Exporter
);
}
if ($value instanceof BackedEnum) {
return sprintf(
'%s Enum #%d (%s, %s)',
$value::class,
spl_object_id($value),
$value->name,
$this->export($value->value),
);
}
if ($value instanceof UnitEnum) {
return sprintf(
'%s Enum #%d (%s)',
$value::class,
spl_object_id($value),
$value->name,
);
}
if (is_string($value)) {
return $this->exportString($value);
}
if ($context === null) {
$context = new ExportContext;
if ($processed === null) {
$processed = new RecursionContext;
}
if (is_array($value)) {
return $this->exportArray($value, $context, $indentation);
return $this->exportArray($value, $processed, $indentation);
}
if (is_object($value)) {
return $this->exportObject($value, $context, $indentation);
return $this->exportObject($value, $processed, $indentation);
}
return var_export($value, true);
}
/**
* Collapses a representation to a single line and shortens it when it is
* longer than $maxLengthForStrings.
*
* @param positive-int $maxLengthForStrings
*/
private function shorten(string $string, int $maxLengthForStrings): string
{
$string = str_replace(["\r", "\n"], '', $string);
if (mb_strlen($string) > $maxLengthForStrings) {
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
}
return $string;
}
private function exportFloat(float $value): string
{
if (is_nan($value)) {
@ -529,17 +485,15 @@ final readonly class Exporter
/**
* @param array<mixed> $value
*
* @throws ObjectNotSupportedException
*/
private function exportArray(array &$value, ExportContext $context, int $indentation): string
private function exportArray(array &$value, RecursionContext $processed, int $indentation): string
{
if (($key = $context->contains($value)) !== false) {
if (($key = $processed->contains($value)) !== false) {
return 'Array &' . $key;
}
$array = $value;
$key = $context->add($value);
$key = $processed->add($value);
$values = '';
if (count($array) > 0) {
@ -552,7 +506,7 @@ final readonly class Exporter
$this->recursiveExport($k, $indentation)
. ' => ' .
/** @phpstan-ignore offsetAccess.invalidOffset */
$this->recursiveExport($value[$k], $indentation + 1, $context)
$this->recursiveExport($value[$k], $indentation + 1, $processed)
. ",\n";
}
@ -562,68 +516,24 @@ final readonly class Exporter
return 'Array &' . (string) $key . ' [' . $values . ']';
}
/**
* @throws ObjectNotSupportedException
*/
private function exportObject(object $value, ExportContext $context, int $indentation): string
private function exportObject(object $value, RecursionContext $processed, int $indentation): string
{
// A custom object exporter is responsible for the entire
// representation of the object it handles. Therefore, it is asked for
// that representation before the recursion context is consulted: every
// occurrence of such an object is exported the same way instead of
// repeated occurrences being replaced with a reference to the object.
if ($this->objectExporter !== null && $this->objectExporter->handles($value)) {
return $this->objectExporter->export($value, $this, $indentation);
}
$class = $value::class;
if ($this->objectExporter !== null) {
// An object that is (indirectly) nested in itself cannot be
// exported by a custom object exporter without recursing
// infinitely and is therefore replaced with a reference to the
// object.
if ($context->isBeingExportedByObjectExporter($value)) {
return $class . ' Object #' . spl_object_id($value);
}
// A custom object exporter is responsible for the entire
// representation of the object it handles. Therefore, it is asked
// for that representation before the recursion context is
// consulted: every occurrence of such an object is exported the
// same way instead of repeated occurrences being replaced with a
// reference to the object.
if ($this->objectExporter->handles($value)) {
$context->beginExportByObjectExporter($value);
try {
return $this->objectExporter->export($value, $this, $indentation, $context);
} finally {
$context->endExportByObjectExporter($value);
}
}
}
// Enums are handled after a custom object exporter has been consulted
// so that the representation of an enum can be customized, but before
// the recursion context is consulted because an enum case is a
// singleton for which a reference to a previous occurrence would be
// less informative than the representation itself.
if ($value instanceof BackedEnum) {
return sprintf(
'%s Enum #%d (%s, %s)',
$class,
spl_object_id($value),
$value->name,
$this->export($value->value),
);
}
if ($value instanceof UnitEnum) {
return sprintf(
'%s Enum #%d (%s)',
$class,
spl_object_id($value),
$value->name,
);
}
if ($context->contains($value) !== false) {
if ($processed->contains($value) !== false) {
return $class . ' Object #' . spl_object_id($value);
}
$context->add($value);
$processed->add($value);
$array = $this->toArray($value);
$buffer = '';
@ -637,7 +547,7 @@ final readonly class Exporter
. ' ' .
$this->recursiveExport($k, $indentation)
. ' => ' .
$this->recursiveExport($v, $indentation + 1, $context)
$this->recursiveExport($v, $indentation + 1, $processed)
. ",\n";
}

View File

@ -14,12 +14,7 @@ interface ObjectExporter
public function handles(object $object): bool;
/**
* Exports an object this object exporter handles.
*
* The ExportContext must be passed on to Exporter::export() when values
* that are nested in the object are exported using $exporter.
*
* @throws ObjectNotSupportedException
*/
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string;
public function export(object $object, Exporter $exporter, int $indentation): string;
}

View File

@ -37,11 +37,11 @@ final readonly class ObjectExporterChain implements ObjectExporter
/**
* @throws ObjectNotSupportedException
*/
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
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, $context);
return $objectExporter->export($object, $exporter, $indentation);
}
}

View File

@ -38,7 +38,6 @@ use SplObjectStorage;
use stdClass;
#[CoversClass(Exporter::class)]
#[UsesClass(ExportContext::class)]
#[UsesClass(ObjectExporterChain::class)]
#[Small]
final class ExporterTest extends TestCase
@ -670,20 +669,6 @@ EOF;
);
}
public function testObjectCanBeExportedByCustomObjectExporterThatIsNotComposedIntoChain(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesEveryObject,
);
$this->assertSame(
'stdClass (indentation: 0)',
$exporter->export(new stdClass),
);
}
public function testObjectNestedInArrayCanBeExportedByCustomObjectExporter(): void
{
$exporter = new Exporter(
@ -799,281 +784,6 @@ EOT,
);
}
public function testEnumCanBeExportedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesEveryObject,
],
),
);
$this->assertSame(
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
$exporter->export(ExampleEnum::Value),
);
$this->assertSame(
'SebastianBergmann\Exporter\ExampleStringBackedEnum (indentation: 0)',
$exporter->export(ExampleStringBackedEnum::Value),
);
}
public function testDefaultExportIsUsedForEnumWhenNoCustomObjectExporterHandlesIt(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
],
),
);
$this->assertStringMatchesFormat(
'SebastianBergmann\Exporter\ExampleEnum Enum #%d (Value)',
$exporter->export(ExampleEnum::Value),
);
$this->assertStringMatchesFormat(
'SebastianBergmann\Exporter\ExampleStringBackedEnum Enum #%d (Value, \'value\')',
$exporter->export(ExampleStringBackedEnum::Value),
);
}
public function testShortenedExportUsesRepresentationProvidedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesEveryObject,
);
$this->assertSame(
'stdClass (indentation: 0)',
$exporter->shortenedExport(new stdClass),
);
$this->assertSame(
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
$exporter->shortenedExport(ExampleEnum::Value, 80),
);
}
public function testShortenedExportCollapsesRepresentationProvidedByCustomObjectExporterToSingleLine(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatReturnsGivenRepresentation("first\nsecond"),
);
$this->assertSame(
'firstsecond',
$exporter->shortenedExport(new stdClass),
);
}
public function testShortenedExportShortensRepresentationProvidedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatReturnsGivenRepresentation(str_repeat('a', 41)),
);
$this->assertSame(
str_repeat('a', 30) . '...' . str_repeat('a', 7),
$exporter->shortenedExport(new stdClass),
);
}
public function testShortenedRecursiveExportUsesRepresentationProvidedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesEveryObject,
);
$data = [new stdClass, 'foo'];
$this->assertSame(
"stdClass (indentation: 0), 'foo'",
$exporter->shortenedRecursiveExport($data),
);
}
public function testShortenedExportDoesNotRecurseInfinitelyForObjectNestedInItself(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatExportsNestedValues,
);
$node = new Node;
$node->children = [$node];
$this->assertStringMatchesFormat(
'Node(Array &0 [ 0 => SebastianBergmann\Exporter\Node Object #%d,])',
$exporter->shortenedExport($node, 200),
);
}
public function testShortenedExportUsesDefaultRepresentationWhenNoCustomObjectExporterHandlesObject(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesNoObject,
);
$this->assertSame(
'stdClass Object ()',
$exporter->shortenedExport(new stdClass),
);
}
public function testKnowsThatCustomObjectExporterProvidesRepresentationForObjectItHandles(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
],
),
);
$this->assertTrue($exporter->hasCustomRepresentationFor(new ExampleClass('bar')));
}
public function testKnowsThatCustomObjectExporterDoesNotProvideRepresentationForObjectItDoesNotHandle(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
],
),
);
$this->assertFalse($exporter->hasCustomRepresentationFor(new stdClass));
}
public function testKnowsThatNoCustomObjectExporterProvidesRepresentationForObjectWhenNoneIsConfigured(): void
{
$this->assertFalse((new Exporter)->hasCustomRepresentationFor(new stdClass));
}
public function testCustomObjectExporterCanExportValuesNestedInObjectItHandles(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$this->assertSame(
<<<'EOT'
Node(Array &0 [
'key' => 'value',
])
EOT,
$exporter->export(new Node(['key' => 'value'])),
);
}
public function testArrayExportedByCustomObjectExporterDoesNotReuseReferenceToOtherArray(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$this->assertSame(
<<<'EOT'
Array &0 [
0 => Node(Array &1 [
'key' => 'value',
]),
]
EOT,
$exporter->export([new Node(['key' => 'value'])]),
);
}
public function testObjectNestedInItselfIsNotExportedByCustomObjectExporterAgain(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$node = new Node;
$node->children = [$node];
$this->assertStringMatchesFormat(
<<<'EOT'
Node(Array &0 [
0 => SebastianBergmann\Exporter\Node Object #%d,
])
EOT,
$exporter->export($node),
);
}
public function testObjectsNestedInEachOtherAreNotExportedByCustomObjectExporterAgain(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$first = new Node;
$second = new Node;
$first->children = [$second];
$second->children = [$first];
$this->assertStringMatchesFormat(
<<<'EOT'
Node(Array &0 [
0 => Node(Array &1 [
0 => SebastianBergmann\Exporter\Node Object #%d,
]),
])
EOT,
$exporter->export($first),
);
}
private function trimNewline(string $string): string
{
return (string) preg_replace('/[ ]*\n/', "\n", $string);

View File

@ -17,7 +17,6 @@ use stdClass;
#[CoversClass(ObjectExporterChain::class)]
#[UsesClass(Exporter::class)]
#[UsesClass(ExportContext::class)]
#[Small]
final class ObjectExporterChainTest extends TestCase
{
@ -56,12 +55,12 @@ final class ObjectExporterChainTest extends TestCase
$this->assertSame(
'stdClass handled by custom exporter',
$chain->export(new stdClass, new Exporter, 0, new ExportContext),
$chain->export(new stdClass, new Exporter, 0),
);
$this->assertSame(
ExampleClass::class . ' (indentation: 0)',
$chain->export(new ExampleClass('foo'), new Exporter, 0, new ExportContext),
$chain->export(new ExampleClass('foo'), new Exporter, 0),
);
}
@ -75,6 +74,6 @@ final class ObjectExporterChainTest extends TestCase
$this->expectException(ObjectNotSupportedException::class);
$chain->export(new stdClass, new Exporter, 0, new ExportContext);
$chain->export(new stdClass, new Exporter, 0);
}
}

View File

@ -1,29 +0,0 @@
<?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;
/*
* Helper to test export of objects that are nested in objects.
*/
final class Node
{
/**
* @var array<mixed>
*/
public array $children;
/**
* @param array<mixed> $children
*/
public function __construct(array $children = [])
{
$this->children = $children;
}
}

View File

@ -1,31 +0,0 @@
<?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 assert;
use function sprintf;
final readonly class ObjectExporterThatExportsNestedValues implements ObjectExporter
{
public function handles(object $object): bool
{
return $object instanceof Node;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
assert($object instanceof Node);
return sprintf(
'Node(%s)',
$exporter->export($object->children, $indentation, $context),
);
}
}

View File

@ -18,7 +18,7 @@ final readonly class ObjectExporterThatHandlesEveryObject implements ObjectExpor
return true;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
public function export(object $object, Exporter $exporter, int $indentation): string
{
return sprintf(
'%s (indentation: %d)',

View File

@ -16,7 +16,7 @@ final readonly class ObjectExporterThatHandlesNoObject implements ObjectExporter
return false;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
public function export(object $object, Exporter $exporter, int $indentation): string
{
throw new ObjectNotSupportedException;
}

View File

@ -31,7 +31,7 @@ final readonly class ObjectExporterThatHandlesObjectsOfSpecificType implements O
return $object instanceof $this->type;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
public function export(object $object, Exporter $exporter, int $indentation): string
{
return sprintf(
'%s handled by custom exporter',

View File

@ -1,30 +0,0 @@
<?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 ObjectExporterThatReturnsGivenRepresentation implements ObjectExporter
{
private string $representation;
public function __construct(string $representation)
{
$this->representation = $representation;
}
public function handles(object $object): bool
{
return true;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
return $this->representation;
}
}