Symfony Live Components และ UX 3.0: แอปพลิเคชันแบบ Reactive โดยไม่ต้องใช้ JavaScript ในปี 2026

Symfony Live Components สร้างอินเทอร์เฟซแบบ reactive ด้วย PHP และ Twig โดยไม่ต้องใช้ JavaScript บทช่วยสอนเกี่ยวกับ LiveProp, LiveAction, form และ deferred loading

Symfony Live Components และ UX 3.0 บทช่วยสอน

Symfony Live Components นำเสนออินเทอร์เฟซผู้ใช้แบบ reactive และ real-time ทั้งหมดด้วย PHP และ Twig เปิดตัวในเดือนเมษายน 2026 Symfony UX 3.0 ลบ deprecation ทั้งหมดจากเวอร์ชัน 2.x ยกระดับความต้องการขั้นต่ำเป็น PHP 8.4 และ Symfony 7.4 พร้อมทั้งตัดแพ็กเกจที่ล้าสมัยออกสี่ตัว — สร้าง UX toolkit ที่กระชับที่สุดเท่าที่เคยมีมา

Live Components คืออะไร?

Live Components เปลี่ยน component Twig ใดก็ได้ให้เป็นเอลิเมนต์ที่มี state และโต้ตอบได้ ซึ่งถูก render ใหม่ฝั่ง server เมื่อผู้ใช้มีปฏิสัมพันธ์ — โดยไม่ต้องเขียน JavaScript data binding, action, validation และการจัดการ form ทั้งหมดเกิดขึ้นใน PHP

การตั้งค่า Symfony UX 3.0 กับ Live Components

การติดตั้งต้องใช้ Symfony 7.4+, PHP 8.4+ และ AssetMapper หรือ Webpack Encore สำหรับ asset ฝั่ง frontend

bash
# terminal
composer require symfony/ux-live-component

# With AssetMapper (recommended for Symfony 7.4+)
php bin/console importmap:require @symfony/ux-live-component

# Verify installation
php bin/console debug:twig --filter=component

AssetMapper จัดการการส่งมอบ JavaScript โดยไม่ต้องใช้ Node.js, Webpack หรือขั้นตอน build ใดๆ Stimulus controller ที่ขับเคลื่อน Live Components จะถูกโหลดโดยอัตโนมัติ

การสร้าง Component ค้นหาแบบ Reactive ด้วย LiveProp

แถบค้นหาที่กรองผลลัพธ์ขณะที่ผู้ใช้พิมพ์ — ไม่ต้องใช้ JavaScript เลย แอตทริบิวต์ #[LiveProp] ทำเครื่องหมาย property ว่ามี state ซึ่งจะถูกรักษาไว้ระหว่างการ re-render ผ่าน token ที่เข้ารหัสและปลอดภัยสำหรับ URL

php
<?php
// src/Twig/Components/ProductSearch.php

namespace App\Twig\Components;

use App\Repository\ProductRepository;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class ProductSearch
{
    use DefaultActionTrait;

    // writable: true allows the frontend to modify this property
    // url: true syncs the value with ?query= in the address bar
    #[LiveProp(writable: true, url: true)]
    public string $query = '';

    #[LiveProp(writable: true)]
    public string $category = 'all';

    public function __construct(
        private ProductRepository $productRepository,
    ) {
    }

    public function getProducts(): array
    {
        if (strlen($this->query) < 2) {
            return [];
        }

        return $this->productRepository->search(
            query: $this->query,
            category: $this->category === 'all' ? null : $this->category,
            limit: 20,
        );
    }
}

เทมเพลต Twig ที่สอดคล้องกันจะผูก input กับ property ด้วย data-model

