Consult custom object exporter when exporting a value into a single-line string
This commit is contained in:
parent
60d658c5b5
commit
1ff285c572
|
|
@ -7,6 +7,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
|
||||
|
||||
## [8.1.1] - 2026-07-13
|
||||
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -319,4 +319,18 @@ Array &0 [
|
|||
print $exporter->export(['basket' => new Basket([new Money(1999, 'EUR')])]);
|
||||
```
|
||||
|
||||
Object exporters are not consulted by `Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()`, which deliberately elide the contents of objects.
|
||||
### 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.
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ final readonly class Exporter
|
|||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string
|
||||
{
|
||||
|
|
@ -134,7 +136,13 @@ 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
|
||||
{
|
||||
|
|
@ -143,13 +151,20 @@ final readonly class Exporter
|
|||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$string = str_replace("\n", '', $this->exportString($value));
|
||||
return $this->shorten($this->exportString($value), $maxLengthForStrings);
|
||||
}
|
||||
|
||||
if (mb_strlen($string) > $maxLengthForStrings) {
|
||||
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
|
||||
}
|
||||
if ($this->objectExporter !== null &&
|
||||
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) {
|
||||
|
|
@ -320,6 +335,8 @@ 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
|
||||
{
|
||||
|
|
@ -405,6 +422,23 @@ final readonly class Exporter
|
|||
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)) {
|
||||
|
|
|
|||
|
|
@ -845,6 +845,100 @@ EOT,
|
|||
);
|
||||
}
|
||||
|
||||
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 testCustomObjectExporterCanExportValuesNestedInObjectItHandles(): void
|
||||
{
|
||||
$exporter = new 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