Hlavní navigace

DI naposled a kuchání PHP Jet

24. 2. 2023 7:58 Mirek Marek

Dnes bych rád definitivně uzavřel ožehavé téma Dependency Injection a jeho použití v PHP Jet a zároveň je přesunul na další téma – na důkladné, nikoliv již povrchní a až moc konkrétní, rozebrání architektury frameworku PHP Jet.

Tak pojďme na to :-)

Dependency Injection = návrhový vzor!

… a ne svatá kráva.

„A terazky mi povedztě, Kefalín, čo vy si predstavujetě pod takým slovom "návrhový vzor“?"

Návrhový vzor je obecný postup návrhu SW architektury, který napovídá jak je vhodné řešit určitou velice širokou škálu situací majících ale podobný elementární charakter a který říká proč je to dobré takto řešit.

Ta škála problémů je de facto nekonečná a není určen žádný pevně daný postup jak má být návrhový vzor implementován v praxi. Tedy v žádném případě to neznamená, že ve frameworku musí být nějaký obecně použitelný ultra-mega kontejner a už vůbec to neznamená, že kontejner má být konfigurovatelný. To je chybné pochopení samotného smyslu existence návrhových vzorů. Návrhové vzory nám pouze napovídají jak by co asi tak šlo řešit a proč, ale v žádném případě neříkají že to tak musí být a že to musí být implementováno právě tak a tak.

To pak návrh SW přestává být technickým oborem a stává se náboženstvím, či lépe řečeno fundamentalisticky chápaným náboženstvím. A to je hodně špatně.

Nechtěl bych tady řešit náboženství. To je velice osobní věc, ale za sebe klidně prozradím, že jsem věřící člověk, který však není „ofiko“ v žádné církvi a není pro mne důležité přesné znění 5. věta na 185. stránce nějaké knihy, ale základní pravidla jako je „nekrást“, „nevraždit“ a tak dále. Nevěřím ničemu jen „protože proto“, ale protože to má nějaký elementární smysl. A toto mé nahlížení se odráží i v profesním životě. Ale pryč od víry. To je fakt moc osobní a soukromá záležitost.

Jeden příměr však použiji a to příměr neutrální. Pamatujete na ten díl Červeného trpaslíka, díl Čekání na boha? To je naprosto super alegorie … Celá civilizace tam válčila kvůli naprostému nesmyslu a ve skutečnosti vše bylo jinak.

Ne, návrh SW fakt není o červených a modrých čepičkách jako v Červeném trpaslíku. Takové uvažování vede až k nepěkným společenským jevům. A diskuze „o čepičkách“ nikam nevedou – je to bludný kruh a ne posun.

Tedy návrhový vzor Dependenci Injecton říká, že často může být velice vhodné a dobré závislost jedné komponenty na druhé vyřešit až za běhu aplikace. A někdy to není jen dobré, ale dokonce naprosto nutné. 

Dále návrhový vzor říká, že v takové situaci je fajn použít nějaký kontejner. Neříká jak má být kontejner implementován. Neříká dokonce ani že má být nějak konfigurovatelný. A neříká ani že to má být jedna mega ultra věc pro celou aplikaci. 

Nic takové neříká žádný návrhový vzor. To by totiž popřelo samotný smysl této problematiky. Ale říkají to různé články a texty ukazující praktické použití tohoto vzoru. Ale ty ukazují jen a pouze jedno z mnoha možných použití.

Dále platí, že žádný návrhový vzor není dobrý nebo špatný. Návrhový vzor je buď dobře chápán a správně použit, nebo nepochopen. Ale každý návrhový vzor má své místo.

No a jakožto IT kmet si dovolím takovou kmetskou poznámku. Návrhové vzory se nedají, alespoň podle mého názoru a dosavadního poznání, opravdu správně naučit teorií, ale pouze reálnou praxí. A to nejlépe tou praxí, kde si našinec namele čumák aby si to pamatoval. 

Prvotní článek jsem teda odflákl …

To jako že fakt … PHP Jet návrhový vzor DI používá hodně a na mnoha místech a tuto pasáž jsem neprobral důkladně.

Hlavně jsem chtěl zdůraznit, že vynucené a nesprávné, nebo lépe řečeno nevhodné, použití návrhového vzoru je … jak to napsat česky … Třeba antivzor? V angličtině tomu kolegové říkají anti-pattern.

Tak to zkusím ještě jednou. 

Čemu chce Dependency Injection předejít? K čemu je to dobré?

