Перейти к содержанию

Синтезаторы⚓︎

Поскольку компоненты Livewire дегидратируются (сериализуются) в JSON, а затем гидратируются (десериализуются) обратно в PHP-объекты между запросами, их свойства должны быть пригодны для сериализации в JSON.

По умолчанию PHP легко сериализует большинство примитивных значений в JSON. Однако, чтобы компоненты Livewire могли поддерживать более сложные типы свойств (такие как модели, коллекции, экземпляры Carbon и Stringable), требуется более мощная система.

Поэтому Livewire предоставляет точку расширения под названием «Синтезаторы», которая позволяет пользователям поддерживать любые собственные типы свойств по своему усмотрению.

Сначала разберитесь с гидратацией

Перед использованием синтезаторов полезно полностью понять систему гидратации Livewire. Вы можете узнать больше, прочитав документацию по гидратации.

Понимание синтезаторов⚓︎

Прежде чем изучать создание пользовательских синтезаторов, давайте сначала посмотрим на внутренний синтезатор, который Livewire использует для поддержки Laravel Stringables.

Предположим, ваше приложение содержит следующий компонент CreatePost:

<?php

class CreatePost extends Component
{
    public $title = '';
}

Между запросами Livewire может сериализовать состояние этого компонента в объект JSON, подобный следующему:

state: { title: '' },

Теперь рассмотрим более сложный пример, когда значение свойства $title является объектом stringable вместо обычной строки:

<?php

class CreatePost extends Component
{
    public $title = '';

    public function mount()
    {
        $this->title = str($this->title);
    }
}

Дегидратированный JSON, представляющий состояние этого компонента, теперь содержит кортеж метаданных вместо простой пустой строки:

state: { title: ['', { s: 'str' }] },

Livewire теперь может использовать этот кортеж для обратной гидратации свойства $title в stringable при следующем запросе.

Теперь, когда вы увидели внешние эффекты работы синтезаторов, вот реальный исходный код внутреннего синтезатора Livewire для stringable:

<?php

use Illuminate\Support\Stringable;

class StringableSynth extends Synth
{
    public static $key = 'str';

    public static function match($target)
    {
        return $target instanceof Stringable;
    }

    public function dehydrate($target)
    {
        return [$target->__toString(), []];
    }

    public function hydrate($value)
    {
        return str($value);
    }
}

Давайте разберем это по частям.

Первое — это свойство $key:

<?php

public static $key = 'str';

Каждый синтезатор должен содержать статическое свойство $key, которое Livewire использует для преобразования кортежа метаданных, такого как ['', { s: 'str' }], обратно в stringable. Как вы можете заметить, каждый кортеж метаданных имеет ключ s, ссылающийся на этот ключ.

И наоборот, когда Livewire выполняет дегидратацию свойства, он использует статическую функцию синтезатора match(), чтобы определить, подходит ли данный конкретный синтезатор для дегидратации текущего свойства ($target — это текущее значение свойства):

<?php

public static function match($target)
{
    return $target instanceof Stringable;
}

Если match() возвращает true, будет использован метод dehydrate(), который принимает PHP-значение свойства в качестве входных данных и возвращает JSON-совместимый кортеж метаданных:

<?php

public function dehydrate($target)
{
    return [$target->__toString(), []];
}

Затем, в начале следующего запроса, после того как этот синтезатор был найден по ключу { s: 'str' } в кортеже, будет вызван метод hydrate(), которому будет передано необработанное JSON-представление свойства с ожиданием, что он вернет полное PHP-совместимое значение для присвоения свойству.

<?php

public function hydrate($value)
{
    return str($value);
}

Регистрация пользовательского синтезатора⚓︎

Чтобы продемонстрировать, как вы можете написать собственный синтезатор для поддержки пользовательского свойства, мы будем использовать следующий компонент UpdateProperty в качестве примера:

<?php

class UpdateProperty extends Component
{
    public Address $address;

    public function mount()
    {
        $this->address = new Address();
    }
}

Вот исходный код класса Address:

<?php

namespace App\Dtos\Address;

class Address
{
    public $street = '';
    public $city = '';
    public $state = '';
    public $zip = '';
}

Для поддержки свойств типа Address мы можем использовать следующий синтезатор:

<?php

use App\Dtos\Address;

class AddressSynth extends Synth
{
    public static $key = 'address';

    public static function match($target)
    {
        return $target instanceof Address;
    }

    public function dehydrate($target)
    {
        return [[
            'street' => $target->street,
            'city' => $target->city,
            'state' => $target->state,
            'zip' => $target->zip,
        ], []];
    }

    public function hydrate($value)
    {
        $instance = new Address;

        $instance->street = $value['street'];
        $instance->city = $value['city'];
        $instance->state = $value['state'];
        $instance->zip = $value['zip'];

        return $instance;
    }
}

Чтобы сделать его доступным глобально в вашем приложении, вы можете использовать метод Livewire propertySynthesizer для регистрации синтезатора в методе boot вашего сервис-провайдера:

