ИТ развој

Ditch the CRUD: Architecting PHP Services with Long-Term Vision

Отфрлете го CRUD: Архитектирање на PHP услуги со долгорочна визија

CRUD е корисна почетна точка и лоша крајна цел. Операциите за создавање, читање, ажурирање и бришење уредно се вклопуваат во табели, формулари и рани API крајни точки. Тие, исто така, поттикнуваат суптилна архитектонска грешка: третирање на деловен систем како тенка обвивка околу база на податоци.

Тој пристап функционира сè додека софтверот не добие правила, исклучоци, интеграции, повторни обиди, дозволи и втор член на тимот што треба безбедно да го менува. Тогаш крајните точки како POST /orders и PATCH /orders/42 почнуваат да собираат валидација, цени, проверки на залихи, е-пошта, ревизорски записи и повици кон трети страни. Контролерот станува сообраќаен метеж, а секоја нова функционалност ризикува да го промени однесувањето некаде што не е поврзано.

Долговечните PHP услуги имаат потреба од дизајн насочен кон значајни дејства, експлицитни граници и работни текови свесни за неуспеси. Базата на податоци останува важна, но треба да го поддржува однесувањето на апликацијата наместо да го дефинира.

Моделирајте ја работата, не само записите

CRUD API прашува: „Како да го ажурирам овој ред за нарачката?“ Дизајн ориентиран кон услуги прашува: „Што значи да се направи, потврди, откаже или рефундира нарачка?“ Тие прашања создаваат различен код и, уште поважно, различни ограничувања.

Разгледајте нарачка што може да се уредува додека е нацрт, но не смее да се менува откако плаќањето е наплатено. Генеричка крајна точка за ажурирање ги повикува повикувачите да испраќаат произволни полиња. Команда како confirmOrder ја прави транзицијата видлива и обезбедува едно место за спроведување на нејзините правила.

final class ConfirmOrder
{
    public function __construct(
        private OrderRepository $orders,
        private PaymentGateway $payments,
        private TransactionManager $transactions,
    ) {}

    public function handle(OrderId $orderId): void
    {
        $this->transactions->run(function () use ($orderId): void {
            $order = $this->orders->get($orderId);

            if (!$order->canBeConfirmed()) {
                throw new DomainException('Order cannot be confirmed.');
            }

            $order->confirm();
            $this->orders->save($order);
        });
    }
}

Овој пример намерно е скромен. Не тврди дека на секоја апликација ѝ е потребен формален команден автобус или богат доменски модел. Неговата вредност е поедноставна: апликацијата има именувана операција со тесна одговорност. Контролерот може да го автентицира барањето, да го преслика влезот, да ја повика операцијата и да форматира одговор. Тој не треба да биде местото каде што се натрупуваат деловните одлуки.

Користете слоеви за промените да бидат поевтини

Слоевитоста не е формалност сама по себе. Таа е начин да се спречи деталите за рамката, транспортот и складирањето да се шират низ кодот што содржи деловни правила.

  • HTTP или конзолни адаптери се справуваат со парсирање на барања, контекстот за автентикација, валидација на обликот на влезот и форматирање на одговорот.
  • Апликациски услуги ги координираат случаите на употреба: вчитување податоци, започнување трансакции, повикување доменско однесување и одлучување што мора да се случи понатаму.
  • Доменски код изразува правила и транзиции на состојби што се важни без разлика дали повикувачот е API, работник од редица или CLI команда.
  • Инфраструктурни адаптери имплементираат перзистентност, пристап до кеш, надворешни клиенти, складирање датотеки и испорака на пораки.

Границите не мора да бидат апсолутни уште од првиот ден. Мала услуга може да започне со добро именувани апликациски методи и интерфејси на репозиториуми таму каде што сложеноста ги оправдува. Предупредувачкиот знак не е одредена структура на директориуми; туку моментот кога промената на правило за плаќање бара уредување SQL, код за HTTP одговор и повик кон SDK на добавувач во истиот метод.

Чувајте го кодот на рамката на рабовите

Laravel, Symfony и слични рамки решаваат реални проблеми: рутирање, вбризгување зависности, редици, валидација, пристап до база на податоци и набљудливост. Користете ги со ентузијазам, но избегнувајте основните правила директно да зависат од објекти за барања или ORM модели кога тие правила ќе се употребуваат повторно или ќе станат сложени.

ORM моделот често е одлична алатка за перзистентност. Помалку е корисен како универзален дом за секој работен тек. Ако метод на модел почне да испраќа HTTP барања, да диспечира задачи, да пресметува политики и да менува повеќе агрегати, преместете ја оркестрацијата во апликациска услуга. Ова, исто така, овозможува фокусирани тестови без подигнување на цела апликација.

Трансакциите ги штитат податоците; на работните текови им треба повеќе

