Compare commits
8 Commits
5f9dc382bf
...
642e2bbda7
| Author | SHA1 | Date |
|---|---|---|
|
|
642e2bbda7 | |
|
|
1ff285c572 | |
|
|
60d658c5b5 | |
|
|
58c91cd888 | |
|
|
792150d027 | |
|
|
b5aff00a52 | |
|
|
067b4fde58 | |
|
|
b9ec7d93fe |
|
|
@ -6,7 +6,9 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
|
||||||
|
|
||||||
### Added
|
### 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)
|
* `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
|
||||||
|
|
||||||
## [8.1.1] - 2026-07-13
|
## [8.1.1] - 2026-07-13
|
||||||
|
|
||||||
|
|
|
||||||
256
README.md
256
README.md
|
|
@ -25,19 +25,19 @@ composer require --dev sebastian/exporter
|
||||||
Exporting:
|
Exporting:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
use SebastianBergmann\Exporter\Exporter;
|
use SebastianBergmann\Exporter\Exporter;
|
||||||
|
|
||||||
$exporter = new Exporter;
|
$exporter = new Exporter;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Exception Object &0000000078de0f0d000000002003a261 (
|
Exception Object #4 (
|
||||||
'message' => ''
|
'message' => '',
|
||||||
'string' => ''
|
'string' => '',
|
||||||
'code' => 0
|
'code' => 0,
|
||||||
'file' => '/home/sebastianbergmann/test.php'
|
'file' => '/home/sebastianbergmann/test.php',
|
||||||
'line' => 34
|
'line' => 34,
|
||||||
'previous' => null
|
'previous' => null,
|
||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ print $exporter->export(new Exception);
|
||||||
Exporting simple types:
|
Exporting simple types:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
use SebastianBergmann\Exporter\Exporter;
|
use SebastianBergmann\Exporter\Exporter;
|
||||||
|
|
||||||
$exporter = new Exporter;
|
$exporter = new Exporter;
|
||||||
|
|
@ -85,64 +85,66 @@ print $exporter->export(chr(0) . chr(1) . chr(2) . chr(3) . chr(4) . chr(5));
|
||||||
Exporting complex types:
|
Exporting complex types:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
use SebastianBergmann\Exporter\Exporter;
|
use SebastianBergmann\Exporter\Exporter;
|
||||||
|
|
||||||
$exporter = new Exporter;
|
$exporter = new Exporter;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Array &0 (
|
Array &0 [
|
||||||
0 => Array &1 (
|
0 => Array &1 [
|
||||||
0 => 1
|
0 => 1,
|
||||||
1 => 2
|
1 => 2,
|
||||||
2 => 3
|
2 => 3,
|
||||||
)
|
],
|
||||||
1 => Array &2 (
|
1 => Array &2 [
|
||||||
0 => ''
|
0 => '',
|
||||||
1 => 0
|
1 => 0,
|
||||||
2 => false
|
2 => false,
|
||||||
)
|
],
|
||||||
)
|
]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
print $exporter->export(array(array(1,2,3), array("",0,FALSE)));
|
print $exporter->export([[1, 2, 3], ['', 0, false]]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Array &0 (
|
Array &0 [
|
||||||
'self' => Array &1 (
|
'self' => Array &1 [
|
||||||
'self' => Array &1
|
'self' => Array &1,
|
||||||
)
|
],
|
||||||
)
|
]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$array = array();
|
$array = [];
|
||||||
$array['self'] = &$array;
|
$array['self'] = &$array;
|
||||||
|
|
||||||
print $exporter->export($array);
|
print $exporter->export($array);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
stdClass Object &0000000003a66dcc0000000025e723e2 (
|
stdClass Object #4 (
|
||||||
'self' => stdClass Object &0000000003a66dcc0000000025e723e2
|
'self' => stdClass Object #4,
|
||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$obj = new stdClass();
|
$obj = new stdClass;
|
||||||
$obj->self = $obj;
|
$obj->self = $obj;
|
||||||
|
|
||||||
print $exporter->export($obj);
|
print $exporter->export($obj);
|
||||||
```
|
```
|
||||||
|
|
||||||
Compact exports:
|
Compact exports:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
use SebastianBergmann\Exporter\Exporter;
|
use SebastianBergmann\Exporter\Exporter;
|
||||||
|
|
||||||
$exporter = new Exporter;
|
$exporter = new Exporter;
|
||||||
|
|
||||||
// Array ()
|
// []
|
||||||
print $exporter->shortenedExport(array());
|
print $exporter->shortenedExport([]);
|
||||||
|
|
||||||
// Array (...)
|
// [...]
|
||||||
print $exporter->shortenedExport(array(1,2,3,4,5));
|
print $exporter->shortenedExport([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
// stdClass Object ()
|
// stdClass Object ()
|
||||||
print $exporter->shortenedExport(new stdClass);
|
print $exporter->shortenedExport(new stdClass);
|
||||||
|
|
@ -150,7 +152,7 @@ print $exporter->shortenedExport(new stdClass);
|
||||||
// Exception Object (...)
|
// Exception Object (...)
|
||||||
print $exporter->shortenedExport(new Exception);
|
print $exporter->shortenedExport(new Exception);
|
||||||
|
|
||||||
// this\nis\na\nsuper\nlong\nstring\nt...\nspace
|
// 'this\nis\na\nsuper\nlong\nstr...nspace'
|
||||||
print $exporter->shortenedExport(
|
print $exporter->shortenedExport(
|
||||||
<<<LONG_STRING
|
<<<LONG_STRING
|
||||||
this
|
this
|
||||||
|
|
@ -173,3 +175,181 @@ space
|
||||||
LONG_STRING
|
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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
190
src/Exporter.php
190
src/Exporter.php
|
|
@ -61,13 +61,13 @@ final readonly class Exporter
|
||||||
* @var positive-int
|
* @var positive-int
|
||||||
*/
|
*/
|
||||||
private int $maxLengthForStrings;
|
private int $maxLengthForStrings;
|
||||||
private ?ObjectExporterChain $objectExporter;
|
private ?ObjectExporter $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, ?ObjectExporterChain $objectExporter = null)
|
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40, ?ObjectExporter $objectExporter = null)
|
||||||
{
|
{
|
||||||
$this->shortenArraysLongerThan = $shortenArraysLongerThan;
|
$this->shortenArraysLongerThan = $shortenArraysLongerThan;
|
||||||
$this->maxLengthForStrings = $maxLengthForStrings;
|
$this->maxLengthForStrings = $maxLengthForStrings;
|
||||||
|
|
@ -86,15 +86,23 @@ final readonly class Exporter
|
||||||
* - Strings are always quoted with single quotes
|
* - Strings are always quoted with single quotes
|
||||||
* - Carriage returns and newlines are normalized to \n
|
* - Carriage returns and newlines are normalized to \n
|
||||||
* - Recursion and repeated rendering is treated properly
|
* - 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): string
|
public function export(mixed $value, int $indentation = 0, ?ExportContext $context = null): string
|
||||||
{
|
{
|
||||||
return $this->recursiveExport($value, $indentation);
|
return $this->recursiveExport($value, $indentation, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<mixed> $data
|
* @param array<mixed> $data
|
||||||
* @param positive-int $maxLengthForStrings
|
* @param positive-int $maxLengthForStrings
|
||||||
|
*
|
||||||
|
* @throws ObjectNotSupportedException
|
||||||
*/
|
*/
|
||||||
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string
|
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string
|
||||||
{
|
{
|
||||||
|
|
@ -128,7 +136,13 @@ final readonly class Exporter
|
||||||
* Newlines are replaced by the visible string '\n'.
|
* Newlines are replaced by the visible string '\n'.
|
||||||
* Contents of arrays and objects (if any) are replaced by '...'.
|
* 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
|
* @param positive-int $maxLengthForStrings
|
||||||
|
*
|
||||||
|
* @throws ObjectNotSupportedException
|
||||||
*/
|
*/
|
||||||
public function shortenedExport(mixed $value, int $maxLengthForStrings = 40): string
|
public function shortenedExport(mixed $value, int $maxLengthForStrings = 40): string
|
||||||
{
|
{
|
||||||
|
|
@ -137,13 +151,20 @@ final readonly class Exporter
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_string($value)) {
|
if (is_string($value)) {
|
||||||
$string = str_replace("\n", '', $this->exportString($value));
|
return $this->shorten($this->exportString($value), $maxLengthForStrings);
|
||||||
|
}
|
||||||
|
|
||||||
if (mb_strlen($string) > $maxLengthForStrings) {
|
if ($this->objectExporter !== null &&
|
||||||
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
|
is_object($value) &&
|
||||||
}
|
$this->objectExporter->handles($value)) {
|
||||||
|
$context = new ExportContext;
|
||||||
|
|
||||||
return $string;
|
$context->beginExportByObjectExporter($value);
|
||||||
|
|
||||||
|
return $this->shorten(
|
||||||
|
$this->objectExporter->export($value, $this, 0, $context),
|
||||||
|
$maxLengthForStrings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value instanceof BackedEnum) {
|
if ($value instanceof BackedEnum) {
|
||||||
|
|
@ -181,6 +202,26 @@ final readonly class Exporter
|
||||||
return $this->export($value);
|
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
|
* Converts an object to an array containing all of its private, protected
|
||||||
* and public properties.
|
* and public properties.
|
||||||
|
|
@ -314,6 +355,8 @@ final readonly class Exporter
|
||||||
/**
|
/**
|
||||||
* @param array<mixed> $data
|
* @param array<mixed> $data
|
||||||
* @param positive-int $maxLengthForStrings
|
* @param positive-int $maxLengthForStrings
|
||||||
|
*
|
||||||
|
* @throws ObjectNotSupportedException
|
||||||
*/
|
*/
|
||||||
private function shortenedCountedRecursiveExport(array &$data, RecursionContext $processed, int &$counter, int $maxLengthForStrings): string
|
private function shortenedCountedRecursiveExport(array &$data, RecursionContext $processed, int &$counter, int $maxLengthForStrings): string
|
||||||
{
|
{
|
||||||
|
|
@ -350,7 +393,10 @@ final readonly class Exporter
|
||||||
return implode(', ', $result);
|
return implode(', ', $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function recursiveExport(mixed &$value, int $indentation = 0, ?RecursionContext $processed = null): string
|
/**
|
||||||
|
* @throws ObjectNotSupportedException
|
||||||
|
*/
|
||||||
|
private function recursiveExport(mixed &$value, int $indentation = 0, ?ExportContext $context = null): string
|
||||||
{
|
{
|
||||||
if ($value === null) {
|
if ($value === null) {
|
||||||
return 'null';
|
return 'null';
|
||||||
|
|
@ -377,44 +423,42 @@ 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)) {
|
if (is_string($value)) {
|
||||||
return $this->exportString($value);
|
return $this->exportString($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($processed === null) {
|
if ($context === null) {
|
||||||
$processed = new RecursionContext;
|
$context = new ExportContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
return $this->exportArray($value, $processed, $indentation);
|
return $this->exportArray($value, $context, $indentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_object($value)) {
|
if (is_object($value)) {
|
||||||
return $this->exportObject($value, $processed, $indentation);
|
return $this->exportObject($value, $context, $indentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return var_export($value, true);
|
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
|
private function exportFloat(float $value): string
|
||||||
{
|
{
|
||||||
if (is_nan($value)) {
|
if (is_nan($value)) {
|
||||||
|
|
@ -485,15 +529,17 @@ final readonly class Exporter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<mixed> $value
|
* @param array<mixed> $value
|
||||||
|
*
|
||||||
|
* @throws ObjectNotSupportedException
|
||||||
*/
|
*/
|
||||||
private function exportArray(array &$value, RecursionContext $processed, int $indentation): string
|
private function exportArray(array &$value, ExportContext $context, int $indentation): string
|
||||||
{
|
{
|
||||||
if (($key = $processed->contains($value)) !== false) {
|
if (($key = $context->contains($value)) !== false) {
|
||||||
return 'Array &' . $key;
|
return 'Array &' . $key;
|
||||||
}
|
}
|
||||||
|
|
||||||
$array = $value;
|
$array = $value;
|
||||||
$key = $processed->add($value);
|
$key = $context->add($value);
|
||||||
$values = '';
|
$values = '';
|
||||||
|
|
||||||
if (count($array) > 0) {
|
if (count($array) > 0) {
|
||||||
|
|
@ -506,7 +552,7 @@ final readonly class Exporter
|
||||||
$this->recursiveExport($k, $indentation)
|
$this->recursiveExport($k, $indentation)
|
||||||
. ' => ' .
|
. ' => ' .
|
||||||
/** @phpstan-ignore offsetAccess.invalidOffset */
|
/** @phpstan-ignore offsetAccess.invalidOffset */
|
||||||
$this->recursiveExport($value[$k], $indentation + 1, $processed)
|
$this->recursiveExport($value[$k], $indentation + 1, $context)
|
||||||
. ",\n";
|
. ",\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -516,24 +562,68 @@ final readonly class Exporter
|
||||||
return 'Array &' . (string) $key . ' [' . $values . ']';
|
return 'Array &' . (string) $key . ' [' . $values . ']';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function exportObject(object $value, RecursionContext $processed, int $indentation): string
|
/**
|
||||||
|
* @throws ObjectNotSupportedException
|
||||||
|
*/
|
||||||
|
private function exportObject(object $value, ExportContext $context, 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;
|
$class = $value::class;
|
||||||
|
|
||||||
if ($processed->contains($value) !== false) {
|
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) {
|
||||||
return $class . ' Object #' . spl_object_id($value);
|
return $class . ' Object #' . spl_object_id($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$processed->add($value);
|
$context->add($value);
|
||||||
|
|
||||||
$array = $this->toArray($value);
|
$array = $this->toArray($value);
|
||||||
$buffer = '';
|
$buffer = '';
|
||||||
|
|
@ -547,7 +637,7 @@ final readonly class Exporter
|
||||||
. ' ' .
|
. ' ' .
|
||||||
$this->recursiveExport($k, $indentation)
|
$this->recursiveExport($k, $indentation)
|
||||||
. ' => ' .
|
. ' => ' .
|
||||||
$this->recursiveExport($v, $indentation + 1, $processed)
|
$this->recursiveExport($v, $indentation + 1, $context)
|
||||||
. ",\n";
|
. ",\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,12 @@ interface ObjectExporter
|
||||||
public function handles(object $object): bool;
|
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
|
* @throws ObjectNotSupportedException
|
||||||
*/
|
*/
|
||||||
public function export(object $object, Exporter $exporter, int $indentation): string;
|
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ final readonly class ObjectExporterChain implements ObjectExporter
|
||||||
/**
|
/**
|
||||||
* @throws ObjectNotSupportedException
|
* @throws ObjectNotSupportedException
|
||||||
*/
|
*/
|
||||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||||
{
|
{
|
||||||
foreach ($this->exporters as $objectExporter) {
|
foreach ($this->exporters as $objectExporter) {
|
||||||
if ($objectExporter->handles($object)) {
|
if ($objectExporter->handles($object)) {
|
||||||
return $objectExporter->export($object, $exporter, $indentation);
|
return $objectExporter->export($object, $exporter, $indentation, $context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ use SplObjectStorage;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
#[CoversClass(Exporter::class)]
|
#[CoversClass(Exporter::class)]
|
||||||
|
#[UsesClass(ExportContext::class)]
|
||||||
#[UsesClass(ObjectExporterChain::class)]
|
#[UsesClass(ObjectExporterChain::class)]
|
||||||
#[Small]
|
#[Small]
|
||||||
final class ExporterTest extends TestCase
|
final class ExporterTest extends TestCase
|
||||||
|
|
@ -669,6 +670,20 @@ 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
|
public function testObjectNestedInArrayCanBeExportedByCustomObjectExporter(): void
|
||||||
{
|
{
|
||||||
$exporter = new Exporter(
|
$exporter = new Exporter(
|
||||||
|
|
@ -784,6 +799,281 @@ 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
|
private function trimNewline(string $string): string
|
||||||
{
|
{
|
||||||
return (string) preg_replace('/[ ]*\n/', "\n", $string);
|
return (string) preg_replace('/[ ]*\n/', "\n", $string);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use stdClass;
|
||||||
|
|
||||||
#[CoversClass(ObjectExporterChain::class)]
|
#[CoversClass(ObjectExporterChain::class)]
|
||||||
#[UsesClass(Exporter::class)]
|
#[UsesClass(Exporter::class)]
|
||||||
|
#[UsesClass(ExportContext::class)]
|
||||||
#[Small]
|
#[Small]
|
||||||
final class ObjectExporterChainTest extends TestCase
|
final class ObjectExporterChainTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -55,12 +56,12 @@ final class ObjectExporterChainTest extends TestCase
|
||||||
|
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
'stdClass handled by custom exporter',
|
'stdClass handled by custom exporter',
|
||||||
$chain->export(new stdClass, new Exporter, 0),
|
$chain->export(new stdClass, new Exporter, 0, new ExportContext),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
ExampleClass::class . ' (indentation: 0)',
|
ExampleClass::class . ' (indentation: 0)',
|
||||||
$chain->export(new ExampleClass('foo'), new Exporter, 0),
|
$chain->export(new ExampleClass('foo'), new Exporter, 0, new ExportContext),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +75,6 @@ final class ObjectExporterChainTest extends TestCase
|
||||||
|
|
||||||
$this->expectException(ObjectNotSupportedException::class);
|
$this->expectException(ObjectNotSupportedException::class);
|
||||||
|
|
||||||
$chain->export(new stdClass, new Exporter, 0);
|
$chain->export(new stdClass, new Exporter, 0, new ExportContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ final readonly class ObjectExporterThatHandlesEveryObject implements ObjectExpor
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||||
{
|
{
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'%s (indentation: %d)',
|
'%s (indentation: %d)',
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ final readonly class ObjectExporterThatHandlesNoObject implements ObjectExporter
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||||
{
|
{
|
||||||
throw new ObjectNotSupportedException;
|
throw new ObjectNotSupportedException;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ final readonly class ObjectExporterThatHandlesObjectsOfSpecificType implements O
|
||||||
return $object instanceof $this->type;
|
return $object instanceof $this->type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function export(object $object, Exporter $exporter, int $indentation): string
|
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||||
{
|
{
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'%s handled by custom exporter',
|
'%s handled by custom exporter',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue