Plain PHP, copy-on-write clones and lazy proxies in symfony/var-exporter
Provides tools to export, instantiate, hydrate, clone and lazy-load PHP objects
At a glance
- What is it?
- symfony/var-exporter is a small PHP component with three documented tools: an exporter that writes object state as plain PHP code, a deep cloner that keeps copy-on-write, and a proxy generator that defers construction. The speed story rests on OPcache and a native extension, and two root classes stay undocumented.
- Who is it for?
- Adopt symfony/var-exporter when you cache PHP state as compiled code and want copy-on-write clones rather than byte copies, and step back when your platform forbids PHP extensions or your classes include Reflection* or IteratorIterator types, which it refuses to export.
- Can I use it commercially?
- Yes. MIT 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 October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Export writes plain PHP code, not a serialised blob
Symfony's VarExporter component targets teams that keep PHP object state outside the running process: a warmup pass that writes a cache file, message payloads, test fixtures, anything a worker later has to rebuild in memory. The surface is small, and the repository root shows it. DeepCloner.php, VarExporter.php, ProxyHelper.php, Instantiator.php and Hydrator.php sit beside composer.json, with Exception/ and Internal/ directories behind them.
`VarExporter::export()` takes a serializable PHP data structure and returns plain PHP code for it. What separates that code from a hand-written cache file is that it preserves the semantics of PHP's own serialization machinery: `__wakeup`, `__sleep`, `Serializable`, `__serialize` and `__unserialize` still do their work when the generated file runs.
The README carries no install command and no configuration file, so there is nothing to paste into a terminal before you start. It points to https://symfony.com/doc/current/components/var_exporter.html for documentation and treats composer.json as the only packaging manifest in the tree. Pull the package in the way your application already handles Symfony dependencies, call the class directly and skip the container, since no service definition or bundle wiring appears anywhere in this repository.
OPcache decides the argument for generated code
The comparison the README draws is not format fidelity but load time. Against `serialize()` and igbinary, `VarExporter::export()` is said to win because OPcache compiles the generated file once, so that code then runs faster and uses less memory than `unserialize()` or `igbinary_unserialize()`. The beneficiary is exactly the cache-and-ship pattern, where the same file gets loaded over and over.
`var_export()` is the other baseline, and the difference there is scope rather than speed: `var_export()` handles a narrow set of values while this exporter works on any serializable PHP value. Two smaller details matter for a file a human will read later. Output is PSR-2 compatible, and the exporter can be re-indented without corrupting carriage returns or newlines embedded in the data, which naive string formatting turns into a file that no longer parses.
Missing classes fail loudly instead of becoming PHP_Incomplete_Class
Silent degradation is the failure mode this component designs against. A class that disappeared between writing and reading a cache is not quietly revived as a `PHP_Incomplete_Class` object; `VarExporter::export()` raises `ClassNotFoundException` at export time instead, so the failure lands where the mistake is rather than three frames deep inside a worker.
Shared references get the same care. When a graph holds `SplObjectStorage`, `ArrayObject` or `ArrayIterator` instances, references into those objects survive export, which counts whenever one instance is reachable from two branches of the tree and identity has to hold.
Not every class is exportable, and the component admits that plainly: `Reflection*` classes, `IteratorIterator` and `RecursiveIteratorIterator` raise an exception instead of producing output, with https://bugs.php.net/76737 cited as the reason. Loud failure, readable file, and 3 class families you cannot export at all: that is the trade.
DeepCloner keeps copy-on-write instead of copying bytes
`DeepCloner::deepClone()` copies an object graph without serialising it first. PHP strings and arrays share storage until one side writes to it, and a clone made this way keeps that sharing, whereas `unserialize(serialize())` writes a second physical copy of every byte. The README states the outcome as lower memory usage and better performance, and the sample is one line:
$clone = DeepCloner::deepClone($originalObject);Repeated cloning of one structure is the case worth tuning, because the part that costs is graph analysis. Binding the prototype once pays for it a single time:
$cloner = new DeepCloner($prototype);
$clone1 = $cloner->clone();
$clone2 = $cloner->clone();Read that signature carefully: `clone()` takes no arguments, so the shape being copied is fixed when the cloner is constructed.
Hydration without a constructor rides on ext-deepclone
Instantiating and hydrating objects is the third job, and it is the one with something to install. The component prefers the native `ext-deepclone` extension and falls back to its polyfill when the extension is absent. That extension supplies `deepclone_to_array()` and `deepclone_from_array()`, which `DeepCloner` and `VarExporter::export()` both lean on, plus `deepclone_hydrate()`, which builds or fills an object without calling its constructor and can write private, protected and readonly properties.
So the deployment profile is part of the design. The performance statements belong to the native path, and the polyfill keeps the API callable without keeping the speed. Where a platform forbids loading PHP extensions, work out which path you are on before anyone builds a throughput expectation around cloning.
For constructor-free construction you do not need that extension at all, because Instantiator.php and Hydrator.php are top-level files in this repository. The README names neither of them, which leaves their exact API to the source.
Native lazy objects in PHP 8.4 stop at abstract classes
PHP 8.4 added lazy objects through the reflection API, and the split inside this component follows that API exactly. The native form handles concrete classes. It does not handle abstracts or internal classes, which rules out interfaces and most of the framework services worth deferring.
For those cases the component generates a decorator instead, source text for a lazy-loading proxy class built around the abstract type you hand it:
$proxyCode = ProxyHelper::generateLazyProxy(new ReflectionClass(AbstractFoo::class));
// $proxyCode should be dumped into a file in production envs
eval('class FooLazyProxy'.$proxyCode);
$foo = FooLazyProxy::createLazyProxy(initializer: function (): AbstractFoo {
// [...] Use whatever heavy logic you need here
// to compute the $dependencies of the $instance
$instance = new Foo(...$dependencies);
// [...] Call setters, etc. if needed
return $instance;
});
// $foo is now a lazy-loading decorator object. The initializer will
// be called only when and if a *method* is called.The version numbers do not line up, and a close read catches it: the README credits native lazy objects to version 8.4 of PHP, while this repository's default branch is 8.2 and its last push was on 2026-09-26. What that branch ships is the decorator path, which predates the native feature and still covers exactly what the native one skips.
Eval decides where a generated proxy can run
That sample makes one deployment requirement plain. `ProxyHelper::generateLazyProxy()` returns source text, and the README turns it into a live class with `eval`, while a comment in the same sample tells you to dump `$proxyCode` into a file in production instead. Hosts that disable `eval` break the first form, and nothing else in the repository covers that case.
The second requirement is that somebody generated the file. Proxy source is not something a fresh checkout can be assumed to have, because the sample builds it at runtime from a `ReflectionClass`. Generating at boot spends code generation on every process start; committing the generated file means a build step has to refresh it whenever the abstract class changes shape.
The payoff is narrow, and worth stating exactly: the initializer passed to `createLazyProxy` runs only when and if a method is called on the proxy. An injected service nobody touches then costs nothing beyond creating the decorator.
Three root files the README never names
The repository root is where the documentation gap is widest. Top-level entries include Hydrator.php, Instantiator.php, LazyObjectInterface.php, an Exception/ directory, Internal/, Tests/ and phpunit.xml.dist. The README describes 3 tools, `VarExporter::export()`, `DeepCloner` and `ProxyHelper::generateLazyProxy()`, and it names none of those three PHP files, so what Hydrator and Instantiator accept stays a question for the source.
The gap is easier to measure against the component description, which promises tools to export, instantiate, hydrate, clone and lazy-load PHP objects. That is 5 actions, and the README gives first-class treatment to 3. Instantiate and hydrate survive only as a by-product of the extension function `deepclone_hydrate()`, described as something extra that the extension provides on top of what `DeepCloner` and `VarExporter::export()` already use.
MIT is the licence identifier recorded for the package, and the README closes with backer and sponsor links to https://symfony.com/backers and https://symfony.com/sponsor rather than any warranty text. Whether that identifier fits a codebase you intend to ship closed is a question for your own reviewer.
Editorial conclusion
Adopt symfony/var-exporter when you cache PHP state as compiled code and want copy-on-write clones rather than byte copies, and step back when your platform forbids PHP extensions or your classes include Reflection* or IteratorIterator types, which it refuses to export. Before wiring anything in, read Hydrator.php and Instantiator.php, the two root classes the README never explains, check whether ext-deepclone or its polyfill is what your target platform will load, and decide whether proxy source is generated at boot or committed as a file.
Frequently asked questions
What does symfony/var-exporter do?
It offers three documented tools: export, which writes any serializable PHP value as plain PHP code; DeepCloner, which deep-clones values while keeping copy-on-write for strings and arrays; and ProxyHelper::generateLazyProxy(), which generates lazy-loading decorators for abstract classes, internal classes and interfaces.
How do I install symfony/var-exporter?
The README carries no install command and no configuration keys. It points at the documentation at https://symfony.com/doc/current/components/var_exporter.html and keeps composer.json as the only packaging manifest in the repository root.
Why use VarExporter::export() instead of serialize() or igbinary?
The README argues from load time: thanks to OPcache, the generated code runs faster and uses less memory than unserialize() or igbinary_unserialize(). Unlike var_export(), it works on any serializable PHP value, and it keeps __wakeup, __sleep, Serializable, __serialize and __unserialize semantics.
Does symfony/var-exporter need a PHP extension?
It depends on the native ext-deepclone extension for maximum performance and falls back to its polyfill when the extension is absent. That extension also provides deepclone_hydrate(), which instantiates or hydrates objects without calling their constructor, including private, protected and readonly properties.
Can symfony/var-exporter make an interface or an abstract class lazy?
Yes, through a generated decorator rather than a native lazy object, since the PHP 8.4 reflection API handles concrete classes only. ProxyHelper::generateLazyProxy() returns proxy source that the README evaluates with eval, or that you dump into a file in production.
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/symfony-var-exporter)Was this useful?