Dependency Injection v Jet

Zdá se vám, že framework Jet nepoužívá paradigma Dependency Injection? Ale kdepak! Používá. Bez toho nelze velké projekty realizovat. Ovšem z Dependency Injection (DI) jak jej asi znáte používá tu část, která objektivně usnadňuje život a naopak eliminuje tu část, která vám život může komplikovat. Pojďme si to vysvětlit.

K čemu je DI vlastně dobré?

Hlavní výhodou DI je, že uživatelské třídy zcela izoluje od vytváření a inicializace jejich služeb. To je hlavní a nejdůležitější přínost. Ale bude lepší si to vysvětlit na konkrétním případě.

Dejme tomu, že máme v systému logování. Logování musí zabezpečit nějaká třída. A logování je služba, která tato třída poskytuje - třída je poskytovatelem služby.

Dále máme uživatele služby. Tedy v našem případě cokoliv co potřebuje logovat do nějakého logu. Buďme opět konkrétní a řekněme že to může být například nějaký kontroler (ale může to samozřejmě být cokoliv jiného). Ve špatně navržené aplikace, která zcela ignoruje DI, by onen kontroler logoval například takto:

class Controller extends Controller_Abstract {
    
    public function 
someAction() : void
    
{
        
//... ... ...
        
$logger = new Logger('~/path/to/some/log/dir');
        
$logger->logEvent$event$event_data );
        
//... ... ...
    
}
}

To je naprosto špatně a takto se programovat nemá. Proč? Z čistě objektivních důvodů: Představte si, že takto vytváříte instanci služby třeba na stovkách míst. A pak přijde den, kdy budete potřebovat logování změnit:

  • Třeba vyměnit onu třídu za úplně jinou
  • Nebo třídu přenastavit, změnit parametry služby
  • Nebo nechat nějakou vyšší autoritu aby rozhodovala o tom jak bude aplikace logovat.
Nic z toho se vám snadno nepodaří. Čeká vás procházení celého projektu, úprava spousty míst kódu (klidně stovek) a s tím samozřejmě spojené riziko chyb a problémů. Pro větší projekt noční můra.

Nehledě na prostý fakt, že kontroler v našem příkladu dělá prostě to co nemá. Stará se o inicializaci služby a to není jeho kompetence a zodpovědnost. To je opět prohřešek proti pravidlům pro vytváření udržitelných projektů.

Ale řekl bych, že hlavním strašákem je ta představa že třeba za dva roky provozu projektu musíte procházet a upravovat stovky míst v projektu to třeba i jen kvůli malé dílčí změně. To je čitě objektivní velký problém a tomu se chceme vyhnout.

A právě to je hlavní benefit mechanismů DI. Při jeho dodržování je zabezpečeno že inicializace služby se nebude starat uživatel služby.

DI je tedy věc nutná.

DI jak jej možná znáte ... a jak jej v Jet nenajdete

Faktem je, že existuje řada metod jak DI používat - ne pouze jeden. Hlavním cílem je neinicializovat služby na mnoha místech a na místech, která k tomu nejsou určená. Ale cest k tomuto cíli vede víc.

Z jiných frameworků znáte pravděpodobně něco takového:

class Controller extends Controller_Abstract {
    
    public function 
someActionLogger_Interface $logger ) : void
    
{
        
//... ... ...
        
$logger->logEvent$event$event_data );
        
//... ... ...
    
}
}

Tedy mechanismus kdy jsou instance služeb vkládány například jako parametry akcí, či parametry konstruktorů. V těchto frameworcích existují různé a často velice složité systémy a infrastruktura pro DI. Dále různé mega kontejnery a tak dále.

Ano, funguje to. Ve své podstatě je to správně. Ale právě ta složitost může být někdy velice kontraproduktivní. Už to že v takové situaci často musíte sami řešit předávání závislostí dál, můžete mít případně problém s cyklickými závislostmi a tak dále. Nehledě na ten prostý fakt, že s ohledem na složitost těchto frameworků to celé znamená velké množství kódu a tedy i technickou režii.

A framework Jet má jako předsevzetí být co nejjednodušší a právě díky tomu efektivnější. A proto se framework PHP Jet vydal jinou cestou.

Garant služeb

