Turbo и живые компоненты
Живой поиск, форма без перезагрузки, обновление части страницы обычно означают много JavaScript. Набор пакетов Symfony UX даёт то же самое средствами Twig и PHP: страница остаётся серверной, но ведёт себя как приложение.
Turbo
composer require symfony/ux-turboПосле установки переходы по ссылкам и отправка форм перестают перезагружать страницу целиком: Turbo запрашивает новую страницу в фоне и заменяет содержимое body. Код менять не нужно.
Одно условие: после успешной отправки формы контроллер должен перенаправлять, а при ошибках проверки — отвечать кодом 422. Форма Symfony делает это сама, если передать её в render().
Turbo Frames
Рамка обновляет только свою часть страницы:
<turbo-frame id="comments">
{% for comment in comments %}
<p>{{ comment.text }}</p>
{% endfor %}
<a href="{{ path('post_comments', {id: post.id, page: page + 1}) }}">Ещё</a>
</turbo-frame>Щелчок по ссылке внутри рамки загрузит новую страницу, но заменит только элемент с тем же id. Остальная страница не тронется.
Компоненты Twig
composer require symfony/ux-twig-componentКомпонент — класс и шаблон, которые вместе описывают кусок интерфейса:
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Alert
{
public string $type = 'info';
public string $message;
}{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}">{{ message }}</div><twig:Alert type="success" message="Статья сохранена" />Живой компонент
composer require symfony/ux-live-componentЖивой компонент перерисовывается на сервере, когда меняются его данные:
// src/Twig/Components/ProductSearch.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent]
class ProductSearch
{
use DefaultActionTrait;
#[LiveProp(writable: true)]
public string $query = '';
public function __construct(private ProductRepository $products) {}
public function getProducts(): array
{
return $this->products->search($this->query);
}
}{# templates/components/ProductSearch.html.twig #}
<div {{ attributes }}>
<input type="search" data-model="query" placeholder="Поиск">
<ul>
{% for product in this.products %}
<li>{{ product.title }}</li>
{% endfor %}
</ul>
</div><twig:ProductSearch />Посетитель печатает — компонент отправляет запрос, сервер выполняет поиск и возвращает новую разметку. Ни строки своего JavaScript.
Действия
Метод с атрибутом #[LiveAction] можно вызвать из шаблона:
use Symfony\UX\LiveComponent\Attribute\LiveAction;
#[LiveProp]
public int $count = 0;
#[LiveAction]
public function increment(): void
{
$this->count++;
}<button data-action="live#action" data-live-action-param="increment">+1</button>
<span>{{ count }}</span>Частая ошибка
Свойство с writable: true может изменить посетитель — в том числе подставить значение, которого нет в интерфейсе. Не делайте изменяемыми идентификаторы, цены и роли, а в действиях проверяйте права так же, как в контроллере. Свойства без writable защищены подписью, и подделать их нельзя.