Cum folosesc corect un DI în afara unui framework?

Să zicem că-i PHP-DI, dar nu ne limităm la ăsta.

Pentru că am o … chestie mai mare în WP, pentru că este nevoie de (mai multe) teste pentru toată povestea și pentru că pasarea ca argument a claselor merge până la un punct, am zis că e cazul să step up the game.

Dependency injection este ce-mi trebuie. Asta știu. Ce nu știu este următoarea treabă:

Există situații în care am nevoie de instanța unei clase într-o funcție, nu într-o clasă. Deci nu am cum să o pasez în __construct.

function process_text($text) 
{
  return (new TextProcessor($text))->process();
}

Chestia asta merge dar… nu e ok. Am tight coupling iar funcția în sine este imposibil să o testez.

Am mai folosit în trecut (Auryn, dacă are vreo importanță) în felul următor:

Fac o funcție accesibilă global

function di()
{
  $container = new Container();
  // $container->set('logger', ....
  // $container->set('Db',...
  return $container;
}

Apoi, unde am nevoie de o instanță:

function process_text($text) 
{
  return di()->get('TextProcessor', $text))->process();
}

Problema este că… nu mi se pare o îmbunătățire (prea mare) a situației inițiale. Mi se pare un pic hacky.

Ce anume îmi scapă din toată povestea asta?

2 Likes

Poate merge folosind o funcție anonimă: PHP: Anonymous functions - Manual

1 Like

In software engineering, dependency injection is **a design pattern in which an object or function receives other objects or functions that it depends on** .

mai concret, pt dependency injection functia ta ar trebui sa accepte si procesorul de text ca argument.
ps:
inteleg ca ala e doar un exemplu simplificat, pune si tu ceva concret

Păi este un exemplu concret. Am un procesor de text care face… chestii.

În WP îl folosesc așa:

add_filter('the_content', 'process_text')

(al doilea argument este un callable)

Mai departe (cod asupra căruia nu am niciun control), este folosită așa:

$content = apply_filters('the_content', $content);

Adică așa?

function process_text($text, TextProcessor $processor) {}

Nu am o problemă cu asta, dar cred că sunt în aceeași situație: cum apelez funcția?

uitasem ca-i worldpress si ca biblia are mai mult sens :D… nu prea are rost sa modifici metoda in sine, dupa aia ar trebui sa faci apply_filters(‘the_content’, $content, new TextProcessor()) peste tot unde-i folosit filtrul respectiv.

avand in vedere ca nu stiu absolut nimic de wp, poti ignora urmatoarea idee…
da n-ar fi mai normal sa testezi metoda de apply_filters?
inteleg ce vrei de fapt… but it’s worldpress… ar trebui sa-l rescrii :smiley:

ffs, inca foloseste globale. parerea mea-i ca te chinui mult prea mult.

1 Like

Asa e. Dar dependintele pe care le injectezi deobicei depind si ele la randul lor de altele.

Exemplu:

Ai clasa de db: aka cel care face conexiunile si executa sql-ul
Ai clasa de repository care depinde de db.

Tu injectezi repository-ul intr-o metoda, dar si dependinta sa de db trebuie satisfacuta. Este un exemplu simplu, dar tot tree-ul de dependinte trebuie rezolvat.

De-aia o biblioteca buna de DI rezolva behind the scenes toate treburile acestea si intr-un mod eficient.

Acum, pentru wordpress se poate folosi o anumita abordare:

Faci un wrapper peste functie, care prin reflection isi da seama ce trebuie DI-uit si face apelul cu parametrii potriviti (aka deja instantiati). Biblioteca de DI trebuie doar sa poata da get la clase si in rest e relativ simplut de scris.

Este ceva atât de generic încât nici să glumesc pe seama asta nu-mi vine.:sweat_smile::sweat_smile:

In a nutshell: apply_filters este folosit pentru … orice. Poate avea absolut orice tip de date, pentru că add_filter poate întoarce orice, fără nicio modalitate de a forța tipul de date (altfel decât cu gettype sau is_a). Iar plugin-urile se agață de aceste filtre și se execută în ordiner.

De exemplu, în core-ul WP, filtrul the_content arată așa:

add_filter( 'the_content', 'wptexturize' );
add_filter( 'the_content', 'wpautop' );
add_filter( 'the_content', 'shortcode_unautop' );
add_filter( 'the_content', 'prepend_attachment' );
add_filter( 'the_content', 'wp_filter_content_tags' );
add_filter( 'the_content', 'wp_replace_insecure_home_url' );

hmm, ai putea incerca sa injectezi din functia apply_filters. dar n-am gasit nici o metoda cum sa te uiti la semnatura unei functii ca sa-ti dai seama ce parametri asteapta.

ps: mint, se poate

1 Like

Nu sunt familiarizat cu WP, însă când m-am mai luptat cu implementări din astea am recurs la wrappere.

De exemplu:

// O funcție dummy pe care o înregistrezi cu WP
// Greu de testat și nu cred că merită testată 
// Nu conține business logic
function process_text($text)
{
    // static cache ca să nu instanțiezi de mai multe ori în caz că 
    // frameworkul de DI nu oferă asta, sau dacă nu folosești framework
    static $processor; 
    if (!isset($processor)) {
       // init $processor
       $processor = new TextProcessor(/* whatever you need*/);
    }
    // sau dacă ai ceva container care oferă tot ce vrei:
    $processor = di()->getTextProcessor();

    // apelezi apoi obiectul care conține toată logica
    return $processor->process($text);
}

// Apoi în WP faci ceva de genul:
add_filter('the_content', 'process_text');

Clasa TextProcessor poate să folosească DI și să fie ușor de scris unit-test-uri pentru ea. Din moment ce logica e în mare parte în această clasă, nu mai conteză că nu testezi și funcția process_text().

Sper că am înțeles bine ce vrei tu.

1 Like

Revin cu un exemplu mai detaliată (folosind php-di):

(Collection este o colecție de documente: pdf, doc, imagini etc)

class CollectionRoute implements RouteInterface
{
  public function __construct(private CollectionFactory $collectionFactory) {}

  public function create(\WP_REST_Request $request)
  {
      //....
      $this->collectionFactory->make($request);
      //....
  } 

Apoi pentru factory am așa:

class CollectionFactory
{
    public function __construct() {}

    public function make(\WP_REST_Request $request)
    {
        $args = new CollectionArgs($request);

        return (new Collection($request->get_param('id'), new Foobar(new Foo(new Bar))))->create($args);
    }
}

Deja aici am nevoie să pun mai multe argumente în constructor:


class Collection
{
    public function __construct(private int $id, private Foobar $foobar, private WpQueryFactory $wpQueryFactory, private FooBaz $foobaz) {}

    public function create(CollectionArgs $args)
    {
        //....
        $this->foobar->doStuff(/*...*/);

        $this->foobaz->otherStuff($args->getId());
    }

Iar acum am câteva probleme:

  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…

Și nu știu cum să procedez mai departe, fac câte un factory pentru fiecare rahat? Mi se pare overkill. Sunt niște detalii care îmi scapă și pe care pur și simplu nu știu de unde să le culeg.

aici faci injectia, nu in Collection

E ok ce ai facut tu initial, e un fel de service locator, imbunatatirea este clara: declari o singura data depdendentele + containerul stie sa iti construiasca servicii cu multe dependente odata ce le delcari.

Cum bine zicea si @alescx incearca sa faci injectia in factory si in colectie trimiti la create() direct datele de care ai nevoie, fara sa mai faci apelurile alea in collection->create()

problema-i ca ala cu php di nu-i di de nici o culoare. cel putin, nu asa cum il inteleg si-l folosesc eu.

@iamntz , poate incerci sa explici mai bine ce exact vrei sa faci. cam cum ar arata testul.

ar mai fi o solutie sa incerci sa construiesti clasa collection manual, sa mockuiesti CollectionFactory si sa-i explici ca metoda make o sa-ti intoarca o instanta de collection.

https://docs.phpunit.de/en/9.5/test-doubles.html

$collection = new Collection(.....);
$myCollection = $this->createStub(CollectionFactory::class);
$myCollection ->method('make')
    ->willReturn($collection);
   // sau direct ->willReturn(new Collection(.....));

Deci ai ajuns la concluzia ca NU asa trebuie sa fie, aka structural, si vrei sa bagi OOP in WP. Te chinui degeaba. In cativa ani (10?) WP va fi obiectual. Gaseste solutii din programarea clasica si nu te incurca cu OOP doar pentru ca e posibil. Nu merita. Am un exemplu de cineva care a bagat doctrine in WP si a fost destul de funny. A iesit, dar… nementenabil.

1 Like

Poți detalia asta?

Nu-s chitit pe php-di, momentan sunt în faza în care învăț conceptul iar orice informație este binevenită, mai ales că toate materialele pe acest subiect par scrise de același autor, că se termină fix în același punct :smiley:

Se simte cam așa:

Păi… vreau să nu mă ocup eu de dependențele fiecărei clase.

I.e. nu vreau să:

