Свойства⚓︎
Свойства хранят и управляют состоянием внутри ваших компонентов Livewire. Они определяются как публичные свойства в классах компонентов и могут быть доступны и изменены как на стороне сервера, так и на стороне клиента.
Инициализация свойств⚓︎
Вы можете задавать начальные значения свойств внутри метода mount() вашего компонента.
Рассмотрим следующий пример:
<?php
use Livewire\Component;
new class extends Component {
public $todos = [];
public $todo = '';
public function mount()
{
$this->todos = ['Купить продукты', 'Выгулять собаку', 'Написать код'];
}
// ...
};
В этом примере мы определили пустой массив todos и инициализировали его списком задач по умолчанию в методе mount(). Теперь при первом рендере компонента пользователю сразу отображаются эти начальные задачи.
Массовое присваивание⚓︎
Иногда инициализация множества свойств в методе mount() может казаться слишком многословной. Чтобы упростить это, Livewire предоставляет удобный способ присвоить сразу несколько свойств с помощью метода fill(). Передавая ассоциативный массив, где ключами являются имена свойств, а значениями — их начальные значения, вы можете установить сразу несколько свойств и сократить количество повторяющегося кода в mount().
Например:
<?php
use Livewire\Component;
use App\Models\Post;
new class extends Component {
public $post;
public $title;
public $description;
public function mount(Post $post)
{
$this->post = $post;
$this->fill(
$post->only('title', 'description'),
);
}
// ...
};
Поскольку метод $post->only(...) возвращает ассоциативный массив атрибутов модели и их значений на основе переданных вами имён, свойства $title и $description будут изначально установлены в значения title и description модели $post из базы данных — без необходимости задавать каждое из них по отдельности.
Привязка данных⚓︎
Livewire поддерживает двустороннюю привязку данных через HTML-атрибут wire:model. Это позволяет легко синхронизировать данные между свойствами компонента и HTML-полями ввода, поддерживая актуальное состояние пользовательского интерфейса и компонента.
Давайте используем директиву wire:model, чтобы привязать свойство $todo в компоненте todos к простому элементу ввода:
<?php
use Livewire\Component;
new class extends Component {
public $todos = [];
public $todo = '';
public function add()
{
$this->todos[] = $this->todo;
$this->todo = '';
}
// ...
};
<div>
<input type="text" wire:model="todo" placeholder="Todo...">
<button wire:click="add">Добавить пункт</button>
<ul>
@foreach ($todos as $todo)
<li wire:key="{{ $loop->index }}">{{ $todo }}</li>
@endforeach
</ul>
</div>
В приведённом выше примере значение текстового поля будет синхронизироваться со свойством $todo на сервере при нажатии кнопки «Добавить пункт».
Это лишь поверхностное знакомство с возможностями wire:model. Более подробную информацию о привязке данных вы найдёте в нашей документации по формам.
Сброс свойств⚓︎
Иногда после выполнения действия пользователем вам может потребоваться вернуть свойства в их исходное состояние. В таких случаях Livewire предоставляет метод reset(), который принимает одно или несколько имён свойств и сбрасывает их значения до начального состояния.
В примере ниже мы можем избежать дублирования кода, используя $this->reset(), чтобы сбросить поле todo после нажатия кнопки «Добавить пункт»:
<?php
use Livewire\Component;
new class extends Component {
public $todos = [];
public $todo = '';
public function addTodo()
{
$this->todos[] = $this->todo;
$this->reset('todo');
}
// ...
};
В приведённом выше примере после того, как пользователь нажмёт «Добавить пункт», поле ввода, содержащее только что добавленную задачу, очистится, что позволит пользователю сразу ввести новую задачу.
reset() не работает со значениями, установленными в mount()
Метод reset() возвращает свойство в то состояние, которое было до вызова метода mount(). Если вы инициализировали свойство в mount() другим значением, вам придётся сбрасывать его вручную.
Получение и сброс свойств⚓︎
В качестве альтернативы вы можете использовать метод pull(), который одновременно сбрасывает свойство и возвращает его текущее значение за одну операцию.
Вот тот же пример из предыдущего раздела, но упрощённый с помощью pull():
<?php
use Livewire\Component;
new class extends Component {
public $todos = [];
public $todo = '';
public function addTodo()
{
$this->todos[] = $this->pull('todo');
}
// ...
};
Приведённый выше пример извлекает одно значение, но метод pull() также можно использовать для сброса и получения (в виде пар ключ-значение) всех или некоторых свойств сразу:
<?php
// Эквивалентно $this->all() и $this->reset();
$this->pull();
// Эквивалентно $this->only(...) и $this->reset(...);
$this->pull(['title', 'content']);
Поддерживаемые типы свойств⚓︎
Livewire поддерживает ограниченный набор типов свойств из-за своего уникального подхода к управлению данными компонента между серверными запросами.
Каждое свойство в компоненте Livewire сериализуется или «дегидратируется» в JSON между запросами, а затем «гидратируется» из JSON обратно в PHP для следующего запроса.
Этот процесс двустороннего преобразования имеет определённые ограничения, которые ограничивают типы свойств, с которыми может работать Livewire.
Примитивные типы⚓︎
Livewire поддерживает примитивные типы, такие как строки, целые числа и т. д. Эти типы легко преобразуются в JSON и обратно, что делает их идеальными для использования в качестве свойств в компонентах Livewire.
Livewire поддерживает следующие примитивные типы свойств: Array, String, Integer, Float, Boolean и Null.
<?php
new class extends Component {
public array $todos = [];
public string $todo = '';
public int $maxTodos = 10;
public bool $showTodos = false;
public ?string $todoFilter = null;
};
Распространённые типы PHP⚓︎
Помимо примитивных типов, Livewire поддерживает распространённые типы объектов PHP, используемые в приложениях Laravel. Однако важно понимать, что эти типы будут дегидратированы в JSON и гидратированы обратно в PHP при каждом запросе. Это означает, что свойство может не сохранять значения времени выполнения, такие как замыкания. Кроме того, информация об объекте, например имена классов, может быть раскрыта в JavaScript.
Поддерживаемые типы PHP:
| Тип | Полное имя класса |
|---|---|
| BackedEnum | BackedEnum |
| Collection | Illuminate\Support\Collection |
| Eloquent Collection | Illuminate\Database\Eloquent\Collection |
| Model | Illuminate\Database\Eloquent\Model |
| DateTime | DateTime |
| Carbon | Carbon\Carbon |
| Stringable | Illuminate\Support\Stringable |
Eloquent-коллекции и модели
При хранении Eloquent-коллекций и моделей в свойствах Livewire учитывайте следующие ограничения:
- Ограничения запроса не сохраняются: Дополнительные ограничения запроса, такие как
select(...), не будут повторно применены при последующих запросах. Подробности см. в разделе Ограничения Eloquent не сохраняются между запросами. - Влияние на производительность: Хранение больших Eloquent-коллекций в свойствах может привести к проблемам с производительностью, поскольку Livewire будет повторно выполнять запрос к базе данных при каждой гидратации компонента. Для дорогостоящих запросов рекомендуется использовать вместо этого вычисляемые свойства, которые выполняются только тогда, когда данные действительно запрашиваются в шаблоне.
Вот быстрый пример установки свойств различных поддерживаемых типов:
<?php
public function mount()
{
$this->todos = collect([]); // Collection
$this->todos = Todos::all(); // Eloquent Collection
$this->todo = Todos::first(); // Model
$this->date = new DateTime('now'); // DateTime
$this->date = new Carbon('now'); // Carbon
$this->todo = str(''); // Stringable
}
Поддержка пользовательских типов⚓︎
Livewire позволяет вашему приложению поддерживать пользовательские типы с помощью двух мощных механизмов:
- Wireables
- Синтезаторы
Wireables просты и удобны в использовании для большинства приложений, поэтому мы рассмотрим их ниже. Если вы продвинутый пользователь или автор пакета и вам нужна большая гибкость, то Синтезаторы — это ваш выбор.
Wireables⚓︎
Wireables — это любой класс в вашем приложении, который реализует интерфейс Wireable.
Например, представим, что в вашем приложении есть объект Customer, содержащий основные данные о клиенте:
<?php
class Customer
{
protected $name;
protected $age;
public function __construct($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
Попытка присвоить экземпляр этого класса свойству компонента Livewire приведёт к ошибке с сообщением о том, что тип свойства Customer не поддерживается:
<?php
new class extends Component {
public Customer $customer;
public function mount()
{
$this->customer = new Customer('Caleb', 29);
}
};
Однако вы можете решить эту проблему, реализовав интерфейс Wireable и добавив в свой класс методы toLivewire() и fromLivewire(). Эти методы указывают Livewire, как преобразовывать свойства этого типа в JSON и обратно:
<?php
use Livewire\Wireable;
class Customer implements Wireable
{
protected $name;
protected $age;
public function __construct($name, $age)
{
$this->name = $name;
$this->age = $age;
}
public function toLivewire()
{
return [
'name' => $this->name,
'age' => $this->age,
];
}
public static function fromLivewire($value)
{
$name = $value['name'];
$age = $value['age'];
return new static($name, $age);
}
}
Теперь вы можете свободно присваивать объекты Customer свойствам ваших компонентов Livewire, и Livewire будет знать, как преобразовывать эти объекты в JSON и обратно в PHP.
Как упоминалось ранее, если вы хотите поддерживать типы более глобально и мощно, Livewire предлагает Синтезаторы — свой продвинутый внутренний механизм для работы с различными типами свойств. Подробнее о Синтезаторах.
Доступ к свойствам из JavaScript⚓︎
Поскольку свойства Livewire также доступны в браузере через JavaScript, вы можете обращаться к ним и манипулировать их JavaScript-представлениями с помощью Alpine.js.
Alpine — это лёгкая JavaScript-библиотека, которая поставляется вместе с Livewire. Alpine позволяет добавлять лёгкие интерактивные элементы в ваши компоненты Livewire без полноценных обращений к серверу.
На внутреннем уровне фронтенд Livewire построен поверх Alpine. Фактически каждый компонент Livewire под капотом является компонентом Alpine. Это означает, что вы можете свободно использовать Alpine внутри ваших компонентов Livewire.
Остальная часть этой страницы предполагает базовое знакомство с Alpine. Если вы с ним не знакомы, посмотрите документацию Alpine.
Доступ к свойствам⚓︎
Livewire предоставляет магический объект $wire для Alpine. Вы можете обращаться к объекту $wire из любого выражения Alpine внутри вашего компонента Livewire.
Объект $wire можно рассматривать как JavaScript-версию вашего компонента Livewire. У него есть все те же свойства и методы, что и у PHP-версии компонента, плюс несколько специальных методов для выполнения определённых функций прямо в шаблоне.
Например, мы можем использовать $wire, чтобы показывать в реальном времени количество символов в поле ввода todo:
<div>
<input type="text" wire:model="todo">
Длина символов в задаче: <h2 x-text="$wire.todo.length"></h2>
</div>
По мере того как пользователь вводит текст в поле, длина текущей задачи, которую он пишет, будет отображаться и обновляться в реальном времени на странице — всё это без отправки сетевых запросов на сервер.
Изменение свойств⚓︎
Аналогичным образом вы можете изменять свойства вашего компонента Livewire из JavaScript с помощью $wire.
Например, давайте добавим кнопку «Очистить» в компонент todos, чтобы пользователь мог очистить поле ввода исключительно с помощью JavaScript:
<div>
<input type="text" wire:model="todo">
<button x-on:click="$wire.todo = ''">Очистить</button>
</div>
После того как пользователь нажмёт «Очистить», поле ввода будет сброшено до пустой строки — без отправки сетевого запроса на сервер.
При следующем запросе серверное значение свойства $todo обновится и синхронизируется.
При желании вы также можете использовать более явный метод .set() для установки свойств на стороне клиента. Однако стоит отметить, что по умолчанию .set() немедленно отправляет сетевой запрос и синхронизирует состояние с сервером. Если такое поведение вам нужно, то это отличный API:
Чтобы обновить свойство без отправки сетевого запроса на сервер, вы можете передать третий параметр типа bool. Это отложит сетевой запрос, и при последующем запросе состояние будет синхронизировано на стороне сервера:
Вопросы безопасности⚓︎
Хотя свойства Livewire — это мощная возможность, существует несколько аспектов безопасности, о которых стоит знать перед их использованием.
Коротко: всегда рассматривайте публичные свойства как пользовательский ввод — точно так же, как если бы это были данные из запроса обычного эндпоинта. В связи с этим крайне важно валидировать и авторизовывать значения свойств перед сохранением их в базу данных — ровно так же, как вы делаете это с данными запроса в контроллере.
Никогда не доверяйте значениям свойств⚓︎
Чтобы показать, как отсутствие авторизации и валидации свойств может создать уязвимости в приложении, рассмотрим следующий компонент post.edit, который уязвим для атаки:
<?php
use Livewire\Component;
use App\Models\Post;
new class extends Component {
public $id;
public $title;
public $content;
public function mount(Post $post)
{
$this->id = $post->id;
$this->title = $post->title;
$this->content = $post->content;
}
public function update()
{
$post = Post::findOrFail($this->id);
$post->update([
'title' => $this->title,
'content' => $this->content,
]);
session()->flash('message', 'Пост успешно обновлён!');
}
};
<form wire:submit="update">
<input type="text" wire:model="title">
<input type="text" wire:model="content">
<button type="submit">Обновить</button>
</form>
На первый взгляд этот компонент может выглядеть совершенно нормальным. Но давайте разберём, как злоумышленник может использовать его для выполнения несанкционированных действий в вашем приложении.
Поскольку мы храним id поста как публичное свойство компонента, его можно так же легко изменить на стороне клиента, как и свойства title и content.
Не имеет значения, что мы не написали поле ввода с wire:model="id". Злоумышленник может без проблем изменить представление в браузере с помощью DevTools на следующее:
<form wire:submit="update">
<input type="text" wire:model="id">
<input type="text" wire:model="title">
<input type="text" wire:model="content">
<button type="submit">Update</button>
</form>
Теперь злоумышленник может изменить значение поля id на ID другого поста. Когда форма будет отправлена и вызван метод update(), Post::findOrFail() вернёт и обновит пост, владельцем которого пользователь не является.
Чтобы предотвратить подобную атаку, можно использовать одну или обе из следующих стратегий:
- Авторизовать ввод
- Заблокировать свойство от обновлений
Авторизация ввода⚓︎
Поскольку $id можно изменить на стороне клиента с помощью wire:model — точно так же, как и в обычном контроллере, — мы можем использовать систему авторизации Laravel, чтобы убедиться, что текущий пользователь имеет право обновлять этот пост:
<?php
public function update()
{
$post = Post::findOrFail($this->id);
$this->authorize('update', $post);
$post->update(...);
}
Если злоумышленник изменит свойство $id, добавленная авторизация перехватит это и выбросит ошибку.
Блокировка свойства⚓︎
Livewire также позволяет «заблокировать» свойства, чтобы предотвратить их изменение на стороне клиента. Вы можете заблокировать свойство от манипуляций на стороне клиента с помощью атрибута #[Locked]:
<?php
use Livewire\Attributes\Locked;
use Livewire\Component;
new class extends Component {
#[Locked]
public $id;
// ...
};
Теперь, если пользователь попытается изменить $id на фронтенде, будет выброшена ошибка.
Используя #[Locked], вы можете быть уверены, что это свойство не было изменено нигде за пределами класса вашего компонента.
Подробнее о блокировке свойств читайте в документации атрибута Locked.
Eloquent-модели и блокировка⚓︎
Когда Eloquent-модель присваивается свойству компонента Livewire, Livewire автоматически блокирует это свойство и гарантирует, что ID не будет изменён, — таким образом вы защищены от подобных атак:
<?php
use Livewire\Component;
use App\Models\Post;
new class extends Component {
public Post $post;
public $title;
public $content;
public function mount(Post $post)
{
$this->post = $post;
$this->title = $post->title;
$this->content = $post->content;
}
public function update()
{
$this->post->update([
'title' => $this->title,
'content' => $this->content,
]);
session()->flash('message', 'Пост успешно обновлён!');
}
};
Свойства раскрывают системную информацию в браузере⚓︎
Ещё одно важное обстоятельство, о котором стоит помнить: свойства Livewire сериализуются или «дегидратируются» перед отправкой в браузер. Это означает, что их значения преобразуются в формат, который можно передать по сети и который понимает JavaScript. Такой формат может раскрывать информацию о вашем приложении в браузере, включая имена свойств и имена классов.
Например, предположим, что у вас есть компонент Livewire с публичным свойством $post. Это свойство содержит экземпляр модели Post из вашей базы данных. В этом случае дегидратированное значение свойства, отправляемое по сети, может выглядеть примерно так:
Как видно, дегидратированное значение свойства $post включает имя класса модели (App\Models\Post), а также ID и любые связи, которые были предварительно загружены (eager-loaded).
Если вы не хотите раскрывать имя класса модели, вы можете использовать функциональность Laravel «morphMap» из сервис-провайдера, чтобы назначить псевдоним имени класса модели:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Relations\Relation;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Relation::morphMap([
'post' => 'App\Models\Post',
]);
}
}
Теперь, когда Eloquent-модель «дегидратируется» (сериализуется), оригинальное имя класса не будет раскрыто — в браузере будет виден только псевдоним «post»:
Ограничения Eloquent не сохраняются между запросами⚓︎
Обычно Livewire способен сохранять и воссоздавать серверные свойства между запросами; однако существуют определённые сценарии, в которых сохранение значений между запросами невозможно.
Например, при хранении Eloquent-коллекций в свойствах Livewire дополнительные ограничения запроса, такие как select(...), не будут повторно применены при последующих запросах.
Для примера рассмотрим компонент show-todos, в котором к Eloquent-коллекции Todos применено ограничение select():
<?php
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
new class extends Component {
public $todos;
public function mount()
{
$this->todos = Auth::user()
->todos()
->select(['title', 'content'])
->get();
}
};
При первоначальной загрузке этого компонента свойство $todos будет установлено в Eloquent-коллекцию задач пользователя; однако в каждой модели будут запрошены и загружены только поля title и content из каждой строки базы данных.
Когда Livewire в последующих запросах гидратирует JSON этого свойства обратно в PHP, ограничение select будет потеряно.
Чтобы гарантировать целостность Eloquent-запросов, мы рекомендуем использовать вместо обычных свойств вычисляемые свойства.
Вычисляемые свойства — это методы в вашем компоненте, помеченные атрибутом #[Computed]. К ним можно обращаться как к динамическим свойствам, которые не хранятся в состоянии компонента, а вычисляются «на лету».
Вот пример выше, переписанный с использованием вычисляемого свойства:
<?php
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
new class extends Component {
#[Computed]
public function todos()
{
return Auth::user()
->todos()
->select(['title', 'content'])
->get();
}
};
Вот как вы можете получить доступ к этим todos из Blade-шаблона:
<ul>
@foreach ($this->todos as $todo)
<li wire:key="{{ $loop->index }}">{{ $todo }}</li>
@endforeach
</ul>
Обратите внимание: внутри ваших представлений (views) вы можете обращаться к вычисляемым свойствам только через объект $this, вот так: $this->todos.
Вы также можете обращаться к $todos изнутри класса. Например, если у вас есть действие markAllAsComplete():
<?php
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
new class extends Component {
#[Computed]
public function todos()
{
return Auth::user()
->todos()
->select(['title', 'content'])
->get();
}
public function markAllComplete()
{
$this->todos->each->complete();
}
};
Вы можете задаться вопросом: почему бы просто не вызывать $this->todos() как обычный метод там, где это нужно? Зачем вообще использовать #[Computed]?
Дело в том, что вычисляемые свойства дают преимущество в производительности, поскольку после первого обращения в рамках одного запроса они автоматически кэшируются. Это означает, что вы можете свободно обращаться к $this->todos внутри компонента сколько угодно раз, и при этом сам метод будет выполнен только один раз — так дорогой запрос к базе данных не будет запускаться многократно в пределах одного запроса.
Подробнее — в документации по вычисляемым свойствам.
Смотрите также⚓︎
- Формы — Привязывайте свойства к полям формы с помощью
wire:model - Вычисляемые свойства — Создавайте производные значения с автоматическим кэшированием
- Валидация — Проверяйте значения свойств перед сохранением
- Атрибут Locked — Запрещайте изменение свойств на стороне клиента
- Alpine — Обращайтесь и управляйте свойствами из JavaScript