Razvoj

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

Zaboravite CRUD: Projektiranje PHP usluga s dugoročnom vizijom

CRUD je korisna polazna točka, ali loše odredište. Operacije stvaranja, čitanja, ažuriranja i brisanja uredno se preslikavaju na tablice, obrasce i rane API krajnje točke. Također potiču suptilnu arhitektonsku pogrešku: tretiranje poslovnog sustava kao tankog omotača oko baze podataka.

Takav pristup funkcionira sve dok softver ne dobije pravila, iznimke, integracije, ponovne pokušaje, dozvole i drugog člana tima koji ga mora sigurno mijenjati. Tada krajnje točke poput POST /orders i PATCH /orders/42 počinju prikupljati validaciju, određivanje cijena, provjere zaliha, e-poruke, zapise revizije i pozive prema uslugama trećih strana. Kontroler postaje prometni čep, a svaka nova značajka riskira promjenu ponašanja na nekom nepovezanom mjestu.

Dugotrajne PHP usluge trebaju dizajn usmjeren na smislene radnje, eksplicitne granice i tijekove rada svjesne neuspjeha. Baza podataka ostaje važna, ali treba podržavati ponašanje aplikacije, a ne ga definirati.

Modelirajte rad, a ne samo zapise

CRUD API pita: „Kako ažurirati ovaj red narudžbe?” Dizajn usmjeren na usluge pita: „Što znači poslati, potvrditi, otkazati ili refundirati narudžbu?” Ta pitanja stvaraju drukčiji kod i, što je još važnije, drukčija ograničenja.

Razmotrite narudžbu koja se može uređivati dok je nacrt, ali se ne smije mijenjati nakon što se naplata izvrši. Generička krajnja točka za ažuriranje poziva pozivatelje da pošalju proizvoljna polja. Naredba poput confirmOrder čini prijelaz vidljivim i pruža jedno mjesto za provedbu njegovih pravila.

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);
        });
    }
}

Ovaj je primjer namjerno skroman. Ne tvrdi da svaka aplikacija treba formalni sabirnik naredbi ili bogat model domene. Njegova je vrijednost jednostavnija: aplikacija ima imenovanu operaciju s uskom odgovornošću. Kontroler može autentificirati zahtjev, preslikati ulaz, pozvati operaciju i oblikovati odgovor. Ne bi trebao biti mjesto na kojem se gomilaju poslovne odluke.

Koristite slojeve kako bi promjene bile jeftinije

Slojevitost nije ceremonija radi same sebe. To je način da se spriječi širenje pojedinosti okvira, prijenosa i pohrane kroz kod koji sadrži poslovna pravila.

  • HTTP ili konzolni adapteri obrađuju parsiranje zahtjeva, kontekst autentifikacije, validaciju oblika ulaza i oblikovanje odgovora.
  • Aplikacijske usluge koordiniraju slučajeve upotrebe: učitavanje podataka, pokretanje transakcija, pozivanje ponašanja domene i odlučivanje o tome što se mora dogoditi sljedeće.
  • Kod domene izražava pravila i prijelaze stanja koji su važni bez obzira na to je li pozivatelj API, radnik reda čekanja ili CLI naredba.
  • Infrastrukturni adapteri implementiraju trajnu pohranu, pristup predmemoriji, vanjske klijente, pohranu datoteka i dostavu poruka.

Granice ne moraju biti apsolutne prvog dana. Mala usluga može započeti s dobro imenovanim aplikacijskim metodama i sučeljima repozitorija tamo gdje ih složenost opravdava. Znak upozorenja nije određena struktura direktorija; to je situacija u kojoj promjena pravila naplate zahtijeva uređivanje SQL-a, koda HTTP odgovora i poziva SDK-a dobavljača u istoj metodi.

Zadržite kod okvira na rubovima

Laravel, Symfony i slični okviri rješavaju stvarne probleme: usmjeravanje, ubrizgavanje ovisnosti, redove čekanja, validaciju, pristup bazi podataka i praćenje rada sustava. Koristite ih s oduševljenjem, ali izbjegavajte da temeljna pravila izravno ovise o objektima zahtjeva ili ORM modelima kada će se ta pravila ponovno koristiti ili postati složena.

ORM model često je izvrstan alat za trajnu pohranu. Manje je koristan kao univerzalno mjesto za svaki tijek rada. Ako metoda modela počne slati HTTP zahtjeve, raspoređivati poslove, izračunavati pravila i mijenjati više agregata, premjestite orkestraciju u aplikacijsku uslugu. To također omogućuje fokusirane testove bez pokretanja cijele aplikacije.

Transakcije štite podatke; tijekovi rada trebaju više