Jet zavádí termín garant služeb. Co to vlastně je? Začněme rovnou konkrétním příkladem logování v Jet (i když to celé se pochopitelně netýká pouze logování):

namespace JetApplicationModule\Content\Articles\Admin;

use 
Jet\Logger;

use 
Jet\MVC_Controller_Default;

class 
Controller_Main extends MVC_Controller_Default
{
    
    public function 
add_Action(): void
    
{
        
//... ... ...
        
        
Logger::success(
            
event'article_created',
            
context_object_data$article
        
);
        
        
//... ... ...
    
}
}

Ne, nelekejte se! Není to žádný service locator (například). A vše dává perfektní smysl. Čtěte dál ;-)

Obecně platí, že jakákoliv služba musí mít nějaké definované rozhraní (ať je to přímo rozhraní které implementuje, nebo abstraktní třída). Prostě a jednoduše uživatelé služby musí znát jaké má služba rozhraní a jak ji použít. To platí i zde. Logger v PHP Jet má rozhraní Jet\Logger_Interface (které je v tomto případě opravdu primitivní). V tomto ohledu se v PHP Jet vůbec nic nemění a pevně platí, že služby mají své dané rozhraní.

V jiných frameworcích však funguje to, čemu říkáme syndrom jedné velké hromady. O služby se stará již zmíněný komplexní subsystém. Služby jsou tak na jedné hromadě.

PHP Jet to dělá tak, že krom rozhraní služby systém definuje ještě garanta služby. Garant služeb není pouze statická fasáda, která je mostem do onoho velkého kontejneru služeb (jak to má jeden konkurenční framework). Ano, pohodlí a přímočarost použití fasády zde zůstává stejná. Ale garant služby v Jet není pouhá fasáda, ale zároveň i kontejner do kterého je vložen konkrétní poskytovatel služby.

Tedy pokud se budeme dále držet našeho příkladu s logováním, tak vězte, že ona fasáda Jet\Logger sama žádné logování neprovádí. Je to pouze garant služby - kontejner mající rozhraní reflektující danou službu, ale do toho kontejneru musel být vložen konkrétní poskytovatel služby (zde tedy logger, který již ví jak má danou operaci provést a službu poskytnout). Například takto:

Logger::setLogger( new Logger_Admin() );

Tedy při inicializaci aplikace na základě okolností (to je velice důležité) mohla nějaká vyšší autorita (zde inicializátor báze, ale může to být cokoliv jiného) rozhodla o tom jaká konkrétní služba bude použita pro logování.

Zde je tedy možné mít jiné logování pro administraci, jiné pro REST API, jiné pro web a tak dále. Zbytek aplikace to však nezajímá a prostě loguje - přesně tak jak v rámci DI pravidel má. Zároveň nenastává žádné peklo závislostí, žádné problémy. A použití všeho je naprosto jednoduché a přímočaré.

Méně kódu, méně složitostí, méně problémů. A zároveň i daleko větší flexibilita a lepší udržitelnost projektů.

Je důležité zmínit, že krom DI mechanismu realizovaného pomocí garantů služeb má Jet i systém továren. Tento systém továren je používán zejména pro instancování systémových entit.

Přehled poskytovatelů služeb v Jet

Pro názornost jsme si ukázali logování jakožto naprosto jednoduchou službu. Ovšem v Jet je samozřejmě daleko více služeb a jejich poskytovatelů. Zde je jejich seznam.

A vám pochopitelně vůbec nic nebrání vytvářet si vlastní služby s využitím obdobného mechanismu a filozofie. A nepotřebujete k tomu žádný komplexní framework ;-)

Služba / subsystém Garant služby Metoda pro vložení poskytovatele
Autentizace a autorizace Jet\Auth Jet\Auth::setController(
Jet\Auth_Controller_Interface $controller
)
Poskytovatelem služby je tak zvaný Auth Controller implementující Jet\Auth_Controller_Interface. Úkolem tohoto kontroleru je řídit logiku práce s uživateli, přesněji logiku autentizace a autorizace.

Neexistuje žádný výchozí Auth Controller. Stejně tak součástí frameworku není žádná implementace Auth Controller. Tyto kontrolery jsou vždy součástí aplikačního prostoru - aplikace. Je tedy plně na vývojáři jak logiku implementuje a nic nebrání jakýmkoliv úpravám. Součástí ukázkové aplikace jsou tři kontrolery.