Трансакција во база на податоци може атомски да зачува нарачка и нејзината состојба на плаќање. Не може атомски да ги зачува тие записи и да гарантира дека давател на е-пошта, платформа за инвентар или потрошувач на webhook ќе прими барање. Обидот тоа да се реши со повик кон оддалечено API во трансакција создава незгодни начини на неуспех: оддалечената страна може да успее додека базата на податоци се враќа назад, или бавна зависност може да држи заклучувања подолго од потребното.

Практичен одговор е шаблонот за трансакциска поштенска кутија. Во истата трансакција во база на податоци што ја менува деловната состојба, зачувајте настан што ја опишува работата што треба да се изврши по потврдувањето. Работник ги чита настаните што чекаат и ги испорачува со логика за повторни обиди.

$transaction->run(function () use ($order, $outbox): void {
    $order->confirm();
    $orders->save($order);

    $outbox->add(new OutboxMessage(
        type: 'order.confirmed',
        payload: ['order_id' => (string) $order->id()]
    ));
});

Работникот мора да претпостави дека испораката се случува најмалку еднаш. Пораката може успешно да биде испратена, а потоа да настане неуспех пред да биде означена како завршена. Затоа на потрошувачите им е потребна идемпотентност: обработката на истиот настан двапати не смее да наплати двапати, да создаде дупликатни испораки или да испрати противречни промени на состојбата. Стабилните идентификатори на настани, уникатните ограничувања и клучевите за идемпотентност поддржани од добавувачите се корисни алатки, но на секоја интеграција ѝ е потребен сопствен експлицитен дизајн.

Дизајнирајте API околу намерата и еволуцијата

Крајните точки за ресурси остануваат вредни за едноставни читања и едноставно одржување на податоци. Проблемот е присилувањето на секоја деловна операција да поминува низ генерички глаголи. Крајна точка заснована на намера, како POST /orders/{id}/confirm, може да биде појасна од patch payload чие значење зависи од комбинација на полиња.

Јасноста, исто така, го подобрува справувањето со грешки. Разликувајте неправилно форматиран влез од валидно барање што прекршува деловно правило и разликувајте ги и двете од привремен инфраструктурен неуспех. На клиентите не им требаат внатрешни stack traces, но им требаат стабилни облици на грешки и кодови што овозможуваат дејствување. Логирањето треба да ги задржи идентификаторите за корелација и контекстот потребни за истражување на неуспехот, без откривање тајни.

Еволуцијата на API заслужува иста грижа. Додавањето опционални полиња обично е полесно од менувањето на значењето на постојните полиња. Избегнувајте да ги изложувате имињата на колоните од базата на податоци како јавен договор кога доменскиот речник е попрецизен. Одговорот треба да опишува што им е потребно на клиентите, а не да ја пресликува која било табела што случајно постои денес.

Направете ја перформансата својство на дизајнот

Повеќето проблеми со перформансите во PHP не се решаваат со паметна синтакса. Тие произлегуваат од непотребна работа: N+1 барања, неограничени множества на резултати, повторени оддалечени повици, преголеми payload-и и задачи во редица што се обидуваат повторно премногу агресивно.

Мерете пред да оптимизирате, а потоа направете ја цената видлива. Страницирајте ги крајните точки за колекции, избирајте само потребни податоци, однапред вчитувајте познати односи кога е соодветно и поставете временски ограничувања за појдовни повици. Кеширајте податоци само кога можете да наведете што го поништува кешот и што се случува кога кешот е недостапен. Брз, но застарен одговор може да биде прифатлив за каталог на производи, а неприфатлив за достапно салдо.

Контејнерите помагаат распоредувањето да биде повторливо, но Docker не е архитектура. Изградете непроменливи слики на апликацијата, обезбедете конфигурација преку околината или управуван механизам за тајни и извршувајте миграции на базата на податоци како намерен чекор на распоредувањето. Не претпоставувајте дека новостартиран контејнер е подготвен само затоа што неговиот процес постои; подготвеноста зависи од зависностите потребни за безбедно опслужување сообраќај.

Изберете го следниот спој, не најголемото преработување

Долгорочната визија не бара предвидување на секоја идна функционалност. Таа значи следната важна промена да има чисто место каде да се смести. Извлечете случај на употреба кога неговите правила растат. Воведете поштенска кутија кога се важни сигурните странични ефекти. Одделете надворешен клиент кога зависноста станува оперативно значајна. Одржете едноставно барање едноставно кога навистина е такво.

Целта не е да се забрани CRUD. Таа е CRUD да се стави на неговото соодветно место: како погодност за едноставни операции со податоци, а не како ментален модел за целиот бизнис. Системите стануваат издржливи кога нивниот код ги одразува одлуките што бизнисот навистина ги носи. Тоа е архитектурата што вреди да се одржува.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.