Cum folosesc corect un DI în afara unui framework?

Vezi ca in anumite frameworkuri mai e un pas, anume cel de compilare/optimizare, in care sursa ta se transforma in altceva la prima accesare, si acolo se intampla legarea magica. Nu e vorba de compilare binara ci de transformare in alt php (in directorul de cache al frameworkului)

În conceptul ăsta banal există un gap măricel între „uite cum instanțiezi două clase” vs „folosește asta în aplicația ta care se întinde pe zeci/sute de clase”

Păi dacă vreau doar DI fără framework nu se poate? :smiley:

NU este vorba de WP sau despre vreun framework aici, vorbim doar despre concept.


Păi tocmai ai descris problema mea :smiley:

Uite un exemplu simplificat.

class DB {
    // clasa asta are parametri, zice în docs cum se folosește, e ok
    // https://php-di.org/doc/php-definitions.html#factories
    public function __construct(private string $host, private string $user, private string $pass, private string $db) {}
    
    public function areCuloare(string $color): array
    {
        return mysqli_query( '', "SELECT * ... etc");
    }
}

class Logan {
    public function __construct( $dbRow, private Logger $logger) {}
}

class ParcAuto {
    public function __construct(private DB $db) {}

    public function getAuto(string $color)
    {
        // metoda asta întoarce un array de instanțe NOI
        $masini = $this->db->areCuloare($color);
        return array_map(fn($dbRow)=> 
          new Logan($dbRow, new Logger()), 
          $masini
        );
    }
}

$container = new DI\Container(/*etc*/);

var_dump($container->get(ParcAuto::class)->getAuto('roșu'));

După cum observi, metoda getAuto returnează un array de instanțe noi. Iar aici e problema mea: cum le instanțiez pe astea?

Ok, mut partea aia într-un factory, injectez factory în constructorul ParcAuto iar metoda rămâne cu:

return array_map(fn($dbRow)=> 
  $this->loganFactory->get($dbRow), 
  $masini
);

Dar apoi nu fac decât să mut aceeași problemă în factory.

Bingo! De aia ii si zice OOP si nu class-oriented programming.

1 Like

pai normal, doar nu vrei sa-ti scaneze tot codul la fiecare request. ca nu le-ar mai folosi nimeni. si asa timpul de bootare al unui framework e imens.

dar asta deja face parte din optimizare, nu cred ca tine neaparat de di.

class ParcAuto {
    public function __construct(private DB $db, private Logger $logger) {}

    public function getAuto(string $color, string $class) //urat.
    {
        // metoda asta întoarce un array de instanțe NOI -> pai nu trebuie sa instantieze obiecte noi. tre sa le primeasca ca dependinte.
        $masini = $this->db->areCuloare($color);
        return array_map(fn($dbRow)=> 
          new $class($dbRow, $this->logger), 
          $masini
        );
    }
}

new ParcAuto($db, new Logger, Logan::class)....

sau


public function getAuto(string $color, CatInstance $car){
 ....
    return array_map(fn($dbRow)=> 
          $car->setRow($dbRow)
                ->setLogger($this->logger),
          $masini
        );
}

getAuto('rosu', new Logan());

Păi tocmai de aia am pus exemplul așa, clasa ParcAuto nu are nevoie de Logger. Apoi, nu fac decât să mut numele clasei dintr-un loc în altul. Mi se pare că cresc complexitatea fără vreun beneficiu.

Al doilea exemplu nu face decât să refolosească aceeași instanță…

te-ai prins. te chinui mult pt… habar n-am ce :smiley:

scoate loggerul din parc auto si-l adaugi in Logan::__construct. atata vreme cat poti aplea $parkAuto->getAuto(‘rosu’, new Logan(), new Logger()).

Ok, hai să presupunem că Logger mai are o dependență, Foo. Iar Foo mai are două depndențe. Cum procedez?