V minulém článku jsem použil příklad, kdy jsem zdůraznit opravdu technickou nutnost. A tak to je. Jsou situace, kdy prostě musíte použít DI princip z technických důvodů a mít něco čemu se říká kontejner.

Ale spousta lidí argumentuje tím, že DI má zlepšit kvalitu kódu a udržitelnost aplikace. A jasně, na tom se shodneme. Udržitelné aplikace chceme všichni. Ale k tomu vede milion+1 cest.

Teď si ukažme, jak aplikace nedělat. Bez ohledu na framework a obecně jakoukoliv technologii. Tohle je špatně (ale předesílám, že je to řešitelné – tedy ne zas tak úplně špatně):

class Article {
    protected Db $db;

    public function __construct()
    {
        $this->db = GetDbConnection();
    }
}

Otázka je, proč je to špatně. Musí být nějaký objektivní a ne subjektivní důvod aby bylo možné to říct. A ten existuje: Protože jednoho krásného dne se může stát, že pro tu tabulku s články budu potřebovat jiné databázové spojení než pro jiné entity aplikace.

Může se stát (a v praxi dříve či později stane), že na základě vnějších okolností budu potřebovat dynamicky za běhu aplikace určovat s jakou databází má ta třída pracovat. Napadá mě třeba nějaký režim náhledu obsahu a tak dále. A nebo pro účely testování – to je možná nejpravděpodobnější scénář. A určitě jsou další situace a možné scénáře a nikdy nikoho nemohou napadnou všechny. Nikdo nemá křišťálovou kouli – pouze maximálně zkušenostmi vybudovanou intuici a ani ta není absolutně přesná.

Ale je takřka jisté, že články budu stále ukládat do nějaké databáze, protože to je smyslem této třídy samotné. A otázka je do jaké databáze, s jakou konfigurací, a tak dále.

Jak to vyřešit?

Řešit to přes parametry a dospět postupně v myšlenkách až k nějakým obecným kontejnerům, konfiguraci a tak dále? Kdepak, nic takového … Teda ano, pokud je to vhodná cesta, ale nic takového neplatí univerzálně.

Mimochodem … Když udělám tu fatální chybu v samotném návrhu SW a závislosti jen a pouze „pevně zadrátuji“ jinam – třeba právě do separované konfigurace, tak v ten moment vlastně vůbec nemám Dependency Injection, ale pouze jsem problém přesunul z místa A na místo B.

Zopakujme si, že Dependency Injection má řešit závislosti za běhu aplikace. A také si zopakujme, že kontejner není nic co by bylo nějak přesně předepsáno. Kontejner je pouze myšlenkový konstrukt a abstrakce.

Tedy situaci s článkem lze vyřešit třeba takto:

class Article {

    protected static ?Db $db = null;

    public function __construct()
    {
    }

    public static function getDb() : Db
    {
        if(!static::$db) {
            static::$db = getSomeDefaultDb();
        }

        return static::$db;
    }

    public static function setDb( Db $db ) : void
    {
        static::$db = $db;
    }

    public function save() : void
    {
        static::getDb()->save( $this );
    }

}

A je to … Jaká bude použita databáze mohu zvolit za běhu, když to budu potřebovat. V tomto případě za nějaké určité specifické situace. A ano, i při testech. Mohu si s třídou dělat co potřebuji.

Zároveň není nutné, aby se ty části aplikace které to vůbec nezajímá staraly o to jaké je použité spojení. To není jejich věc – jejich kompetence. Však každá komponenta SW má odpovídat za svou kompetenci a neřešit co jí nenáleží.

A fór je v tom, že takto se dá aplikace i refaktorovat a to třeba i z původní podoby, kterou jsem uváděl jako příklad špatné cesty. Stačí, když je třída zapouzdřená a má jasně specifikované rozhraní. Třída je pak flexibilnější jak potřebuji. 

Mimochodem … 

Správně navrženou třídu mohu refaktorovat tak, že nebude v budoucnu data ukládat do databáze, ale posílat je poštovním holubem (jasně že si dělám legraci). Jde hlavně o to, aby bylo jasně dané rozhraní a nic z venčí se „nevrtalo ve střevech“ třídy – viz používání public vlastností třídy a můj kategorický nesouhlas s takovým přístupem. Jde o to, aby mezi komponentami a třídami byly jasně dané hranice a na těch hranicích jasná rozhraní. Ne aby se vše prolínalo a zamotávalo do sebe. Právě tam je hlavní kámen úrazu projektů.

To je ten důvod, proč stále mluvím o zapouzdření atd. Protože když dodržím tento princip a třída či jiná komponenta má jasně dané rozhraní (pochopitelně promyšlené rozhraní), tak to do budoucna mohu refaktorovat jak potřebuji a nemusím se bát rozbití celého projektu – to je to hlavní.