<?php

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Livewire::propertySynthesizer(AddressSynth::class);
    }
}

Поддержка привязки данных⚓︎

Используя пример UpdateProperty выше, вполне вероятно, что вы захотите поддерживать привязку wire:model напрямую к свойствам объекта Address. Синтезаторы позволяют поддерживать это с помощью методов get() и set():

<?php

use App\Dtos\Address;

class AddressSynth extends Synth
{
    public static $key = 'address';

    public static function match($target)
    {
        return $target instanceof Address;
    }

    public function dehydrate($target)
    {
        return [[
            'street' => $target->street,
            'city' => $target->city,
            'state' => $target->state,
            'zip' => $target->zip,
        ], []];
    }

    public function hydrate($value)
    {
        $instance = new Address;

        $instance->street = $value['street'];
        $instance->city = $value['city'];
        $instance->state = $value['state'];
        $instance->zip = $value['zip'];

        return $instance;
    }

    public function get(&$target, $key)
    {
        return $target->{$key};
    }

    public function set(&$target, $key, $value)
    {
        $target->{$key} = $value;
    }
}

JavaScript-синтезаторы⚓︎

Синтезаторы позволяют использовать на сервере сложные значения, такие как даты Carbon, однако по умолчанию JavaScript получает только необработанное дегидратированное значение. Например, рассмотрим следующий компонент:

<?php

use Carbon\Carbon;

class ShowPost extends Component
{
    public Carbon $publishedAt;
}

При обращении к $wire.publishedAt в JavaScript вы получите обычную строку даты в формате ISO, например "2021-01-01T00:00:00+00:00", а не объект, у которого можно вызывать методы для работы с датой.

JavaScript-синтезаторы — это клиентский аналог PHP-синтезаторов. Если зарегистрировать синтезатор с тем же ключом (synth key), необработанное значение автоматически преобразуется в полноценный JavaScript-объект при попадании состояния компонента на клиент, а при отправке обновлений на сервер — обратно в его исходное wire-представление.

Зарегистрируйте JavaScript-синтезатор с помощью Livewire.synth() внутри обработчика livewire:init, чтобы он был доступен до инициализации компонентов:

document.addEventListener('livewire:init', () => {
    Livewire.synth('cbn', {
        match: (value) => value instanceof Date,

        hydrate: (value, meta) => new Date(value),

        dehydrate: (value) => value.toISOString(),
    })
})

После регистрации этого синтезатора каждое свойство Carbon на клиенте становится полноценным объектом Date:

<span x-text="$wire.publishedAt.toLocaleDateString()"></span>

<button x-on:click="$wire.publishedAt = new Date()">Опубликовать</button>

Когда вы присваиваете новое значение типа Date и отправляется следующий запрос, Livewire вызывает dehydrate(), сервер получает строку в формате ISO, а PHP-синтезатор CarbonSynth гидратирует её обратно в экземпляр Carbon.

Разберём три обязательные функции.

Первый параметр Livewire.synth() — это ключ PHP-синтезатора, с которым вы связываете JavaScript-синтезатор. Это тот же ключ, который находится в поле s кортежа метаданных свойства (cbn — ключ встроенного синтезатора Carbon в Livewire).

Функция hydrate() получает необработанное wire-значение вместе с полным кортежем метаданных и возвращает полноценное JavaScript-значение. Она вызывается каждый раз, когда состояние компонента поступает с сервера: при первоначальной загрузке страницы и после каждого последующего запроса.

hydrate: (value, meta) => new Date(value),

dehydrate() выполняет обратное преобразование: получает полноценное значение и должна вернуть его в необработанном wire-формате. Она вызывается, когда Livewire сравнивает состояние компонента и формирует данные обновления для отправки на сервер. Возвращаемое значение должно быть в формате, который понимает метод hydrate() соответствующего PHP-синтезатора — как правило, это тот же формат, в котором сервер изначально отправил данные клиенту.

dehydrate: (value) => value.toISOString(),

match() определяет полноценные значения в состоянии компонента. Livewire использует её, чтобы понимать, какие значения следует рассматривать как неделимые единицы при вычислении различий, а также какой синтезатор должен выполнять дегидратацию нового экземпляра, который вы присвоили (например, new Date(), который никогда не поступал с сервера).

match: (value) => value instanceof Date,

Контекст гидратации⚓︎

hydrate() принимает третий аргумент — объект контекста, содержащий компонент-владелец (component) и путь (path) значения в состоянии компонента ('date', 'items.2' и т. д.). Полноценные значения могут сохранить этот объект, чтобы знать своё местоположение — например, для собственного обновления или удаления через component.$wire:

hydrate: (value, meta, context) => new Reminder(value, context),

// Внутри полноценного класса:
dismiss() {
    this.context.component.$wire.$set(this.context.path, null)
}

Значения без wire-представления⚓︎

