Nowoczesny UX bez drugiego frontendu
Jedna funkcja, która wymusza:
HTML z serwera, interaktywność z gotowych klocków.
Twig Components
Live Components
Turbo
Toolkit
Autocomplete
Chart.js
React
Vue.js
Image Cropper
Stylized Dropzone
Notify
Lazy Image
Swup Integration
Toggle Password
Typed22 pakiety | jeden composer require | zero własnego API | wyszarzone = deprecated
node_modules
assets/vendor.
asset-map:compile -> pliki z hashem,
preload zależności.
composer
require paczka UX dopisuje swój kontroler, a jej biblioteki (np. Tom Select) trafiają do
importmapy.
"fetch": "eager""fetch": "lazy"
<twig:Alert
type="warning"
title="Nic nie znaleziono"
id="empty"
>
Spróbuj "kawa" albo "książka".
</twig:Alert>
{% props %}templates/components i już jest komponent.
attributesid)
ląduje w HTML, a class doklei się do domyślnej.
content.
<twig:ProductCard :product="product">
<twig:block name="actions">
<button>Do koszyka</button>
</twig:block>
</twig:ProductCard>
attributes w HTMLdata-model albo
data-action: Stimulus wysyła propsy + zmiany
Bez API. Bez stanu po stronie JS.
Bez sesji: każdy request
niesie cały stan.
data-model<input data-model="on(change)|debounce(300)|min_length(2)|query">
| Modyfikator | Co robi | Kiedy |
|---|---|---|
debounce(300) |
request dopiero po przerwie w pisaniu (domyślnie 150 ms) | wyszukiwarka, każde pole tekstowe |
on(change) |
aktualizacja po opuszczeniu pola, nie przy każdym klawiszu | pola z walidacją, daty, kwoty |
norender |
zapisuje wartość w JS, request poleci dopiero z akcją | formularz z przyciskiem "Zapisz" |
min_length(2) / max_length(50) |
brak requestu, gdy tekst za krótki lub za długi | nie szukaj po jednej literze |
min_value(0) / max_value(1000) |
brak requestu, gdy liczba poza zakresem | ceny, ilości |
Modyfikatory łączysz przez |, nazwa modelu na końcu. Nieznany modyfikator to błąd w konsoli, nie ciche zignorowanie. |
||
submitForm()data-loading blokuje przycisk
emitUp('cart:quantity-changed')#[LiveListener] + #[LiveArg]emit()
#[LiveListener]Chart (ux-chartjs)
action(generate)|... tylko dla tej akcjimodel(title)|show tylko dla tego polaaddClass, addAttribute(disabled),
delay(300)
defer: request po załadowaniulazy: gdy wjedzie w viewport
{{ ux_icon('lucide:rocket', {class: 'icon'}) }}
{# dev: pobiera na żądanie #}
{# prod: bin/console ux:icons:lock #}
Ikony zablokowane w assets/icons: działa offline, zero requestów.
Encje Doctrine: opcje dociągane AJAX-em w trakcie pisania, nie z HTML-a.
#[AsEntityAutocompleteField]
final class ProductField extends AbstractType
{
public function configureOptions(OptionsResolver $r): void
{
$r->setDefaults([
'class' => Product::class,
'searchable_fields' => ['name'], // LIKE po stronie serwera
]);
}
}
$chart = $chartBuilder
->createChart(Chart::TYPE_DOUGHNUT)
->setData(['labels' => ..., 'datasets' => ...])
->setOptions(['cutout' => '62%']);
{{ render_chart(chart) }}
W Live Component wykres sam się aktualizuje po re-renderze, co widzieliście na wyspach.
$map = (new Map())
->center(new Point(52.0, 19.2))
->zoom(5.6)
->addMarker(new Marker(
position: new Point(53.12, 18.01),
title: 'PHPers Bydgoszcz',
infoWindow: new InfoWindow('Jesteśmy tutaj!', opened: true),
));
Tu bez kafelków (tileLayer(false)), bo prezentacja działa offline.
| React (SPA) | Next.js | Symfony UX Live | |
|---|---|---|---|
| Renderowanie | przeglądarka (CSR) | serwer (SSR/RSC) + hydracja | serwer (Twig) + morphing DOM |
| Osobne API | tak: REST/GraphQL | nie zawsze, ale backend w JS albo BFF przed PHP | nie: komponent to PHP |
| Języki | TS + język backendu | TS (+ PHP, jeśli już jest) | PHP + Twig |
| Stan | klient | klient + serwer | serwer (LiveProp w HTML) |
| Walidacja | podwójna | zwykle podwójna | raz: Symfony Validator |
| Toolchain | Node + bundler | Node + serwer Next | AssetMapper, bez Node |
| JS (min+gzip) | React : + Twój kod | więcej niż React: runtime frameworka | Live: (cała strona demo ) |
| Każda interakcja | lokalnie, natychmiast | lokalnie albo serwer | request do serwera (latencja) |
| Najlepsze do | bogatych, offline'owych UI | treści + aplikacji, SEO | CRUD-ów, paneli, formularzy, zespołów PHP |
| Rozmiary zmierzone przy buildzie tej prezentacji (esbuild minify + gzip -9). React: react + react-dom/client + scheduler, bez routera, zarządzania danymi i kodu aplikacji. Next.js niemierzony: jego runtime zależy od aplikacji. | |||
debounce i data-loading pomagają, ale nie zmienią fizyki.
#[AsLiveComponent],
<twig:...>, data-*. Model nie musi zgadywać architektury.
InteractsWithLiveComponents i debug:twig-component:
agent sam zweryfikuje swój kod.
AGENTS.md w szkieleciefetch /app/_components/...$_SERVER