No a pokud vás teď napadlo, že žádné rozhraní třídy nemůže být promyšlené tak, aby obsáhlo vše potřebné, tak odpovím: Zcela jistě je to tak. Ale v OOP máme třeba dědičnost, že …  

Zpět k uvedenému příkladu: A kde je sakra ten kontejner? V tomto případě je třída sama sobě kontejnerem, do kterého ze může vložit závislost, protože zrovna v této situaci to může být dobré řešení.

Mám vložení závislosti a hlavně mám možnost závislost určit za běhu? Mám. Fajn. Mám tedy pravé a nefalšované DI. A nepotřebuji na to žádný ultra-mega kontejner na vše. A že je de facto třída sama sobě kontejnerem? Fajn, zde v tomto případě to může být ideální řešení. Přímočaré, jednoduché, srozumitelné. V jiné situaci to může být jinak a takové řešení nestačí. V jiných situací opravdu budu potřebovat samostatné kontejnery – viz minulý článek. Ale ani jedno a ani druhé neplatí univerzálně.

Fór není v tom řídit se slepě tím co někdo někde někdy napsal. Fór je v tom vědět co se vymstí a co ne a jak správně aplikaci navrhnout a proč. 

Správné je myslet při návrhu aplikace na to, že v budoucnu se bude refaktorovat a nic nesmí bránit refaktorování jednotlivých komponent tak, aby pří refaktoringu komponenty nebylo nutné sahat na komponenty jiné, které s danou komponentou nesouvisí. 

Prostě a jednoduše: Aplikace kde mohu s klidným svědomím třeba zásadně předělat jednu třídu / komponentu a nezničím tím projekt je dobrá aplikace. A koncept DI je jeden s konceptů, které vedou k tomuto cíli.

DI v Jet – ještě jednou a lépe

Minule jsem vám ukázal pouze příklady doslovných kontejnerů – tříd přímo tuto roli představujívích. A na některé jsem ke všemu zapomněl.

Ovšem ve skutečnosti je to tak, že celý Jet je DI. Nic není pevně zadrátované. Absolutně nic! (teda pokud jsem nic nepřehlédl … že … ). Do všeho možného se dají a někdy dokonce i musí vkládat závislosti.

A teď nemyslím jen závislosti třída na třídě, ale i závislosti na konfiguraci.

Příklad? Tak jistě jste si všimli, že používám Bootstrap. Ten není pro Jet mandatorní. Ten používám, protože mi na řadu úloh vyhovuje. Např. na administrace je super (pro mě, ne pro každého).

Ale co když žádný Bootstrap nechcete? Nebo co když jej chcete nechat v administraci, ale na webu, e-shopu či čemkoliv použít úplně něco jiného, ale zároveň chcete mít k dispozici stále systém formulářů a generování UI v PHP a to zcela bez modifikace tříd a zejména jejich rozhraní? 

Maličkost! Ono je totiž nutné vložit konfigurační a závislost do těchto subsystémů. Je nutné určit kde jsou view pro formuláře a pro pro UI. Bez toho to ani fungovat nebude. Proto když Jet zjistí v jaké bázi se uživatel pohybuje, tak volá inicializátor. A tam najdete třeba toto:

namespace JetApplication;

class Application_Admin
{
    public static function init( MVC_Router $router ): void
    {
        Logger::setLogger( new Logger_Admin() );
        Auth::setController( new Auth_Controller_Admin() );

        SysConf_Jet_UI::setViewsDir( $router->getBase()->getViewsPath() . 'ui/' );
        SysConf_Jet_Form::setDefaultViewsDir( $router->getBase()->getViewsPath() . 'form/' );
        SysConf_Jet_ErrorPages::setErrorPagesDir( $router->getBase()->getPagesDataPath( $router->getLocale() ) );
    }

}

Na co koukáte? Koukáte na vložení závislostí za běhu. Tohle je Dependency Injection v praxi.

A jak vidíte zde:

SysConf_Jet_Form::setDefaultViewsDir( $router->getBase()->getViewsPath() . 'form/' );

Tak se dynamicky vkládá závislost na konfiguraci. A nic vám nebrání udělat si vlastní view pro formulářové prvky s jakoukoliv technologií, ty dát do jakéhokoliv přístupného adresáře a ten nastavit. Aplikace dál vesele poběží, jen HTML, CSS a JS bude jiné. Ostatně ukážeme si to v dnešním videu.

