Компонент VarExporter

Дата оновлення перекладу 2024-05-07

Компонент 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-класів

Інстанціатор

Цей компонент надає інстанціатор, який може створювати обʼєкти та встановлювати їхні властивості без виклику їх конструкторів або будь-яких інших методів:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\VarExporter\Instantiator;

// створює порожній екземпляр 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 також дозволяє перетворювати неліниві класи на лінивіs:

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

Призначення віртуальних проксі в тому ж, що і в об'єктів-привидів , але їхня внутрішня поведінка зовсім інша. Там, де об'єкти-привиди вимагають розширення базового класу, віртуальні проксі використовують переваги принципу підстановки Ліскова. Цей принцип описує, що якщо два об'єкти реалізують один і той самий інтерфейс, ви можете поміняти місцями різні реалізації, не порушуючи роботу додатку. Це те, чим користуються віртуальні проксі. Для використання віртуальних проксі ви можете використовувати 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-класами.