Při použití Jet MVC se instance kontroleru vkládá do garanta služby v rámci inicializátoru báze.
Autoloader Jet\Autoloader Jet\Autoloader::register(
Jet\Autoloader_Loader $loader
): void
Systém pro automatické nahrávání tříd je ve skutečnosti tvořen jednotlivými moduly, které je do něj nutné vložit při inicializaci aplikace. Těchto modulů samozřejmě může být (a v praxi je) víc.

Ve výchozí konfiguraci inicializaci naleznete ve skriptu ~/application/Init/Autoloader.php
Keš autoloaderu Jet\Autoloader_Cache Jet\Autoloader_Cache::init(
Jet\Autoloader_Cache_Backend $backend
)
Backend keše systému automatického nahrávání tříd je nutné vložit.

Ve výchozí konfiguraci inicializaci naleznete ve skriptu ~/application/Init/Cache/Autoloader.php
Správce aplikačních modulů Jet\Application_Modules Jet\Application_Modules::setHandler(
Jet\Application_Modules_Handler $handler
) : void
Správce aplikačních modulů není nutné vkládat. Systém má výchozí implementace, která se použije pokud při inicializaci nebyl vložen jiný správce.

Tedy je možné vložit alternativního správce modulů, ale v tomto případě není nutné vkládání provádět.

Správa aplikačních modulů používá i systém továren.
Logger Jet\Logger Jet\Logger::setLogger(
Jet\Logger_Interface $logger
): void
Systém zabezpečující obecné logování. Implementace loggeru je vždy součástí aplikace a ne frameworku. Ukázková aplikace obsahuje předpřipravené logery.

Při použití Jet MVC se instance loggeru vkládá do garanta služby v rámci inicializátoru báze.
Posílání e-mailů Jet\Mailing Jet\Mailing::setBackend(
Mailing_Backend_Abstract $backend
): void
Systém posílání e-mailů potřebuje backend, který zajistí samotné odeslání e-mailu. Výchozí backend zajistí odeslání před standardní prostředky samotného PHP a tento výchozí backend není nutné vkládat. Výchozí backend je použit automaticky pokud jiný backend není vložen do garanta služby.

I zde platí, že je možné vložit vlastní backend, ale pokud vám stačí backend výchozí, pak vkládání není nutné provádět.
Hlavní router MVC Jet\MVC Jet\MVC::setRouter(
MVC_Router_Interface $router
): void
Taktéž hlavní router Jet MVC není nutné vkládat, ale má svou výchozí implementaci, ale zároveň je možné vytvořit si vlastní implementaci routeru a tu vložit. Zajímavostí je, že router Jet MVC využívá i systém továren.
Keš MVC Jet\MVC_Cache Jet\MVC_Cache::init(
MVC_Cache_Backend $backend
): void
Keš Jet MVC je nutné inicializovat a vložit backend do kontejneru / poskytovatele služeb na místě k tomu určeném. Tedy ve skriptu:

~/application/Init/Cache/MVC.php

K dispozici jsou dva připravené backendy využívající pro ukládání buď soubory, nebo Redis. A samozřejmě nic nebrání implementovat libovolný další backend a ten vložit do systému.
REST API server Jet\RESTServer Jet\RESTServer::setBackend(
RESTServer_Backend $backend
) : void
Backend REST API serveru má na starosti konkrétní implementaci čtený HTTP požadavků naopak tvorbu konkrétních HTTP odpovědí. Pokud tedy chcete změnit výchozí chování REST API serveru, tak to samozřejmě není problém.

I zde platí, že existuje výchozí implementace, která se inicializuje automaticky a ve výchozí situaci tedy není nutné backend vkládat.
Překladač - ukládání slovníků Jet\Translator Jet\Translator::setBackend(
Translator_Backend $backend
) : void
Úkolem backendu překladače je načítat a ukládat slovníky překladů.

I v tomto případě existuje výchozí implementace, která operuje se soubory a tento výchozí backend je použit automaticky pokud není vložen jiný.
Předchozí kapitola
Atributy - Jet\Attributes
Další kapitola
Továrny