Tedy pokud chcete mít pro nějakou část projektu úplně jiné view pro formuláře, tak vám absolutně nic nebrání si je vytvořit a použít. PHP Jet počítá s tím, že to budete dělat – vlastně očekává se to, je to cíl a samotný smysl tohoto frameworku. 

A to byl jen jeden příklad. Tohle neplatí jen pro daný příklad, ale pro vše.

Jet prostě počítá s tím, že si cokoliv nastavíte, upravíte či vyměníte a to i za běhu.

PHP Jet jde tak daleko, že nepoužívá konstanty ani pro základní nastavení. V samotné architektuře systému je zahrnuto, že vy máte vše pod plnou kontrolou a vládou.

Pokud vás zajímá víc, tak si to projděte. Dejte si trochu času s přečtením dokumentace, kde je vše popsáno. Nebo se ptejte v diskuzi na GitHub, kde vám co nejrychleji odpovím já (a mimochodem tak podpoříte projekt). Je nutné pouze tuto filozofii pochopit. Tedy neptat se „Jak udělat …?“ ale pokládat otázku „Proč?“. Pak zjistíte, že je to celé vlastně směšně primitivní a také že to máte opravdu plně pod kontrolou.

Ale pojďme si ukázat další PHP Jet hrátky:

Co kdybych se z nějakého důvodu rozhodl, že logy logy administrace chci vždy ukládat do samostatné MariaDB databáze, ještě ke všemu s jiným typem tabulek, bez ohledu na zbytek systému, který může běžet na něčem jiném – třeba na úplně jiném typu databáze s úplně jiným nastavením? 

(Upřesnění: Za předpokladu že použijete stávající třídu a neuděláte si vlastní implementaci …) 

Neřešme zda je to reálná praxe. Možná ano, možná ne – to nikdo nikdy nemůže předem vědět. Důležité je, aby to bylo jednoduše možné:

$custom_backend_config = new DataModel_Backend_MySQL_Config([
    'connection_read' => 'logs_connection',
    'connection_write' => 'logs_connection',
    'engine' => 'MyISAM',
    'default_charset' => 'utf8',
    'collate' => 'utf8_general_ci'
]);
$custom_backend = new DataModel_Backend_MySQL( $custom_backend_config );

DataModel_Backend::setCustomBackend(
    Logger_Admin_Event::class,
    $custom_backend
);

Tohle bych dal na vhodné místo inicializace aplikace (na jaké místo je opět na posouzení situace) a je to.

Stejně tak je možné vytvářet ad-hoc databázová spojení a tak dále.

Prostě nic není pevně zadrátované. Celý PHP Jet je souhrn navzájem spolupracujících komponent. A koncept DI v tom pochopitelně hraje svou roli.

Ale pojďme dále kuchat Jet a ukažme si další ještě příklady.

Co třeba zobrazení chybových hlášení PHP? Nelíbí se vám? Jasně, není a nebude v něm žádný výpis zdrojáku a hodnoty parametrů. Proč? Protože když to někdo omylem nechá zapnuté na produkci, tak to může znamenat bezpečnostní problém. A také to výchozí zobrazení není nějak extra hezké – nejsem, nebyl jsem a nikdy nebudu designer a grafik. Ale víte co? Nic vám nebrání udělat si vlastní Error Handler pro zobrazování. I to co operuje s chybami jsou moduly. Však se mrkněte do inicializace ~/application/Init/ErrorHandler.php. 

A nebo chcete při fatální chybě (napři výpadku DB spojení) raději rovnou poslat SMS? Tak si jednoduše doplňte vlastní ErrorHandler – je to snadné. Od toho to je. Lze udělat vyvinout vlastní error handler, který na základě určité chyby a jejího přesného charakteru provede cokoliv. Například zavolá webservisu, která pošle SMS … Fantazii se meze nekladou.

A co takhle třeba nový typ formulářového pole? Ten typ formulářového pole by mohl být nejen nějaký input s validací, ale třeba něco komplexnějšího, co může obsahovat i dialogové okno a tak dále. Maličkost. I typy polí je možné vkládat, ale i měnit ty stávající ( mimo jiné také vkládáním závislostí ) a tak dále.

PHP Jet není CMS, kde by bylo něco do nějaké (větší či menší) míry určeno. Jet je absolutně obecný framework. PHP Jet sám o sobě je pouze to co je v adresáři ~/library/Jet. 

Tento framework pouze sebou rovnou nese v distribučním balíčku ukázkovou aplikaci, která slouží pro experimenty a také zahrnuje to co je typické a co je pro běžné projekty potřeba. 

