Компонент VarExporter
Дата обновления перевода 2024-10-12
Компонент VarExporter
Компонент VarExporter экспортирует всю структуру PHP-данных, поддающуюся сериализации, в чистый PHP-код и позволяет инстанциировать и населять объекты без вызова к их конструкторам.
Установка
1
$ composer require --dev symfony/var-exporter
Note
Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно
подключить файл vendor/autoload.php
в вашем коде для включения механизма
автозагрузки классов, предоставляемых Composer. Детальнее читайте в
этой статье.
Экспорт/сериализация переменных
Главная функция этого компонента - сериализовать структуры PHP-данных в чистый PHP-код, схоже в функцией PHP var_export:
1 2 3 4 5 6 7 8
use Symfony\Component\VarExporter\VarExporter;
$exported = VarExporter::export($someVariable);
// сохраните $exported данные в каком-то файле или системе кеширования для дальнейшего повторного использования
$data = file_put_contents('exported.php', $exported);
// позже, регенерируйте изначальную переменную, когда она вам понадобится
$regeneratedVariable = require 'exported.php';
Причина использования этого компонента вместо serialize()
или igbinary
кроется
в производительности: благодаря OPcache, итоговый код значительно быстрее и более
эффективный с точки зрения памяти, чем при использовании unserialize()
или igbinary_unserialize()
.
Кроме этого, есть некоторые небольшие различия:
- Если изначальная переменная это определяет, вся семантика, ассоциированная с
serialize()
(такая как__wakeup()
,__sleep()
, иSerializable
) сохраняется (var_export()
игнорирует это); - Ссылки, задействующие экземпляры
SplObjectStorage
,ArrayObject
илиArrayIterator
сохраняются; - Отсутствующие классы вызывают
ClassNotFoundException
вместо десериализации в объектыPHP_Incomplete_Class
; - Классы
Reflection*
,IteratorIterator
иRecursiveIteratorIterator
вызывают исключение при попытке сериализации.
Экспортированный данные - это PSR-2, совместимый с PHP-файлом. Рассмотрите, к примеру, следующую иерархию классов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
abstract class AbstractClass
{
protected int $foo;
private int $bar;
protected function setBar($bar): void
{
$this->bar = $bar;
}
}
class ConcreteClass extends AbstractClass
{
public function __construct()
{
$this->foo = 123;
$this->setBar(234);
}
}
При экспорте данных ConcreteClass
с помощью VarExporter, сгенерированный PHP-файл
выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
$o = [
clone (\Symfony\Component\VarExporter\Internal\Registry::$prototypes['Symfony\\Component\\VarExporter\\Tests\\ConcreteClass'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('Symfony\\Component\\VarExporter\\Tests\\ConcreteClass')),
],
null,
[
'Symfony\\Component\\VarExporter\\Tests\\AbstractClass' => [
'foo' => [
123,
],
'bar' => [
234,
],
],
],
$o[0],
[]
);
Инстанцирование и гидратация PHP-классов
Инстанциатор
Этот компонент предоставляет инстанциатор, который может создавать объекты и устанавливать
их свойства без вызова их конструкторов или каких-либо других методов;:
use SymfonyComponentVarExporterInstantiator;
// создает пустой экземпляр Foo $fooObject = Instantiator::instantiate(Foo::class);
// создает экземпляр Foo и устанавливает одно из его свойств $fooObject = Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]);
// создает экземпляр Foo и устанавливает частное свойство, определенное в его родительском классе Bar $fooObject = Instantiator::instantiate(Foo::class, [], [ Bar::class => ['privateBarProperty' => $propertyValue], ]);
Инстанциатор также может заполнить свойство родительского класса. Предположим, что
Bar
- это родительский класс Foo
, который определяет атрибут privateBarProperty
:
1 2 3 4 5 6
use Symfony\Component\VarExporter\Instantiator;
// создаёт экземпляр Foo и устанавливает приватное свойство, определённое в его родительском классе Bar
$fooObject = Instantiator::instantiate(Foo::class, [], [
Bar::class => ['privateBarProperty' => $propertyValue],
]);
Экземпляры ArrayObject
, ArrayIterator
и SplObjectHash
могут быть созданы
используя специальное имя свойства "\0"
для определения их внутреннего значения:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Component\VarExporter\Instantiator;
// Создаёт SplObjectHash, где $info1 ассоциируется с $object1, и т.д.
$theObject = Instantiator::instantiate(SplObjectStorage::class, [
"\0" => [$object1, $info1, $object2, $info2...],
]);
// создаёт ArrayObject, заполненный $inputArray
$theObject = Instantiator::instantiate(ArrayObject::class, [
"\0" => [$inputArray],
]);
Гидратор
Вместо того чтобы заполнять ещё несуществующие объекты (с помощью инстанциатора), иногда требуется заполнить свойства уже существующего объекта. Это цель Hydrator. Вот базовое использование гидратора для заполнения свойства объекта:
1 2 3 4
use Symfony\Component\VarExporter\Hydrator;
$object = new Foo();
Hydrator::hydrate($object, ['propertyName' => $propertyValue]);
Гидратор также может заполнить свойство родительского класса. Предположим, что
Bar
является родительским классом Foo
и определяет атрибут privateBarProperty
:
1 2 3 4 5 6 7 8 9
use Symfony\Component\VarExporter\Hydrator;
$object = new Foo();
Hydrator::hydrate($object, [], [
Bar::class => ['privateBarProperty' => $propertyValue],
]);
// как вариант, вы можете использовать специальный синтаксис "\0"
Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]);
Экземпляры ArrayObject
, ArrayIterator
и SplObjectHash
могут быть заполнены
используя специальное имя свойства "\0"
, чтобы определить их внутренние значения:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Symfony\Component\VarExporter\Hydrator;
// создаёт SplObjectHash, где $info1 ассоциируется с $object1 и т.д.
$storage = new SplObjectStorage();
Hydrator::hydrate($storage, [
"\0" => [$object1, $info1, $object2, $info2...],
]);
// созадёт ArrayObject, заполненный $inputArray
$arrayObject = new ArrayObject();
Hydrator::hydrate($arrayObject, [
"\0" => [$inputArray],
]);
Создание ленивых объектов
Ленивые объекты - это объекты, которые инстанцируются пустыми и заполняются по требованию. Это особенно полезно, когда в классах имеются, например, свойства, для определения значения которых требуются тяжёлые вычисления. В этом случае вам может потребоваться запускать обработку значения свойства только тогда, когда оно действительно необходимо. Благодаря этому, тяжёлые вычисления не будут производиться, если вы никогда не будете использовать данное свойство. Компонент VarExporter поставляется в комплекте с двумя чертами, помогающими легко реализовать подобный механизм в ваших классах.
LazyGhostTrait
Объекты-призраки - это пустые объекты, свойства которых заполняются при первом вызове
любого метода. Благодаря LazyGhostTrait,
облегчается реализация ленивого механизма. В приведенном ниже примере свойство
$hash
определено как ленивое. Кроме того, метод MyLazyObject::computeHash()
должен вызываться только тогда, когда значение $hash
должно быть известно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
namespace App\Hash;
use Symfony\Component\VarExporter\LazyGhostTrait;
class HashProcessor
{
use LazyGhostTrait;
// Из-за того, как черта LazyGhostTrait работает внутренне, вы должны
// добавить это приватное свойство в ваш класс
private int $lazyObjectId;
// Это свойство может требовать тяжёлых вычислений для получения его значения
public readonly string $hash;
public function __construct()
{
self::createLazyGhost(initializer: [
'hash' => $this->computeHash(...),
], instance: $this);
}
private function computeHash(array $data): string
{
// Вычислить значение $this->hash с переданными данными
}
}
LazyGhostTrait также позволяет преобразовать неленивые классы в ленивые:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
namespace App\Hash;
use Symfony\Component\VarExporter\LazyGhostTrait;
class HashProcessor
{
public readonly string $hash;
public function __construct(array $data)
{
$this->hash = $this->computeHash($data);
}
private function computeHash(array $data): string
{
// ...
}
public function validateHash(): bool
{
// ...
}
}
class LazyHashProcessor extends HashProcessor
{
use LazyGhostTrait;
}
$processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance): void {
// Сделать любую необходимую вам операцию здесь: вызвать геттеры, сеттеры, методы для валидации хеша и т.д.
$data = /** Извлечь необходимые для вычисления хеша данные */;
$instance->__construct(...$data);
$instance->validateHash();
});
Если вы никогда не будете запрашивать значение $processor->hash
, то тяжелые методы
никогда не будут вызваны. Но, тем не менее, объект $processor
существует и может
быть использован в вашем коде, передан методам, функциям и т.д.
Кроме того, добавив два аргумента в функцию инициализатора, можно инициализировать свойства по одному:
1 2 3 4 5 6 7
$processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance, string $propertyName, ?string $propertyScope): mixed {
if (HashProcessor::class === $propertyScope && 'hash' === $propertyName) {
// Вернуть значение $hash
}
// Затем вы можете добавить больше логики для других свойств
});
Объекты-призраки, к сожалению, не могут работать с абстрактными классами или внутренними классами PHP. Тем не менее, компонент VarExporter покрывает эту потребность с помощью Виртуальных прокси .
LazyProxyTrait
Назначение виртуальных прокси то же, что и у объектов-призраковs , но их внутреннее поведение совершенно иное. Там, где объекты-призраки требуют расширения базового класса, виртуальные прокси используют принцип замещения Лискова. Этот принцип описывает, что если два объекта реализуют один и тот же интерфейс, то можно поменять различные реализации местами без нарушения работы приложения. Именно это и используют виртуальные прокси. Для использования виртуальных прокси можно использовать ProxyHelper для генерации кода класса прокси:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
namespace App\Hash;
use Symfony\Component\VarExporter\ProxyHelper;
interface ProcessorInterface
{
public function getHash(): bool;
}
abstract class AbstractProcessor implements ProcessorInterface
{
protected string $hash;
public function getHash(): bool
{
return $this->hash;
}
}
class HashProcessor extends AbstractProcessor
{
public function __construct(array $data)
{
$this->hash = $this->computeHash($data);
}
private function computeHash(array $data): string
{
// ...
}
}
$proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(AbstractProcessor::class));
// $proxyCode содержит реальный прокси и ссылку на LazyProxyTrait.
// В окружении производства это должно быть сброшено в файл, чтобы избежать вызова eval().
eval('class HashProcessorProxy'.$proxyCode);
$processor = HashProcessorProxy::createLazyProxy(initializer: function (): ProcessorInterface {
$data = /** Извлечь необходиме для вычисления хеша данные */;
$instance = new HashProcessor(...$data);
// Сделать любую необходимую вам операцию здесь: вызвать геттеры, сеттеры, методы для валидации хеша и т.д.
return $instance;
});
Подобно объектам-призракам, если вы никогда не запрашиваете $processor->hash
, то
его значение не будет вычислено. Основное отличие от объектов-призраков заключается в
том, что на этот раз создаётся прокси абстрактного класса. Это работает и с внутренними
классами PHP.