sebastian/exporter: printing PHP values with stable identity markers
Provides the functionality to export PHP variables for visualization
At a glance
- What is it?
- sebastian/exporter is a small PHP library whose only job is to turn a value into a readable string, with identity markers that show when two positions in an export point at the same array or object. It also has a shortened form for output that should stay small, and an interface for taking over how a type of object is rendered.
- Who is it for?
- Reach for sebastian/exporter when you are writing the code that reports a value back to a person: a test failure message, a debug dump, a diagnostic string. The identity markers and the object exporter interface solve problems that get overlooked as soon as a structure nests, and the shortened form keeps long output from swamping a test report.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- sebastianbergmann/exporter6,810 ★ · -1
| Repository | 2026-09-23 | 2026-11-13 |
|---|---|---|
| sebastianbergmann/exporter | 6.8k | 6.8k |
-1 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
One constructor, one method, one string back
The whole public surface is small. You construct an Exporter, hand it a value, and get a string:
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
print $exporter->export(new Exception);The component states its purpose in one line: it provides the functionality to export PHP variables for visualization. Nothing else is claimed for it, and nothing else is needed. The value you pass comes back rendered, with the structure visible rather than the class internals collapsed into a single word.
Adding the dependency is a Composer operation, and there are two forms of it:
composer require sebastian/exporterIf you only need the library during development, for instance to run a project's test suite, the development-time form is the one to use:
composer require --dev sebastian/exporterThat distinction decides whether the package is installed for production at all, and it is the first thing worth getting right.
Scalar types each get a notation of their own
The interesting decisions show up in how each type is written. An integer prints as itself, a float prints as itself, a string prints in quotes, and false and null print as themselves. The special cases are the ones worth knowing about, because they are where a naive exporter usually gives up. NAN prints as NAN, taken from acos(8). Negative infinity prints as -INF, taken from log(0). An open file handle prints as a resource with its type:
print $exporter->export(fopen('php://stderr', 'w'));and the result is written as resource(13) of type (stream), where the number is the handle identifier.
Binary strings get their own notation too, written as Binary String: followed by a hexadecimal dump, so a run of low bytes becomes 0x000102030405 rather than a series of unprintable characters.
There is a second entry point for output that should stay short:
print $exporter->shortenedExport([]);
print $exporter->shortenedExport([1, 2, 3, 4, 5]);
print $exporter->shortenedExport(new stdClass);An empty array stays as [], a five element array collapses to [...], an object with no properties becomes stdClass Object (), and a long string is cut with an ellipsis in the middle rather than at the end.
The #n and &n markers are the point of the format
What separates this from printing a value naively is the identity tracking. Arrays are written as Array &0, Array &1, Array &2, with the number identifying one particular array for the whole export, and objects carry the same idea as a hash, so an exception renders as Exception Object #4 with its message, code, file, line and previous listed underneath.
Those numbers pay off in the cases where a naive dump is useless. A nested array prints as Array &0 [ 0 => Array &1 [...], 1 => Array &2 [...] ], and a structure that contains itself prints its own reference rather than expanding forever. When the same object appears twice in one export, the default representation is used for the first occurrence and the second becomes a bare reference to it, so the same Money object shows up once with its amount and currency and then again as a name. An object that is indirectly nested in itself gets the same treatment, and the reason is stated plainly: it cannot be exported without recursing infinitely.
The boundary here is worth stating too. The exporter returns a string and nothing else.
An ObjectExporter owns the whole representation, not part of it
By default an object is exported property by property. To change that for a type, you implement the ObjectExporter interface, which has two methods. handles() decides whether the exporter applies, and export() produces the string:
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());
}
}Registration happens through Exporter's constructor, using the objectExporter argument, and it is consulted before the default representation is used. Because it is a named argument at construction time, an exporter applies to the whole Exporter rather than to a single call, which means the choice is made once per Exporter instance. When no object exporter handles an object, the default property by property output is used, so adding one is additive. Enums are objects and are passed to object exporters as well.
The catch is that an object exporter is responsible for the entire representation of the object it handles. Return a plain string and that object loses its identity marker, and because every occurrence is exported by it, the repeated occurrence rule stops applying to it too.
A chain of exporters resolves in composition order, first match wins
Several object exporters can be composed into one with ObjectExporterChain. 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:
$exporter = new Exporter(
objectExporter: new ObjectExporterChain(
[
new BasketExporter,
new MoneyExporter,
],
),
);The ordering is the entire policy. There is no priority field, no specificity score and no way to express that one exporter should be consulted first for a subclass relationship, so a chain that grows over time is a list whose meaning depends on position. A broad exporter placed early will quietly shadow every narrower one behind it, and the failure is invisible because the output is still a plausible rendering of the object.
The same rule explains why handles() is worth writing narrowly. Returning true for a base class in an early position is enough to take over types you never intended to touch, and the objects affected are exactly the ones whose default representation would have been most useful for comparison.
Pass the ExportContext on, or references get assigned twice
This is the sharpest rule in the library and the easiest to break. An object exporter may use the Exporter it is given to export values nested inside the object it handles, and when it does, the ExportContext it is given has to be passed on as well:
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
assert($object instanceof Basket);
return 'Basket ' . $exporter->export($object->items(), $indentation, $context);
}The reason is not style. Without the context argument, the export of those 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. In other words, forgetting one argument does not produce a warning or a wrong-looking string; it produces identity numbers that have already been handed out in a different part of the same output. The Basket example shows the intended shape, where a basket's items are exported through the same Exporter and the result reads as Basket followed by the array.
The context is a required parameter of export(), which makes it hard to skip.
Ant, Phive and infection sit next to a Composer package
The repository is laid out like a library from a long standing PHP toolchain vendor rather than a plain library. src/ holds the code, tests/ the test suite, build/ auxiliary build output, and tools/ the third-party binaries that are not declared as Composer dependencies. build.xml is an Ant build file, .phive/ is the configuration for Phive, which is what manages those tools, and phpstan.neon, phpunit.xml, infection.json5.dist and .php-cs-fixer.dist.php cover static analysis, the test runner, mutation testing and code style. renovate.json handles dependency updates and SECURITY.md states how to report a problem.
The licence is BSD-3-Clause, with LICENSE and ChangeLog.md in the tree. Release numbering is where the cadence shows: 8.1.1 was published on 2026-07-13, and 8.2.0 and 8.2.1 were both published on 2026-08-07, two minutes apart, which reads as a feature release followed immediately by a fix. The last push to main was 2026-11-02, so the branch has moved on since those tags.
None of that machinery is required to use the library. It matters to you only if you intend to read the source and follow its reasoning, or to run the same checks before opening a pull request.
Editorial conclusion
Reach for sebastian/exporter when you are writing the code that reports a value back to a person: a test failure message, a debug dump, a diagnostic string. The identity markers and the object exporter interface solve problems that get overlooked as soon as a structure nests, and the shortened form keeps long output from swamping a test report. Do not reach for it when you need to serialise a value for storage or transport, since nothing here converts the output back into PHP values. Before adding it to a project, check the minimum PHP version this release needs against the one your composer.json already enforces.
Frequently asked questions
What does sebastian/exporter do?
It exports PHP variables for visualization. You construct an Exporter and call its export method with a value, and you get back a string that renders arrays, objects and scalars, with identity markers such as Array &0 and Exception Object #4 that show when the same value appears more than once.
How do I install sebastian/exporter?
With Composer, either as a per-project dependency using composer require sebastian/exporter, or as a development-time dependency using composer require --dev sebastian/exporter when you only need it while developing, for instance to run your project's test suite.
How do I control how an object is exported in sebastian/exporter?
Implement the ObjectExporter interface, which requires a handles() method and an export() method, then pass the instance to Exporter's constructor through the objectExporter argument. It is consulted before the default property by property representation, and when nothing handles an object the default is used. Enums reach object exporters as well.
How do I keep exported output short in sebastian/exporter?
Use the shortenedExport method instead of export. An empty array prints as [], a five element array prints as [...], an object with no properties prints as stdClass Object (), and a long string is shortened with an ellipsis in the middle.
What happens when the same object is exported twice in sebastian/exporter?
The default representation is used for the first occurrence and subsequent occurrences are replaced with a reference to that object. An object exporter handles every occurrence itself, so a custom representation appears for all of them, except when an object is indirectly nested in itself, which is also replaced with a reference because exporting it would recurse infinitely.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sebastianbergmann-exporter)Star history chart for your README
[](https://hysenlabs.com/projects/sebastianbergmann-exporter)Was this useful?