  • instanțiez manual chestii
  • injectez manual chestii

@alexjorj nu este neapărat vorba de wp aici. Încerc să asimilez un concept; din moment ce decuplez clasele, este irelevant mediul în care vreau să fac asta…

Pai Symfony a standardizat DI acum mai bine de o decada in lumea PHP. The DependencyInjection Component (Symfony Docs)

De atunci au tot aparut ba un PSR, ba Laravel encapsulations peate aceleasi chestii scrise de altii, ai tot asa.

Mergi si invata cum se foloseste componenta Symfony si gata.

2 Likes

Cred ca ai o parerea prea buna despre concept si te astepti la ceva minunat. E asa de banal incat autorul crede ca te-ai prins deja de ce vrea sa zica. E pur si simplu normal sa accesezi cumva clasele de care depinde clasa ta, iar cel mai natural e ca instanta obiectului sa primeasca pointerii de care depinde la momentul crearii. Pana la urma vorbim de obiecte/instante si nu de clase/definitii.

Păi uite că nu e chiar atât de banal, că nu este evident cum accesezi containerul când ai nevoie de o instanță nouă razna în cod…

Uite cazuri de care m-am lovit recent și nu știu cum să le rezolv atât de banal:

  • 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?

  • 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.

Sunt atât de neevidente lucrurile astea încât încep să fiu din ce în ce mai sigur că fac eu ceva greșit. Dar nu-mi dau seama ce…


@tekkie documentația de la Symfony am parcurs-o de zilele trecute, dar s-a întâmplat fix cum zice Creangă: m-am dus bou, m-am întors vacă :joy::joy:

1 Like

Gandeste abordarea in obiecte nu in clase. Tu creezi obiecte care ar avea nevoie de un alt obiect Logger. Sa zicem ca instantiezi 5 obiecte “logan” definite de clasa Masina. La intantiere le pasezi tuturor aceeasi instanta a obiectului “log” creeat din clasa Logger. Asa merge treaba.
Daca ai o metoda getCeva() care returneaza instante ale unor alte clase, acele instante le creezi cu new NumeClasa. Daca si ele depind de ceva, acel ceva trebuie sa ajunga cumva in punctul unde le creezi, deci fie il adaugi ca dependinta in clasa Masina fie ca parametru in metoda getCeva(). Containerul e doar un super-obiect care stocheaza instante ale altor obiecte folosite mai des, de obicei numite “servicii”.

2 Likes

ba e chiar atat de banal. metoda primeste ca parametri obiectele de care are nevoie, nu le instantiaza singura.

new MyClass(new Dependency());

function(new Dependency());

asta-i tot conceptul.

anumite frameworkuri stiu sa se uite la parametrii unei metode si instaintiaza automat toate dependintele. dar o fac doar in constructor sau in metodele rutelor.

class MyService{
 public function __construct(Dependency1 $dependency, Depencdency2 $dependency ....)
}
/**
* @Route("endpoint")
*/
public function endpoint(string id, SomeService $service);

fraemworkul o sa se uite la ruta apelata, o sa vada metoda si o sa injecteze SomeService ca parametru. id-ul ar trebui sa vina din request.

in schimb nu o sa stie sa injecteze $obj.

function doStufff(Object $obj){
}

dar nu cred ca e prea usor sa faci asta in wp.