Home

Awesome

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().

Build Status Coverage Status Latest Stable Version Total Downloads License

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:

Additionally, the output is not very pretty:

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 7.4 or later.

For PHP 7.2 & 7.3 compatibility, you can use version 0.3. For PHP 7.1, you can use version 0.2. 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.5.*.

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 screwed up. 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 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\Model\Entity();
}

Note how all namespaced classes, and explicitly namespaced functions and constants, have been rewritten, while the non-namespaced function strlen() and the non-namespaced constant have been left as is. Please see the first caveat.

Use statements

By default, exporting closures that have variables bound through use() will throw an ExportException. This is intentional, because exported closures can be executed in another context, and as such must not rely on the context they've been originally defined in.

When using the CLOSURE_SNAPSHOT_USES option, VarExporter will export the current value of each use() variable instead of throwing an exception. The exported variables are added as expression inside the exported closure.

$planet = 'world';

echo VarExporter::export([
    'callback' => function(string $greeting) use ($planet) {
        return $greeting . ', ' . $planet . '!';
    }
], VarExporter::CLOSURE_SNAPSHOT_USE);
[
    'callback' => function (string $greeting) {
        $planet = 'world';
        return $greeting . ', ' . $planet . '!';
    }
]

Arrow functions

PHP supports shorthand syntax for closures (since PHP 7.4), also known as arrow functions. VarExporter will export these as normal closures.

Arrow functions can implicitly use variables from the context they've been defined in. If any context variable is used in the arrow function, VarExporter will throw an ExportException unless the CLOSURE_SNAPSHOT_USES option is used.

$planet = 'world';

echo VarExporter::export([
    'callback' => fn(string $greeting) => $greeting . ', ' . $planet . '!';
], VarExporter::CLOSURE_SNAPSHOT_USES);
[
    'callback' => function (string $greeting) {
        $planet = 'world';
        return $greeting . ', ' . $planet . '!';
    }
]

Caveats

You can disable exporting closures, using the NO_CLOSURES option. When this option is set, an ExportException will be thrown when attempting to export a closure.

Options

VarExporter::export() accepts a bitmask of options as a second parameter:

VarExporter::export($var, VarExporter::ADD_RETURN | VarExporter::ADD_TYPE_HINTS);

Available options:

VarExporter::ADD_RETURN

Wraps the output in a return statement:

return (...);

This makes the code ready to be executed in a PHP file―or eval(), for that matter.

VarExporter::ADD_TYPE_HINTS

Adds type hints to objects created through reflection, and to $this inside closures bound to an object. This allows the resulting code to be statically analyzed by external tools and IDEs:

/** @var \My\CustomClass $object */
$object = $class->newInstanceWithoutConstructor();

(function() {
    /** @var \My\CustomClass $this */
    $this->privateProp = ...;
})->bindTo($object, \My\CustomClass::class)();

VarExporter::SKIP_DYNAMIC_PROPERTIES

Skips dynamic properties on custom classes in the output. Dynamic properties are properties that are not part of the class definition, and added to an object at runtime. By default, any dynamic property set on a custom class is exported; if this option is used, dynamic properties are only allowed on stdClass objects, and ignored on other objects.

VarExporter::NO_SET_STATE

Disallows exporting objects through __set_state().

VarExporter::NO_SERIALIZE

Disallows exporting objects through __serialize() and __unserialize().

VarExporter::NOT_ANY_OBJECT

Disallows exporting any custom object using direct property access and bound closures.

VarExporter::NO_CLOSURES

Disallows exporting closures.

VarExporter::INLINE_ARRAY

Formats arrays on a single line:

VarExporter::export([
    'one' => ['hello', 'world', 123, true, false, null, 7.5],
    'two' => ['hello', 'world', [
        'one',
        'two',
        'three'
    ]]
], VarExporter::INLINE_ARRAY);
['one' => ['hello', 'world', 123, true, false, null, 7.5], 'two' => ['hello', 'world', ['one', 'two', 'three']]]

VarExporter::INLINE_SCALAR_LIST

Formats numeric arrays containing only scalar values on a single line:

VarExporter::export([
    'one' => ['hello', 'world', 123, true, false, null, 7.5],
    'two' => ['hello', 'world', ['one', 'two', 'three']]
], VarExporter::INLINE_SCALAR_LIST);
[
    'one' => ['hello', 'world', 123, true, false, null, 7.5],
    'two' => [
        'hello',
        'world',
        ['one', 'two', 'three']
    ]
]

Types considered scalar here are int, bool, float, string and null.

This option is a subset of INLINE_ARRAY, and has no effect when INLINE_ARRAY is used.

VarExporter::TRAILING_COMMA_IN_ARRAY

Adds a trailing comma after the last item of non-inline arrays:

VarExporter::export(
    ['hello', 'world', ['one', 'two', 'three']],
    VarExporter::TRAILING_COMMA_IN_ARRAY | VarExporter::INLINE_SCALAR_LIST
);
[
    'hello',
    'world',
    ['one', 'two', 'three'],
]

VarExporter::CLOSURE_SNAPSHOT_USES

Export the current value of each use() variable as expression inside the exported closure.

Indentation

You can use the 3rd argument of VarExporter::export() to control the indentation level. This is useful when you want to use the generated code string to replace a placeholder in a template used to generate code files.

So using output of VarExporter::export(['foo' => 'bar'], indentLevel: 1) in the template below to replace {{exported}}:

public foo() 
{
    $data = {{exported}};
}

would result in:

public foo() 
{
    $data = [
        'foo' => 'bar'
    ];
}

Note that the first line will never be indented, as we can see in the example above.

Error handling

Any error occurring on export() will throw an ExportException:

use Brick\VarExporter\VarExporter;
use Brick\VarExporter\ExportException;

try {
    VarExporter::export(fopen('php://memory', 'r'));
} catch (ExportException $e) {
    // Type "resource" is not supported.
}

Limitations

In pretty much every other case, it offers an elegant and very efficient way to cache data to PHP files, and a solid alternative to serialization.