componenta/var-export

Export PHP variables to their string representation with support for closures, arrays, and configurable formatting

Maintainers

πŸ‘ Shelamkoff

Package info

github.com/componenta/var-export

pkg:composer/componenta/var-export

Statistics

Installs: 9

Dependents: 6

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-16 21:50 UTC

Requires

Requires (Dev)

Suggests

None

Provides

None

Conflicts

None

Replaces

None

MIT 7d472fc6650d591450226a2d4e6aad666f0977bd

  • Componenta <dev.woop@Componenta.io>

serializationarrayclosureexportvar_exportcode-generation

This package is auto-updated.

Last update: 2026-06-17 15:44:07 UTC


README

πŸ‘ PHP Version
πŸ‘ License
πŸ‘ Tests

Round-trip PHP values to executable source code. Unlike var_export(), this library handles closures, readonly value objects and enums β€” and keeps namespace semantics intact so the exported code evaluates correctly anywhere.

Русская вСрсия

Features

  • Closures β€” full AST-based export with correct name resolution (class refs β†’ FQN, function/constant refs keep global fallback)
  • Readonly value objects β€” round-tripped via new ClassName(...) when every constructor parameter is a public property
  • Enums β€” both pure and backed cases
  • Arrays β€” sequential, associative, mixed, unlimited nesting (guarded by maxDepth)
  • Configurable output β€” pretty or compact layout, custom indent, sorted keys, trailing commas
  • Typed exceptions β€” precise error context without leaking the values that caused them

Requirements

  • PHP 8.4+
  • nikic/php-parser ^5.0

Related Packages

Package Why it matters here
componenta/config Uses export for executable PHP configuration cache files.
componenta/app Application cache can persist compiled arrays and descriptors as PHP files.
componenta/di Compiled DI plans and dependency cache should be executable PHP arrays.
nikic/php-parser Required for closure export and name-resolution semantics.

Installation

composer require componenta/var-export

Quick start

use Componenta\VarExport\Export;

// Any value β€” returns executable PHP code
Export::var(['host' => 'localhost', 'port' => 5432]);
// β†’ ['host' => 'localhost', 'port' => 5432]

// Pretty layout (multi-line + trailing comma)
Export::pretty([1, 2, 3]);
// β†’ [
// 1,
// 2,
// 3,
// ]

// Closures β€” namespace-aware
$handler = static fn(int $x): int => $x * 2;
Export::closure($handler);
// β†’ static fn(int $x): int => $x * 2

// For file output β€” appends the semicolon
Export::toFile(['env' => 'prod']);
// β†’ ['env' => 'prod'];

Round-trip works through eval() (and any PHP source file):

$code = Export::var($original);
$restored = eval("return {$code};");
// $restored === $original

Configuration

use Componenta\VarExport\Config\ExportConfig;
use Componenta\VarExport\Config\ClosureUseMode;
use Componenta\VarExport\Config\FormatterMode;

$config = new ExportConfig(
 mode: FormatterMode::Pretty,
 indent: '', // spaces or tabs
 maxDepth: 64, // guards runaway recursion
 sortKeys: false, // sort associative array keys
 trailingComma: true, // add trailing comma in pretty mode
 closureUseMode: ClosureUseMode::Preserve,
);

// Presets
ExportConfig::pretty(); // multi-line + trailing comma
ExportConfig::compact(); // single-line

// Fluent copies (every with* returns a new instance)
$config = ExportConfig::pretty()
 ->withIndent("\t")
 ->withSortKeys();

Reusing an exporter

One-off calls go through the static facade. For many exports with the same configuration, instantiate VarExporter directly β€” the parsed-AST cache is reused across calls:

use Componenta\VarExport\VarExporter;

$exporter = new VarExporter(ExportConfig::pretty());
$a = $exporter->export($closure1);
$b = $exporter->export($closure2); // closure1's file AST is cached

Closure capture modes

ClosureUseMode::Preserve (default)

Keeps the use(...) clause verbatim. The captured variables must exist in the scope where the exported code runs.

$multiplier = 2;
$fn = function (int $x) use ($multiplier): int {
 return $x * $multiplier;
};

Export::closure($fn);
// β†’ function (int $x) use ($multiplier): int { return $x * $multiplier; }

ClosureUseMode::Inline

Replaces each use(...) variable with its current value, yielding a self-contained closure:

$config = new ExportConfig(closureUseMode: ClosureUseMode::Inline);

Export::closure($fn, $config);
// β†’ function (int $x): int { return $x * 2; }

Inline mode accepts scalar captures and nested scalar arrays. Captures of objects, resources, nested closures or by-reference variables (use (&$x)) are rejected with ClosureExportException.

Exporting objects

enum Priority: string {
 case Low = 'low';
 case High = 'high';
}

final readonly class Task {
 public function __construct(
 public string $title,
 public Priority $priority,
 public array $tags,
 ) {}
}

Export::var(new Task('Ship', Priority::High, ['core']));
// β†’ new \App\Task('Ship', \App\Priority::High, ['core'])

Requirements for readonly-class export:

  • Class is marked readonly
  • Every constructor parameter has a matching public property

ObjectExporter::supports($object) reports up front whether a given object satisfies these rules β€” use it to pre-flight untrusted input.

Helper functions

For callers that prefer free functions over static methods:

use function Componenta\VarExport\var_export_string;
use function Componenta\VarExport\var_export_pretty;
use function Componenta\VarExport\array_export;
use function Componenta\VarExport\closure_export;

var_export_string($value);
var_export_string($value, pretty: true);
var_export_pretty($value);
array_export([1, 2, 3]);
closure_export(fn() => 42);

Error handling

use Componenta\VarExport\Exception\{
 ExportException,
 ArrayExportException,
 ClosureExportException,
 ConfigurationException,
};

try {
 $code = Export::var($data);
} catch (ArrayExportException $e) {
 // Max depth, unexportable element, key path context in $e->context
} catch (ClosureExportException $e) {
 // Bound $this, inline captures not supported, ambiguous location
} catch (ConfigurationException $e) {
 // Invalid indent / maxDepth in ExportConfig
} catch (ExportException $e) {
 // Unsupported top-level type
}

Every exception carries a $context array with metadata (class, key path, variable names, file/line) β€” values that caused the failure are not stored, so logs stay safe.

Not supported

  • Mutable objects (non-readonly, or with private state that cannot be reconstructed through the constructor)
  • Resources (including stream/curl handles)
  • Closures bound to $this β€” convert to static function() { ... } before export
  • Closures defined in eval()'d code (no source file to parse)
  • Two or more closures on the same line with identical signatures (ambiguous β€” keep them on separate lines)

License

MIT