twig
{# templates/components/ProductSearch.html.twig #}
<div {{ attributes }}>
    <input
        type="search"
        data-model="debounce(300)|query"
        placeholder="Search products..."
        class="form-input w-full"
    />

    <select data-model="category" class="form-select mt-2">
        <option value="all">All categories</option>
        <option value="electronics">Electronics</option>
        <option value="books">Books</option>
    </select>

    <div class="mt-4">
        {% for product in this.products %}
            <div key="{{ product.id }}" class="border-b py-2">
                <h3>{{ product.name }}</h3>
                <span>{{ product.price|format_currency('EUR') }}</span>
            </div>
        {% else %}
            {% if query|length >= 2 %}
                <p>No results for "{{ query }}".</p>
            {% endif %}
        {% endfor %}
    </div>
</div>

Modifier debounce(300) รอ 300ms หลังจากการกดแป้นพิมพ์ครั้งสุดท้ายก่อนส่ง request แอตทริบิวต์ key บน item ของรายการช่วยให้ DOM morphing ทำงานอย่างมีประสิทธิภาพ — เฉพาะเอลิเมนต์ที่เปลี่ยนแปลงเท่านั้นที่ถูกอัปเดต

URL Binding ด้วย LiveProp

การตั้งค่า url: true บน LiveProp จะซิงค์ค่า property กับ URL ของเบราว์เซอร์เป็น query parameter ผลการค้นหาที่สามารถ bookmark และแชร์ได้ — ด้วยแอตทริบิวต์เดียว

การจัดการ Action ของผู้ใช้ด้วย LiveAction

นอกเหนือจาก data binding แล้ว #[LiveAction] เปิดเผย method PHP เป็น action ที่สามารถเรียกใช้จาก frontend ได้ อาร์กิวเมนต์ถูกส่งผ่าน #[LiveArg]

php
<?php
// src/Twig/Components/ShoppingCart.php

namespace App\Twig\Components;

use App\Entity\CartItem;
use App\Service\CartService;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class ShoppingCart
{
    use DefaultActionTrait;
    use ComponentToolsTrait;

    #[LiveProp]
    public array $items = [];

    public function __construct(
        private CartService $cartService,
    ) {
    }

    #[LiveAction]
    public function addItem(#[LiveArg] int $productId, #[LiveArg] int $quantity = 1): void
    {
        $this->cartService->add($productId, $quantity);
        $this->items = $this->cartService->getItems();

        // Notify other components on the page
        $this->emit('cart:updated', [
            'count' => count($this->items),
        ]);
    }

    #[LiveAction]
    public function removeItem(#[LiveArg] int $itemId): void
    {
        $this->cartService->remove($itemId);
        $this->items = $this->cartService->getItems();
        $this->emit('cart:updated', ['count' => count($this->items)]);
    }

    public function getTotal(): float
    {
        return array_sum(array_map(
            fn(CartItem $item) => $item->getPrice() * $item->getQuantity(),
            $this->items,
        ));
    }
}

เทมเพลตเรียก action ด้วย helper live_action() หรือแอตทริบิวต์ data-action

twig
{# templates/components/ShoppingCart.html.twig #}
<div {{ attributes }}>
    <h2>Cart ({{ items|length }})</h2>

    {% for item in items %}
        <div key="{{ item.id }}" class="flex justify-between py-2">
            <span>{{ item.name }} x{{ item.quantity }}</span>
            <span>{{ (item.price * item.quantity)|format_currency('EUR') }}</span>
            <button {{ live_action('removeItem', { itemId: item.id }) }}
                    class="text-red-600">
                Remove
            </button>
        </div>
    {% endfor %}

    <div class="border-t pt-2 font-bold">
        Total: {{ this.total|format_currency('EUR') }}
    </div>
</div>

เมธอด emit() กระจาย event ไปยัง Live Components อื่นบนหน้า — ตัวอย่างเช่น component badge ตะกร้าสินค้าสามารถรับฟังด้วย #[LiveListener('cart:updated')] และอัปเดตจำนวนโดยไม่ต้องโหลดหน้าใหม่ทั้งหมด

พร้อมที่จะพิชิตการสัมภาษณ์ Symfony แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การตรวจสอบ Form แบบ Real-Time โดยไม่ต้องใช้ JavaScript

Live Components ทำงานร่วมกับ Symfony Forms ได้โดยตรง ComponentWithFormTrait เชื่อมต่อสถานะ form, ข้อผิดพลาดจาก validation และการ submit เข้ากับ lifecycle ของ component

php
<?php
// src/Twig/Components/RegistrationForm.php

namespace App\Twig\Components;

use App\Entity\User;
use App\Form\RegistrationType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class RegistrationForm extends AbstractController
{
    use DefaultActionTrait;
    use ComponentWithFormTrait;

    #[LiveProp]
    public ?User $initialFormData = null;

    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(RegistrationType::class, $this->initialFormData ?? new User());
    }

    #[LiveAction]
    public function save(EntityManagerInterface $em): mixed
    {
        // Submits + validates — re-renders with errors if invalid
        $this->submitForm();
        $user = $this->getForm()->getData();

        $em->persist($user);
        $em->flush();

        $this->addFlash('success', 'Account created.');

        return $this->redirectToRoute('app_login');
    }
}
twig
{# templates/components/RegistrationForm.html.twig #}
<div {{ attributes }}>
    {{ form_start(form, {
        attr: {
            'data-action': 'live#action:prevent',
            'data-live-action-param': 'save'
        }
    }) }}
        {{ form_row(form.email) }}
        {{ form_row(form.plainPassword, { label: 'Password' }) }}
        {{ form_row(form.fullName) }}

        <button type="submit"
                class="btn-primary w-full"
                data-loading="addAttribute(disabled) addClass(opacity-50)">
            <span data-loading="hide">Create Account</span>
            <span data-loading="show" class="animate-spin">Loading...</span>
        </button>
    {{ form_end(form) }}
</div>

ข้อผิดพลาดจาก validation จะแสดงแบบ inline เมื่อผู้ใช้เลื่อนระหว่างฟิลด์ — modifier on(change) บน data-model จะทริกเกอร์ re-render เมื่อ blur แสดงข้อความ constraint จาก Symfony Validator ทันที

Deferred และ Lazy Loading เพื่อประสิทธิภาพ

Component ที่หนัก — dashboard, กราฟวิเคราะห์, รายการยาว — ได้ประโยชน์จาก deferred rendering แทนที่จะบล็อกการโหลดหน้าแรก component จะแสดง placeholder และดึงเนื้อหาผ่าน AJAX หลังจากหน้าพร้อมแล้ว

php
<?php
// src/Twig/Components/AnalyticsDashboard.php

namespace App\Twig\Components;

use App\Service\AnalyticsService;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class AnalyticsDashboard
{
    use DefaultActionTrait;

    #[LiveProp]
    public string $period = '30d';

    public function __construct(
        private AnalyticsService $analytics,
    ) {
    }

    public function getMetrics(): array
    {
        // Expensive query — runs only after page load
        return $this->analytics->getMetrics($this->period);
    }
}
twig
{# Parent page: load the dashboard after the page renders #}
<twig:AnalyticsDashboard loading="defer" period="30d" />

{# Or load when scrolled into view #}
<twig:AnalyticsDashboard loading="lazy" period="30d" />

มาโคร placeholder ภายในเทมเพลต component กำหนดสิ่งที่ผู้ใช้เห็นขณะที่เนื้อหาจริงกำลังโหลด

twig
{# templates/components/AnalyticsDashboard.html.twig #}
<div {{ attributes }}>
    <h2>Analytics — {{ period }}</h2>
    {% for metric in this.metrics %}
        <div class="stat-card">
            <span class="stat-label">{{ metric.label }}</span>
            <span class="stat-value">{{ metric.value|number_format }}</span>
        </div>
    {% endfor %}
</div>

{% macro placeholder(props) %}
    <div class="animate-pulse space-y-4">
        <div class="h-6 bg-gray-200 rounded w-1/3"></div>
        <div class="h-20 bg-gray-200 rounded"></div>
        <div class="h-20 bg-gray-200 rounded"></div>
    </div>
{% endmacro %}

โหมด loading="defer" ส่ง AJAX request ทันทีเมื่อหน้าโหลด โหมด loading="lazy" ใช้ IntersectionObserver — request จะถูกส่งเมื่อ component เลื่อนเข้ามาใน viewport เท่านั้น ทั้งสองวิธีรักษาการตอบสนองของหน้าแรกให้เร็ว

CSRF ถูกลบใน UX 3.0

Symfony UX 3.0 แทนที่ token CSRF ด้วยการป้องกัน same-origin/CORS สำหรับ Live Components อาร์กิวเมนต์ csrf บน #[AsLiveComponent] ไม่มีอีกต่อไป ต้องแน่ใจว่า server บังคับใช้นโยบาย same-origin

การสื่อสารระหว่าง Component ด้วย Events

Live Components สื่อสารผ่านระบบ event component ลูกส่ง event ออกไป component แม่ (หรือ sibling) รับฟังและตอบสนอง

php
<?php
// src/Twig/Components/CartBadge.php

namespace App\Twig\Components;

use App\Service\CartService;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class CartBadge
{
    use DefaultActionTrait;

    #[LiveProp]
    public int $count = 0;

    public function __construct(CartService $cartService)
    {
        $this->count = $cartService->getItemCount();
    }

    // Automatically re-renders when ShoppingCart emits 'cart:updated'
    #[LiveListener('cart:updated')]
    public function onCartUpdated(#[LiveArg] int $count): void
    {
        $this->count = $count;
    }
}

รูปแบบนี้รักษาให้ component แยกจากกัน component ShoppingCart ไม่ได้อ้างอิง CartBadge โดยตรง — event bus จัดการการเชื่อมต่อ

สิ่งที่เปลี่ยนแปลงใน Symfony UX 3.0

UX 3.0 เป็นรุ่นที่ทำความสะอาด แอปพลิเคชันที่ทำงานโดยไม่มีคำเตือน deprecation บน 2.x สามารถอัปเกรดได้โดยมีอุปสรรคน้อยที่สุด

| การเปลี่ยนแปลง | ก่อน (2.x) | หลัง (3.0) | |---------------|------------|------------| | การป้องกัน CSRF | csrf: true บน #[AsLiveComponent] | Same-origin/CORS (อัตโนมัติ) | | ฟังก์ชัน Twig CVA | cva() | html_cva() จาก twig/html-extra 3.12+ | | Config defaults ของ component | ไม่บังคับ | twig_component.defaults บังคับ | | แพ็กเกจที่ถูกลบ | Swup, LazyImage, Typed, TogglePassword | ใช้ native API หรือ UX Toolkit | | ความต้องการ PHP | 8.1+ | 8.4+ | | ความต้องการ Symfony | 6.4+ | 7.4+ |

UX Toolkit ซึ่งเปิดตัวในช่วง 2.x มี component UI สำเร็จรูป (Button, Dialog, Card, Table, Pagination) ที่ styled ด้วย Shadcn UI หรือ Flowbite 4.0 — เติมเต็มช่องว่างที่แพ็กเกจที่ถูกลบทิ้งไว้

กำลังเตรียมคำถามสัมภาษณ์งาน Symfony อยู่หรือไม่? ความเข้าใจเกี่ยวกับ Live Components และระบบ event มีความเกี่ยวข้องมากขึ้นสำหรับตำแหน่งระดับ senior คู่มือคำถามสัมภาษณ์ Symfony ครอบคลุมหัวข้อพื้นฐานที่เหมาะสำหรับจับคู่กับความรู้เชิงปฏิบัตินี้

สรุป

  • Live Components ขจัดความจำเป็นในการใช้ framework JavaScript ในแอปพลิเคชัน Symfony ที่เน้น CRUD ส่วนใหญ่ data binding, action, validation และการจัดการ form ยังคงอยู่ใน PHP และ Twig
  • #[LiveProp(writable: true, url: true)] สร้างอินเทอร์เฟซที่มี state สามารถ bookmark และแชร์ได้ด้วยแอตทริบิวต์เดียว
  • Deferred และ lazy loading (loading="defer" / loading="lazy") รักษาการโหลดหน้าแรกให้เร็วในขณะที่ component หนักถูก render แบบ asynchronous
  • UX 3.0 ยกเลิก token CSRF เพื่อใช้การป้องกัน same-origin/CORS — ความปลอดภัยที่เรียบง่ายกว่าด้วยชิ้นส่วนที่เคลื่อนไหวน้อยลง
  • ระบบ event (emit / #[LiveListener]) ช่วยให้การสื่อสารระหว่าง component แยกจากกันโดยไม่ต้องจัดการ state แบบ global
  • สำหรับ API ให้ผสม Live Components กับ API Platform บน Symfony 7 สำหรับสถาปัตยกรรมที่เน้น backend

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#symfony
#live-components
#ux-3
#php
#reactive

แชร์

บทความที่เกี่ยวข้อง