dehydrate() может вернуть undefined, чтобы показать, что значение пока не имеет серверного представления. Livewire никогда не отправляет такие значения на сервер: они пропускаются при формировании данных обновления и полностью исключаются из массивов (вместо сериализации в null). Это позволяет использовать оптимистичные клиентские значения — например, полноценные объекты загрузки файлов в Livewire используют такой подход для представления файлов, которые ещё загружаются, предоставляя реактивное свойство progress, пока загрузка не завершится.

Использование вместе с пользовательским PHP-синтезатором⚓︎

JavaScript-синтезаторы раскрывают свой потенциал в полной мере при использовании вместе с пользовательским PHP-синтезатором. Продолжая пример с Address, показанный выше, можно предоставить клиентской части соответствующий полноценный объект:

class Address {
    constructor({ street, city, state, zip }) {
        this.street = street
        this.city = city
        this.state = state
        this.zip = zip
    }

    get full() {
        return `${this.street}, ${this.city}, ${this.state} ${this.zip}`
    }
}

document.addEventListener('livewire:init', () => {
    Livewire.synth('address', {
        match: (value) => value instanceof Address,

        hydrate: (value) => new Address(value),

        dehydrate: (value) => ({
            street: value.street,
            city: value.city,
            state: value.state,
            zip: value.zip,
        }),
    })
})

Теперь одна и та же концепция Address существует по обе стороны wire:

<span x-text="$wire.address.full"></span>

Вы по-прежнему можете изменять отдельные поля ($wire.address.street = '...' или wire:model="address.street"). Когда полноценное значение изменяется, Livewire отправляет на сервер всю его дегидратированную форму, где PHP-синтезатор гидратирует её обратно в объект Address.

Реактивность⚓︎

Полноценные значения участвуют в системе реактивности Alpine на уровне свойства. Любой эффект, использующий это свойство — x-text, $wire.$watch(), wire:dirty — будет выполняться повторно каждый раз, когда свойство заменяется:

<!-- Это будет повторно отрисовано, когда $wire.publishedAt получит новое значение... -->
<div x-text="$wire.publishedAt.toDateString()"></div>

<!-- ...независимо от того, произошло ли это на клиенте: -->
<button x-on:click="$wire.publishedAt = new Date()">Опубликовать</button>

То же самое происходит, когда свойство изменяется на сервере: поскольку полноценные значения являются неделимыми, Livewire заменяет всё значение целиком при объединении ответа сервера, что приводит к повторному выполнению всех эффектов, использующих это свойство. Если же полноценное значение не изменилось, Livewire сохраняет существующий объект (с той же идентичностью), поэтому эффекты не выполняются повторно без необходимости.

Полноценные значения, построенные на основе обычных классов, таких как Address из примера выше, также обладают глубокой реактивностью, как и обычные объекты: изменение $wire.address.street приведёт к повторному выполнению эффекта, использующего $wire.address.full.

Единственное исключение — изменение встроенных объектов, таких как Date, «на месте»:

<!-- Это НЕ приведёт к повторному выполнению x-text выше: -->
<button x-on:click="$wire.publishedAt.setFullYear(2030)">Реакции не будет</button>

JavaScript-прокси не могут перехватывать внутренние методы объекта Date, поэтому Alpine (как и Vue) не способен отследить такое изменение. Само изменение всё равно обнаруживается механизмом сравнения состояния Livewire и отправляется на сервер при следующем запросе, однако никакие эффекты на клиенте при этом не срабатывают. Рассматривайте такие значения, как даты, в качестве неизменяемых: вместо изменения существующего экземпляра создавайте и присваивайте новый.

Что важно знать⚓︎

  • Регистрация выполняется глобально для каждого ключа. Если зарегистрировать синтезатор для cbn, все свойства Carbon и нативные даты во всём приложении будут автоматически преобразовываться. Убедитесь, что ваш клиентский код готов к такому поведению.
  • Полноценные значения являются неделимыми. Livewire никогда не вычисляет различия внутри такого значения. При его изменении на сервер отправляется вся его дегидратированная форма, а обновление соответствующего свойства происходит по пути этого свойства.
  • Полноценные значения также работают как параметры действий. При вызове $wire.save(new Date()) параметр сначала дегидратируется, а затем отправляется на сервер.
  • Прямая привязка поля ввода к полноценному свойству (wire:model="publishedAt") записывает в свойство необработанное строковое значение из поля. Эта строка отправляется на сервер в неизменном виде и гидратируется там, однако само поле ввода будет отображать строковое представление полноценного объекта, сформированное браузером. Для элементов формы предпочтительнее привязываться к вложенным полям или самостоятельно выполнять преобразование.

Для справки, встроенные синтезаторы Livewire используют следующие ключи: arr (массивы), cbn (Carbon/DateTime), clctn (коллекции), str (Stringable-объекты), enm (перечисления), std (stdClass), mdl (модели Eloquent), elcln (коллекции Eloquent), wrbl (Wireable-объекты), form (объекты Form) и fil (загрузка файлов).