DigiSpace
  • автор admin

Архітектура Symfony для складних PHP-застосунків: модулі, Messenger та інтеграції

Архітектура Symfony для складних PHP-застосунків: модулі, Messenger та інтеграції

Symfony корисний тоді, коли PHP-застосунок переростає набір контролерів та інтеграцій. Фреймворк дає команді явні межі — HTTP, валідацію, серіалізацію, обмін повідомленнями й конфігурацію — не нав’язуючи одну форму для всього продукту. Цінність з’являється тоді, коли ці межі залишаються зрозумілими після кількох років роботи системи.

Бізнес-результат — система, яка поглинає складність без перетворення кожної зміни на ризикований реліз: доменні модулі залишаються читабельними, повільна робота виходить із request path, а зовнішні інтеграції можна замінити за відомим контрактом.

Обирайте Symfony за формою задачі

Невеликому CRUD-застосунку не потрібна складна архітектура. Symfony має сенс, коли в системі кілька бізнес-доменів, довгоживучі інтеграції, асинхронні workflow або команда хоче використовувати окремі компоненти поза full-stack монолітом. Рішення має випливати з цих обмежень, а не з прихильності до бренду фреймворку.

Публічний застосунок DigiSpace побудований на Laravel, тому ця стаття є технічним посібником, а не заявою про production-репозиторій на Symfony. Патерни спираються на офіційні компоненти Symfony та ті самі інженерні питання, які видно в нашій PHP-роботі: явна валідація запитів, тонкі контролери, тестовані сервіси, черги, зовнішні API й деплої з можливістю відкату.

Життєвий цикл запиту Symfony з модульним кодом і асинхронним транспортом Messenger

Залишайте HTTP-шар простим

Symfony-контролер має перетворити HTTP-запит на application command, а результат — на HTTP-відповідь. Він не повинен вирішувати, як рахується рахунок, як повторюється виклик провайдера або як координується транзакція. Такі рішення належать application services або handlers, які можна тестувати без запуску всього kernel.

final class CreateOrderController
{
    public function __construct(
        private CreateOrderHandler $handler,
    ) {}

    public function __invoke(CreateOrderRequest $request): JsonResponse
    {
        $order = $this->handler->handle(
            new CreateOrderCommand(
                customerId: $request->customerId(),
                lines: $request->lines(),
            ),
        );

        return new JsonResponse(['id' => $order->id()], Response::HTTP_CREATED);
    }
}

Важлива саме межа. Валідація може відхилити неправильний input на краю, а handler працює з command відомої форми. Це спрощує майбутній перехід від синхронного контролера до message handler.

Організовуйте код навколо доменів, а не папок

Symfony не вимагає одного layout директорій. У системі, що зростає, організовуйте код навколо бізнес-можливостей: Orders, Billing або Catalog. Кожен модуль може містити application commands, domain rules, infrastructure adapters і HTTP entry points. Важливіше за назви папок те, щоб залежності мали зрозумілий напрям.

Доменний модуль не повинен знати, чи email відправив Symfony Mailer, queue worker або test double. Він має виразити event або command. Інфраструктура вирішує, як це повідомлення передається. Так зміна провайдера стає обмеженою задачею, а не пошуком по всіх контролерах.

Використовуйте Messenger, коли запит має завершуватися швидше

Symfony Messenger підтримує і негайну обробку, і transport, що виконує повідомлення пізніше. Команда може почати із синхронної шини повідомлень, а окрему роботу перенести в queue, коли цього вимагають latency або failure behaviour. Message — це мала серіалізована інструкція, а handler володіє side effect.

final readonly class GenerateInvoice
{
    public function __construct(public string $invoiceId) {}
}

#[AsMessageHandler]
final class GenerateInvoiceHandler
{
    public function __invoke(GenerateInvoice $message): void
    {
        // Завантажити рахунок, створити PDF, зберегти його, повідомити користувача.
    }
}

Фонова робота потребує operational policy. Налаштуйте retry для тимчасових помилок провайдера, failure transport для повідомлень, які не обробилися, та idempotency rule, щоб повтор не створив подвійне списання або повідомлення. Queue — це механізм доставки, а не автоматична безпека неідемпотентної операції.

Ховайте зовнішні системи за adapter-ами

Symfony HttpClient дає єдиний client abstraction, але він не повинен просочувати формати відповіді провайдера в application code. Створіть невеликий adapter, який мапить provider request і response у власні value objects. Timeout, authentication, retry policy та logging тримайте в adapter або його transport configuration.

Це особливо корисно для платежів, синхронізації CRM та webhook. Застосунок може тестувати помилку провайдера без реального виклику, а adapter — мати окремий contract test проти sandbox. Коли провайдер змінює payload, спочатку змінюється одна межа.

Робіть конфігурацію та секрети явними

Довгоживучі Symfony-системи часто ламаються на конфігурації: worker має інше середовище, CLI-команда не бачить секрет або staging endpoint потрапляє в production. Розглядайте конфігурацію як input застосунку. Перевіряйте обов’язкові значення під час старту, зберігайте секрети поза репозиторієм, а середовище worker внесіть до deployment checklist.

Не ховайте операційні рішення у static globals. Типізований configuration object або injected parameter робить залежність видимою в constructor і зрозумілою в тесті.

Тестуйте контракти, які справді важливі

Unit-тести корисні для domain rules, але не замінюють HTTP та integration tests. Практичний Symfony-набір має три рівні: швидкі тести розрахунків і політик, application tests для command та handler behaviour і менший набір HTTP-тестів для routing, validation та serialization. Інтеграційні тести мають покривати межі, де губляться дані: queues, webhook, databases та external clients.

Під час модернізації наявного PHP-застосунку characterization tests — безпечний перший крок. Зафіксуйте поточну відповідь і side effects до перенесення коду. Потім замінюйте один модуль або інтеграцію за раз, залишаючи оборотний route або feature switch.

Плануйте деплой з урахуванням worker-ів і messages

У Symfony є деталь, якої може не бути у звичайному HTTP-деплої: worker-и довгоживучі. Новий реліз може містити змінений message class або handler, а старий worker ще тримає попередній код. Перезапускайте worker-и під час релізу, використовуйте сумісні зміни messages під час rolling deploy і стежте за failure transport після активації.

Міграції бази спочатку мають бути additive. Спершу додайте колонку або таблицю, випустіть код, який читає обидві версії, зробіть backfill, а потім видаліть compatibility code в наступному релізі. Так rollback не залежить від стану бази, якого вже немає.

Що цей підхід дає бізнесу

Модульність знижує ціну паралельної роботи й прояснює відповідальність. Messenger прибирає повільну або ненадійну роботу з user request. Adapter-и роблять заміну провайдера контрольованою. Явна конфігурація та перезапуск worker-ів зменшують клас інцидентів “працює лише в одному середовищі”. Ці переваги практичні й не вимагають обіцяти, що Symfony прибере всю складність.

Коли Laravel або окремі компоненти підходять краще

Не варто обирати Symfony автоматично. Laravel може бути швидшим для продукту, якому потрібні його конвенції та інтегровані інструменти. Меншій системі можуть знадобитися лише Symfony components — HttpClient, Messenger або Serializer — без повного фреймворку. Правильна відповідь — найменший набір меж, який зберігає бізнес-правила ясними й операційні ризики видимими.

Опис послуги — на сторінці Розробка на Symfony. Якщо ви обираєте між Symfony, Laravel або окремими компонентами, почніть із доменів, failure modes інтеграцій і життєвого циклу worker-ів.

Share this post