$parkAuto->getAuto(‘rosu’, new Logan(), new Logger( new Foo( new Bar, new FooBaz ) )

Poți spune fără să te bufnească râsul că chestia asta are sens când am un DI container în aplicație? :smiley:

Modul asta modern de a instantia obiecte direct din clase ascunde de fapt ideea de OOP si mai mult. Tu nu vrei sa instantiezi Loggerul atunci cand apelezi getAuto() ci trebuie sa fie instantiat undeva pe parcursul cererii curente, iar instanta trimisa tuturor obiectelor care au nevoie sa logheze ceva.

Side-comment: nu definesti metoda “areCuloare()” in clasa DB pentru ca

OBIECTUL INSTANTIAT DIN CLASA DB NU ARE CULOARE!

poate sa te bufneasca rasul pana-ti iese pe nas, ala-i dependency injection. de asta te folosesti de un framework care se ocupa de toate instantele.

sau, $parkAuto->getAuto(‘rosu’, new Logan(), $diContainer->get(‘logger’))

dar daca o sa vrei sa ai un alt loger pt bmw, cam naspa.

sa-ti arat cum arata cache-ul generat de symfony?

omu vrea sa faca bmw din caruta si tu observi ca n-are decat doi cai.

Momentan am rezolvat problema în următorul fel:

  1. am făcut un factory
  2. în factory accesez containerul direct și instanțiez chestii de acolo

Rezultat:

  1. Clasele sunt decuplate, prin urmare
  2. Pot testa tot codul mai puțin factories

Yay me, cred?

Păi man, nu de aia folosești un container? Pentru a nu face asta manual?


Omu’ vrea să învețe, dar ok.

Și tu și Alex v-ați blocat pe ideea că folosesc WP, cu toate că am specificat de câteva ori că nu despre WP este vorba.

Nu, vreau sa subliniez ca trebuie gandit din alta perspectiva. Asta a fost primul lucru cu care m-am confruntat cand am invatat Java (acu 15+ ani). E ca scosul de masea, doare dar trece.

cred ca-i un exercitiu bun, chiar daca te chinui mult si probabil, intr-un final, o sa faci revert :D. o sa te faca sa-ti schimbi modul in care scrii cod in viitor.

ps: n-a fost cu rea intentie. nu ma implicam daca consideram ca-i total inutil.

exact ce spune @alexjorj mai sus.

1 Like

Te rog, am mentionat WP intr-un singur thread. Vrei sa te ajut sau sa argumentam pro/cons wp? Tot ce am zis tine de cazul general.

Revenind pe subiect: DI nu necesita Container, ca si concept.

  1. clasa Collection depinde de mai multe clase: API, WpQueryFactory, FooBaz
  2. clasa Collection are nevoie și de un parametru custom, ce nu poate fi injectat;
  3. clasa CollectionFactory instanțiază clasa asta, dar problema este că le instanțiez eu, nu DI container, deci eu trebuie să mă ocup de toate deps;
  4. Fiecare clasă poate avea alte dependențe, caz în care ajung în punctul în care am degeaba container, dacă le pun manual…

Cred că toate astea le rezolvi spărgând împărțirea codului în clase tip servicii și clase tip date (DTO/DAO). În cazul tău Collection este și serviciu (face chestii - business logic) și date (conține ID-ul).

Amestecând astea două în aceiași clasă e greu de folosit DI. De obicei folosești DI pentru servicii (clasele care conțin logică). Datele le pasezi ca și argument când apelezi serviciile (în cazul tău ID).

o clasă are nevoie de niște parametri și are dependențe. Gen __construct( int $userID, Logger $logger). Cum injectez Logger automat dar specific și $userID?

Aceiași problemă aici. Dacă ai ID înseamnă că folosești clasa pentru date și atunci aș evita să folosesc loggerul. Dacă ai ceva probleme throw an exception și faci catch în clasa unde ai business logic.

aceeași clasă are o metodă ce trebuie să returneze instanțe de ceva-uri: public function getCeva(): Ceva. Cum instanțez un Ceva fără să accesez direct containerul? Pot accesa direct containerul DAR asta înseamnă că am o cuplare directă între clasa mea și container.

getCeva() ar trebui să fie parte dintr-o clasă cu business logic. Tot ce are nevoie din container ar trebui să fie injectat în constructor și să primească ca parametru orice altceva.


Pentru că PHP-ul e executat pentru fiecare request nu te forțează deloc să optimizezi și să-ți structurezi codul cât mai abstract.

În alte limbaje de programare, un request e doar un call din multe altele pe durata de execuție a aplicației și te obligă un pic să implementezi business logic-ul în clase tip servicii care sunt re-utilizabile pentru mai multe request-uri (tot ce ține de request vine ca parametru input). Inclusiv PHP poate fi folosit așa (ReactPHP, PHP-PM, PHPFastCGI).

În cazul tău, dacă un serviciu conține ID care ține de HTTP request nu mai e practic serviciu, merge rulat doar pentru requestul cu ID-ul respectiv.

Request/Response/datele din database sunt obiecte de tip date:

  • pot fi instanțiate oriunde în aplicație fără să ai nevoie de dependency injection (depind doar de datele pe care le conțin)
  • nu ar trebui să conțină business logic (poate doar proprietăți calculate on-the-fly; de exemplu: ai dimensiunile în DB și o metodă care-ți calculează volumul)
  • metodele pe care le conțin nu ar trebui să schimbe state-ul aplicației (nu aș folosi logger de exemplu)

Știu că nu este singura abordare (de exemplu dacă folosești ActiveRecords nu mai e valabil ce am zis eu), deci nu vreau să se înțeleagă că o consider soluția unică. Însă cu asta mă întâlnesc eu zi de zi și în PHP și în Java și cred că separarea asta ajută și cu DI.

E greu să înțelegem problema de care te lovești tu și să-ți sugerăm soluția bazându-ne doar pe exemple abstracte. Însă consider că este un exercițiu util. Eu am mai învățat chestii de când cu discuția de aici și sper să nu degenereze în flame war.

Interesant :thinking: Tema mea de casă pentru azi: să aprofundez Pure Dependency Injection și care-s exact diferențele dintre Service Locator și DI.

1 Like

Mea culpa, când m-am referit la DI mă refeream de fapt la containerul DI (hence the mention php-di)

Tema mea de casă este să aprofundez asta :smiley: