Varexporter
A powerful alternative to var_export(), which can export closures and objects without __set_state()
Install / Use
/learn @brick/VarexporterREADME
Brick\VarExporter
<img src="https://raw.githubusercontent.com/brick/brick/master/logo.png" alt="" align="left" height="64">A powerful and pretty replacement for PHP's var_export().
Introduction
PHP's var_export() function is a handy way to export a variable as executable PHP code.
It is particularly useful to store data that can be cached by OPCache, just like your source code, and later retrieved very fast, much faster than unserializing data using unserialize() or json_decode().
But it also suffers from several drawbacks:
- It cannot export custom objects that do not implement
__set_state(), and__set_state()does not play well with private properties in parent classes, which makes the implementation tedious - It does not support closures
Additionally, the output is not very pretty:
- It outputs arrays as
array()notation, instead of the short[]notation - It outputs numeric arrays with explicit and unnecessary
0 => ...key => value syntax
This library aims to provide a prettier, safer, and powerful alternative to var_export().
The output is valid and standalone PHP code, that does not depend on the brick/varexporter library.
Installation
This library is installable via Composer:
composer require brick/varexporter
Requirements
This library requires PHP 8.2 or later.
For PHP 8.1 compatibility, you can use version 0.6. For PHP 7.4 and PHP 8.0, you can use version 0.5. For PHP 7.2 & 7.3, you can use version 0.3. Note that these PHP versions are EOL and not supported anymore. If you're still using one of these PHP versions, you should consider upgrading as soon as possible.
Project status & release process
While this library is still under development, it is well tested and should be stable enough to use in production environments.
The current releases are numbered 0.x.y. When a non-breaking change is introduced (adding new methods, optimizing existing code, etc.), y is incremented.
When a breaking change is introduced, a new 0.x version cycle is always started.
It is therefore safe to lock your project to a given release cycle, such as 0.7.*.
If you need to upgrade to a newer release cycle, check the release history for a list of changes introduced by each further 0.x.0 version.
Quickstart
This library offers a single method, VarExporter::export() which works pretty much like var_export():
use Brick\VarExporter\VarExporter;
echo VarExporter::export([1, 2, ['foo' => 'bar', 'baz' => []]]);
This code will output:
[
1,
2,
[
'foo' => 'bar',
'baz' => []
]
]
Compare this to the var_export() output:
array (
0 => 1,
1 => 2,
2 =>
array (
'foo' => 'bar',
'baz' =>
array (
),
),
)
Note: unlike var_export(), export() always returns the exported variable, and never outputs it.
Exporting custom objects
var_export() assumes that every object has a static __set_state() method that takes an associative array of property names to values, and returns a object.
This means that if you want to export an instance of a class outside your control, you're out of luck. This also means that you have to write boilerplate code for your classes, that looks like:
class Foo
{
public $a;
public $b;
public $c;
public static function __set_state(array $array) : self
{
$object = new self;
$object->a = $array['a'];
$object->b = $array['b'];
$object->c = $array['c'];
return $object;
}
}
Or the more dynamic, reusable, and less IDE-friendly version:
public static function __set_state(array $array) : self
{
$object = new self;
foreach ($array as $key => $value) {
$object->{$key} = $value;
}
return $object;
}
If your class has a parent with private properties, you may have to do some gymnastics to write the value, and if your class overrides a private property of one of its parents, you're out of luck as var_export() puts all properties in the same bag, outputting an array with a duplicate key.
What does VarExporter do instead?
It determines the most appropriate method to export your object, in this order:
-
If your custom class has a
__set_state()method,VarExporteruses it by default, just likevar_export()would do:\My\CustomClass::__set_state([ 'foo' => 'Hello', 'bar' => 'World' ])The array passed to
__set_state()will be built with the same semantics used byvar_export(); this library aims to be 100% compatible in this regard. The only difference is when your class has overridden private properties:var_export()will output an array that contains the same key twice (resulting in data loss), whileVarExporterwill throw anExportExceptionto keep you on the safe side.Unlike
var_export(), this method will only be used if actually implemented on the class.You can disable exporting objects this way, even if they implement
__set_state(), using theNO_SET_STATEoption. -
If your class has
__serialize()and__unserialize()methods,VarExporteruses the output of__serialize()to export the object, and gives it as input to__unserialize()to reconstruct the object:(static function() { $class = new \ReflectionClass(\My\CustomClass::class); $object = $class->newInstanceWithoutConstructor(); $object->__unserialize([ 'foo' => 'Test', 'bar' => 1234 ]); return $object; })()This method is recommended for exporting complex custom objects: it is compatible with the new serialization mechanism introduced in PHP 7.4, flexible, safe, and composes very well under inheritance.
If for any reason you do not want to export objects that implement
__serialize()and__unserialize()using this method, you can opt out by using theNO_SERIALIZEoption. -
If the class does not meet any of the conditions above, it is exported through direct property access, which in its simplest form looks like:
(static function() { $object = new \My\CustomClass; $object->publicProp = 'Foo'; $object->dynamicProp = 'Bar'; return $object; })()If the class has a constructor, it will be bypassed using reflection:
(static function() { $class = new \ReflectionClass(\My\CustomClass::class); $object = $class->newInstanceWithoutConstructor(); ... })()If the class has non-public properties, they will be accessed through closures bound to the object:
(static function() { $class = new \ReflectionClass(\My\CustomClass::class); $object = $class->newInstanceWithoutConstructor(); $object->publicProp = 'Foo'; $object->dynamicProp = 'Bar'; (function() { $this->protectedProp = 'contents'; $this->privateProp = 'contents'; })->bindTo($object, \My\CustomClass::class)(); (function() { $this->privatePropInParent = 'contents'; })->bindTo($object, \My\ParentClass::class)(); return $object; })()You can disable exporting objects this way, using the
NOT_ANY_OBJECToption.
If you attempt to export a custom object and all compatible exporters have been disabled, an ExportException will be thrown.
Exporting closures
Since version 0.2.0, VarExporter has experimental support for closures:
echo VarExporter::export([
'callback' => function() {
return 'Hello, world!';
}
]);
[
'callback' => function () {
return 'Hello, world!';
}
]
To do this magic, VarExporter parses the PHP source file where your closure is defined, using the well-established nikic/php-parser library, inspired by SuperClosure.
To ensure that the closure will work in any context, it rewrites its source code, replacing any namespaced class/function/constant name with its fully qualified counterpart:
namespace My\App;
use My\App\Model\Entity;
use function My\App\Functions\imported_function;
use const My\App\Constants\IMPORTED_CONSTANT;
use Brick\VarExporter\VarExporter;
echo VarExporter::export(function(Service $service) : Entity {
strlen(NON_NAMESPACED_CONSTANT);
imported_function(IMPORTED_CONSTANT);
\My\App\Functions\explicitly_namespaced_function(\My\App\Constants\EXPLICITLY_NAMESPACED_CONSTANT);
return new Entity();
});
function (\My\App\Service $service) : \My\App\Model\Entity {
strlen(NON_NAMESPACED_CONSTANT);
\My\App\Functions\imported_function(\My\App\Constants\IMPORTED_CONSTANT);
\My\App\Functions\explicitly_namespaced_function(\My\App\Constants\EXPLICITLY_NAMESPACED_CONSTANT);
return new \My\App\Mode