Transakcija baze podataka može atomski spremiti narudžbu i njezino stanje naplate. Ne može atomski spremiti te zapise i jamčiti da pružatelj e-pošte, platforma za zalihe ili potrošač webhooka prima zahtjev. Pokušaj rješavanja toga pozivom udaljenog API-ja unutar transakcije stvara nezgodne načine neuspjeha: udaljena strana može uspjeti dok se baza podataka vraća na prethodno stanje ili spora ovisnost može držati zaključavanja dulje nego što je potrebno.

Praktičan odgovor je obrazac transakcijskog odlaznog spremnika. U istoj transakciji baze podataka koja mijenja poslovno stanje, pohranite događaj koji opisuje posao koji treba izvršiti nakon potvrde transakcije. Radnik čita događaje na čekanju i isporučuje ih uz logiku ponovnih pokušaja.

$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()]
    ));
});

Radnik mora pretpostaviti da se isporuka događa najmanje jednom. Poruka može biti uspješno poslana, a zatim može doći do neuspjeha prije nego što se označi kao dovršena. Potrošačima je stoga potrebna idempotentnost: obrada istog događaja dvaput ne smije dvaput naplatiti, stvoriti dvostruke pošiljke ni poslati proturječne promjene stanja. Stabilni identifikatori događaja, jedinstvena ograničenja i ključevi idempotentnosti koje podržava pružatelj korisni su alati, ali svaka integracija treba vlastiti eksplicitni dizajn.

Dizajnirajte API-je oko namjere i evolucije

Krajnje točke resursa i dalje su vrijedne za jednostavna čitanja i jednostavno održavanje podataka. Problem je prisiljavati svaku poslovnu operaciju kroz generičke glagole. Krajnja točka utemeljena na namjeri poput POST /orders/{id}/confirm može biti jasnija od sadržaja zakrpe čije značenje ovisi o kombinaciji polja.

Jasnoća također poboljšava rukovanje pogreškama. Razlikujte neispravno oblikovan ulaz od valjanog zahtjeva koji krši poslovno pravilo, a oba razlikujte od privremenog infrastrukturnog neuspjeha. Klijentima ne trebaju interni tragovi stoga, ali trebaju stabilni oblici pogrešaka i kodovi koji omogućuju djelovanje. Zapisivanje treba zadržati identifikatore korelacije i kontekst potreban za istraživanje neuspjeha bez otkrivanja tajni.

Evolucija API-ja zaslužuje jednaku pažnju. Dodavanje neobaveznih polja obično je lakše od mijenjanja značenja postojećih polja. Izbjegavajte otkrivanje naziva stupaca baze podataka kao javnog ugovora kada je vokabular domene precizniji. Odgovor treba opisivati ono što klijenti trebaju, a ne zrcaliti tablicu koja se slučajno danas koristi.

Učinite performanse svojstvom dizajna

Većina problema s performansama PHP-a ne rješava se domišljatom sintaksom. Nastaju zbog nepotrebnog rada: N+1 upita, neograničenih skupova rezultata, ponovljenih udaljenih poziva, prevelikih sadržaja i poslova reda čekanja koji se preagresivno ponavljaju.

Mjerite prije optimiziranja, a zatim učinite trošak vidljivim. Paginirajte krajnje točke kolekcija, odabirite samo potrebne podatke, unaprijed učitajte poznate odnose kada je prikladno i postavite vremenska ograničenja za odlazne pozive. Podatke spremajte u predmemoriju samo kada možete navesti što je poništava i što se događa kada predmemorija nije dostupna. Brz, ali zastario odgovor može biti prihvatljiv za katalog proizvoda, a neprihvatljiv za raspoloživo stanje.

Spremnici pomažu da implementacija bude ponovljiva, ali Docker nije arhitektura. Izgradite nepromjenjive slike aplikacije, osigurajte konfiguraciju putem okruženja ili upravljanog mehanizma za tajne i pokrećite migracije baze podataka kao namjeran korak implementacije. Nemojte pretpostaviti da je novopokrenuti spremnik spreman samo zato što njegov proces postoji; spremnost ovisi o ovisnostima potrebnima za sigurno posluživanje prometa.

Odaberite sljedeći spoj, a ne najgrandioznije prepisivanje

Dugoročna vizija ne zahtijeva predviđanje svake buduće značajke. Znači ostaviti sljedećoj važnoj promjeni čisto mjesto za uklapanje. Izdvojite slučaj upotrebe kada njegova pravila rastu. Uvedite odlazni spremnik kada su pouzdani popratni učinci važni. Odvojite vanjskog klijenta kada ovisnost postane operativno značajna. Zadržite jednostavan upit jednostavnim kada to doista jest.

Cilj nije zabraniti CRUD. Cilj je smjestiti CRUD na njegovo pravo mjesto: kao pogodnost za jednostavne podatkovne operacije, a ne kao mentalni model za cijelo poslovanje. Sustavi postaju trajni kada njihov kod odražava odluke koje poslovanje doista donosi. To je arhitektura koju vrijedi održavati.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.