Nebo snad děláte online aplikace třeba bez administrace? I jiné frameworky mají například přihlášení. Ale jsou to třeba balíčky a někdy je to i tak zvané „do-do“. A třeba Jet Studio? No co je takový artisan v Laravelu, či jiné nástroje jinde?

Tedy ne .. Nic není pevně dáno. Pouze máte řadu typických věcí naservírováno na podnose, ale dál si s tím může dělat kdokoliv cokoliv.

A dnes bude i video

V dnešním video si lépe představíme architekturu a uspořádání PHP Jet a ukážeme si rovnou v praxi pár „triků“ (žádné triky to ve skutečnosti nebudou). Vytvoříme si vlastní error handler pro zasílání SMS při vážné chybě, ukážeme si autentizační a autorizační kontrolery a lehce redesignujeme formuláře.

Závěr

Tedy abych to shrnul: Jet DI používá a to masivně. Ten koncept se táhne celou architekturou jako červená niť. V PHP Jet není (alespoň doufám) nic pevně zadrátované.

Ani já a ani Jet sám o sobě vám nezkoušíme tvrdit, že je dobré používat parametry, že je nějaké „pravidlo číslo 1“, že je dobré používat a priory nějaké kontejnery či obecně princip a tak dále. 

Ne. Je správné dobré principy používat na správném místě. Používat něco někde, kde to nemá opodstatnění, nebo dobrý nástroj používat špatným způsobem je to co dělá základ špatné aplikace.

A dobrý programátor je ten, který toto chápe. A to zcela bez ohledu na framework, jazyk, prostředí, na cokoliv dalšího. Ano, důležité jsou teoretické znalosti, ale zcela zásadní jsou praktické zkušenosti a vědomí, že to a to také může znamenat, že budete žehlit p.....r u klienta, nebo šéfa.

Klíčem k „čistému kódu“ a udržitelným aplikacím není žádná technologie či framework (to platí samozřejmě i pro PHP Jet – ten také nic sám o sobě nespasí), ani žádný návrhový vzor sám o sobě. Klíčem jsou pouze schopnosti a zkušenosti vývojáře. A dobrý vývojář neřeší dogmaticky 65. větu na 165. stránce knihy XY. Takové diskuze náleží teologům, či právníkům (asi – nejsem ani právník a ani teolog). Protože možností v našem oboru máme takřka nekonečno a fundamentalismus a dogmatismus nás pouze brzdí a doslova škodí.

A ano, já si mohu dovolit za délku a charakter a různorodost mé praxe toto vše tvrdit a taková „moudra“ psát. Jaksi jsem si za desítky let úspěchů, ale i pořádných a někdy fatálních průšvihů odpracoval jistou možnost mé reálné zkušenosti předávat dál – těm co o ně stojí.

A ještě si to dalšími úspěchy a hlavně průšvihy odpracuji a určitě narazím na další moudra – cesta poznání je nekonečná. Ale mé tovaryšské časy už dávno odvála doba, stejně jako velkou část mých vlasů :-D To co píšu není teorie, kterou jsem si někde přečetl. Za tím co říkám a píšu jsou někdy pořádně propocené vlasy, protože se něco nepovedlo, nebo naopak radost z úspěchu. Za vším co píšu a říkám je nějaký konkrétní příběh.

Tedy co že vám to vlastně tvrdím? Jádro myšlenky je toto: Nic neplatí univerzálně. Žádná cesta není ta jediná správná. Jediné co je opravdu špatně je uzavírání si cest. A jediné co je opravdu dobře je vždy si ponechat cestu otevřenou. Otevřená cesta v našem oboru = bez bolesti refaktorovatelné komponenty. To je pravidlo číslo 1. Jaká cesta k tomuto cíli je správná? To už je jen a pouze na vás, protože to nikdo za vás nemůže vědět. Každý projekt je unikátní, stejně jako je unikátní každý z nás.

 A PHP Jet je navržen tak, aby toto maximálně respektoval. Tento framework cesty nabízí, ale zároveň přímo vybízí k tvoření vlastních cest a řešení. S tím, že by vám měl primárně pomoct s otravnou a objektivně se stále opakující rutinou. Ale PHP Jet vám nic pevně nevnucuje, nediktuje a netvrdí, že postup XY je ta správná cesta. 

Však samotný balíček s ukázkovou aplikací je souhrn hned několika aplikací a každá z nich je vytvořena trochu jinak i když jsou všechny postaveny na stejném frameworku.

Díky za přečtení a mějte se fajn! 

Příště se mrkneme na ORM – článek mám již rozepsaný.

Sdílet