Tehnologija Vodič

Kako napraviti dependency injection kontejner u PHP-u

dependency_injection

Prilikom razvoja modernih PHP aplikacija, naročito kod objektno orijentisanog programiranja (OOP), važno je da na pravilan način upravljamo zavisnostima među klasama. Umesto prakse da jedna klasa sama instancira sve što koristi, bolji pristup je da joj se te zavisnosti ubrizgaju spolja. Ovaj pristup je poznat kao Dependency Injection (DI) i predstavlja jedan od najvažnijih dizajn šablona u savremenom PHP razvoju.

Šta je Dependency Injection?

Dependency Injection (ubrizgavanje zavisnosti) je dizajn šablon u objektno orijentisanom programiranju koji omogućava klasama da dobiju sve što im je potrebno za rad spolja, umesto da ih same kreiraju.

Drugim rečima, umesto da klasa sama stvara instance objekata koje koristi (npr. da sama pravi Logger ili Database), sve te objekte dobija kao gotove prilikom instanciranja, najčešće u formi konstruktora. Na taj način se uklanja direktna zavisnost od konkretnih implementacija, što vaš kod čini fleksibilnijim i lakšim za održavanje.

Bez dependency injection-a, kod često postaje težak za testiranje i previše zavisan od konkretnih implementacija. Ukoliko klasa sama stvara svoje zavisnosti, ne možete lako da je testirate sa lažnim (mock) objektima, kao ni da dinamički menjate ponašanje aplikacije.

Dependency injection rešava ovaj problem tako što odvaja proces kreiranja objekata od njihove upotrebe.

Koje su alternative Dependency Injection-u?

Dependency injection nije jedini način na koji se reševaju zavisnosti u objektno orijentisanom programiranju. Neki od alternativnih pristupa uključuju:

  • Korišćenje servis lokatora, gde klasa sama traži zavisnosti iz globalnog registra. Iako je ovo fleksibilno rešenje, ovo uvodi neke skrivene zavisnosti i samim tim otežava testiranje.
  • Globalne promenljive ili singleton instance, što svakako može biti praktično, ali značajno narušava čitljivost i modularnost koda.
  • Ručno instanciranje zavisnosti, što postaje nepregledno kako vremenom vaša aplikacija raste.

I pored ovih uslovnih alteranativa, dependency Injection se u praksi pokazao kao najodrživiji i najčistiji pristup.

Kako izgleda jednostavan dependency injection kontejner u PHP-u?

Da bismo razumeli kako funkcioniše dependency injection, najpre ćemo napraviti sopstveni dependency injection kontejner u najjednostavnijem mogućem obliku. Ova klasa će nam omogućiti da registrujemo objekte (ili „recept“ za pravljenje objekata) pod određenim ključem, i kasnije ih dohvatimo kada su nam potrebni. Time postižemo osnovnu funkcionalnost svakog dependency injection sistema.

class Container {

    private array $bindings = [];

    public function set(string $id, callable $factory): void {

        $this->bindings[$id] = $factory;

    }

    public function get(string $id) {

        if (!isset($this->bindings[$id])) {

            throw new Exception("Zavisnost [$id] nije registrovana.");

        }

        return ($this->bindings[$id])($this);

    }

}

Primer upotrebe dependency injection kontejnera

Sada ćemo pokazati kako se koristi kontejner koji smo definisali. Napisaćemo tri klase koje zavise jedna od druge: Logger, Database i UserController. Zatim ćemo ih registrovati u kontejner, a onda i zatražiti instancu glavnog kontrolera. Na taj način ćemo praktično pokazati kako kontejner automatski rešava zavisnosti između klasa bez potrebe da ih ručno kreiramo svaki put.

class Logger {

    public function log(string $msg) {

        echo "LOG: $msg\n";

    }

}

class Database {

    public function query(string $sql): array {

        return [['name' => 'Milan']];

    }

}

class UserController {

    public function __construct(private Logger $logger, private Database $db) {}

    public function index() {

        foreach ($this->db->query('SELECT * FROM users') as $user) {

            $this->logger->log("Korisnik {$user['name']} je prijavljen.");

        }

    }

}

Ovaj primer prikazuje jednostavnu implementaciju dependency injection-a u PHP-u.

  • Logger klasa služi za ispis log poruka.
  • Database klasa simulira bazu podataka i vraća korisnike.
  • UserController zavisi od Logger i Database klasa, koje prima kroz konstruktor (DI putem konstruktora).

Metoda index() poziva query() da dobije korisnike i zatim za svakog ispisuje log poruku o prijavi.
Na ovaj način, UserController ne stvara sam svoje zavisnosti, već ih prima spolja, što omogućava bolju testabilnost i fleksibilnost.

Registracija u kontejneru izgleda ovako:

$container = new Container();

$container->set('logger', fn() => new Logger());

$container->set('database', fn() => new Database());

$container->set('userController', fn($c) => new UserController(

    $c->get('logger'),

    $c->get('database')

));

$controller = $container->get('userController');

$controller->index();

U ovom primeru se koristi DI kontejner za ručnu registraciju zavisnosti.

  • Prvo kreiramo instancu Container klase.
  • Metodom set() registrujemo kako će se instancirati Logger, Database i UserController.
  • Kod UserController registracije, koristi se closure (funkcija) koja iz kontejnera prvo dohvata logger i database, pa ih prosleđuje konstruktoru UserController-a.

Na kraju, pozivom $container->get('userController') dobijamo potpuno konfigurisanu instancu UserController-a, spremnu za upotrebu.
Pozivanjem index() metode se izvršava logika koja koristi prethodno ubrizgane zavisnosti.

Automatsko rešavanje zavisnosti pomoću PHP refleksije

U prethodnom primeru morali smo ručno da registrujemo svaku klasu i ručno da prosleđujemo zavisnosti. To funkcioniše i kod malih aplikacija, ali kako broj klasa raste, ovaj pristup postaje manje upotrebljiv i sklon greškama. Zato ćemo sada da napravimo unapređenu verziju kontejnera koja koristi PHP refleksiju da automatski otkrije koje klase su potrebne za instanciranje, bez ručne registracije.

public function build(string $class) {

    $reflector = new ReflectionClass($class);

    if (!$reflector->isInstantiable()) {

        throw new Exception("Klasa $class nije instancabilna.");

    }

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {

        return new $class;

    }

    $deps = [];

    foreach ($constructor->getParameters() as $param) {

        $type = $param->getType();

        if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {

            $deps[] = $this->get($type->getName());

        } else {

            throw new Exception("Zavisnost {$param->getName()} ne može da se reši.");

        }

    }

    return $reflector->newInstanceArgs($deps);

}

Ova metoda build() koristi PHP refleksiju da automatski instancira klasu i reši njene zavisnosti.

  • ReflectionClass omogućava uvid u strukturu klase (da li može da se instancira, koje parametre prima konstruktor itd).
  • Ako klasa nema konstruktor, kreira se nova instanca bez parametara.
  • Ako postoji konstruktor, prolazi se kroz sve njegove parametre.
  • Za svaki parametar proverava se da li ima definisan tip i da li je taj tip neka klasa (a ne ugrađeni tip poput int ili string).
  • Ako je validan, iz kontejnera se dohvata odgovarajuća instanca i dodaje u niz $deps.
  • Na kraju, newInstanceArgs() instancira klasu sa svim zavisnostima automatski prosleđenim.

Ovaj pristup eliminiše potrebu da ručno navodimo kako se kreiraju objekti. Dovoljno je samo da imamo registrovane klase i njihove zavisnosti u kontejneru.

Primer korišćenja:
$controller = $container->build(UserController::class);
$controller->index();

Ovde se poziva metoda build() iz DI kontejnera da bi se automatski kreirala instanca klase UserController, zajedno sa svim njenim zavisnostima (Logger i Database), koristeći refleksiju. Kontejner analizira konstruktor UserController klase i automatski ubacuje potrebne objekte. Nakon toga se poziva metoda index(), koja koristi te zavisnosti da izvrši logiku (npr. ispisuje log poruke o prijavljenim korisnicima).

Testiranje uz dependency injection kontejner

Jedna od najvećih prednosti dependency injection pristupa je lakše testiranje. Kada klasa prima svoje zavisnosti spolja (umesto da ih sama pravi), možemo u testovima da ih zamenimo simuliranim verzijama. U sledećem primeru ćemo definisati lažnu klasu FakeLogger, koja umesto da ispisuje poruke, samo ih skladišti u nizu. Time možemo da proverimo da li je logika izvršena bez stvarne baze ili logger-a.

class FakeLogger extends Logger {

    public array $logMessages = [];

    public function log(string $message): void {

        $this->logMessages[] = $message;

    }

}

U PHPUnit testu koristimo ovaj FakeLogger i stub za bazu:

public function testUserControllerLogsUsers() {

    $logger = new FakeLogger();

    $db = new InMemoryDatabaseStub();

    $controller = new UserController($logger, $db);

    $controller->index();

    $this->assertContains('Korisnik Jelena se prijavio.', $logger->logMessages);

}

U ovom primeru prikazano je kako Dependency Injection olakšava testiranje pomoću lažnih (fake) i stub klasa.

  • FakeLogger nasleđuje Logger, ali umesto da ispisuje poruke, one se čuvaju u nizu $logMessages.
  • U PHPUnit testu, kao baza koristi se InMemoryDatabaseStub, koji simulira bazu u memoriji (nije prikazan ovde, ali se pretpostavlja da vraća podatke sa korisnicima).
  • UserController se instancira tako što se proslede FakeLogger i stub baze.
  • Nakon poziva index(), proverava se da li je u nizu log poruka zabeležena očekivana poruka.

Na ovaj način, test ne zavisi od stvarne baze ni od pravog loggera, što ga čini brzim, predvidivim i izolovanim — što je jedna od glavnih prednosti Dependency Injection-a.

Kada koristiti ručnu registraciju, a kada refleksiju?

Pitanje kada koristiti ručno registrovanje zavisnosti, a kada se osloniti na refleksiju za automatsko rešavanje, zavisi od više faktora, složenosti aplikacije, potreba za performansama, nivoa kontrole koji želite, kao i okruženja u kom se aplikacija izvršava (razvojno, testno, produkciono).

Ručna registracija zavisnosti daje potpunu kontrolu nad načinom na koji se objekti kreiraju. Možemo precizno da definišemo kako se instancira neka klasa, dodamo spoljne parametre (npr. konfiguraciju iz .env fajla, API ključeve, podatke o konekciji s bazom), prosledimo vrednosti koje nisu klase, već obični primitivni tipovi, kao i da koristimo različite implementacije iste klase u zavisnosti od konteksta (recimo, pravi Mailer u produkciji, a FakeMailer u testiranju). Ovaj pristup je idealan kada vam je potrebna sigurnost, predvidljivost i kada tačno znate kako želite da izgleda tok instanciranja i inicijalizacije.

Takođe, kod ručne registracije izbegava se refleksija, što znači da nema dodatnog „razmišljanja“ PHP-a u vreme izvršavanja, nema introspektivnog analiziranja konstruktora, tipova parametara i strukture klasa. To za rezultat ima bolje performanse, naročito kada se aplikacija izvodi pod opterećenjem ili u ograničenom okruženju (npr. CLI skripte, mikroservisi, worker procesi i sl.).

Refleksija je bolji izbor u situacijama kada ne želite da pišete desetine ili stotine fabričkih funkcija, već vam je potrebno da sistem automatski detektuje koje zavisnosti su potrebne nekoj klasi i automatski ih ubrizga. Ovo značajno ubrzava razvoj i smanjuje količinu konfiguracije. Pogotovo je korisno kada imate klase sa jednostavnim konstruktorima, koje koriste samo tipizirane zavisnosti. Refleksija je takođe vrlo korisna u ranim fazama razvoja projekta, kada se sve još menja, jer omogućava da ne definišete tok instanciiranja unapred.

Naravno, refleksija ima i svoju cenu — svako pozivanje ReflectionClass, getParameters() i analiza tipova zahteva dodatno procesorsko vreme. U aplikacijama koje zahtevaju visoke performanse i gde se veliki broj objekata često instancira, refleksija može da postane usko grlo. Tada se preporučuje da se kritične komponente (npr. kontroleri koji se pozivaju pri svakom zahtevu) registruju ručno.

Najefikasniji pristup u praksi je kombinacija oba: refleksija za jednostavne slučajeve koji ne zahtevaju dodatnu konfiguraciju, a ručna registracija za složenije objekte, za one koji zavise od runtime vrednosti ili za instance koje treba keširati, prilagoditi po okruženju, testirati drugačije ili precizno kontrolisati.

Gotova rešenja: PHP-DI i druge biblioteke

Iako je korisno razumeti kako funkcioniše dependency injection kontejner, većina ozbiljnih PHP aplikacija koristi gotove biblioteke koje rešavaju različite izazove automatski i na ispravan način. Jedna od najpopularnijih biblioteka je PHP-DI. U pitanju je napredan kontejner koji podržava sve što je potrebno za moderan razvoj: automatsko rešavanje zavisnosti, anotacije, lazy-loading, keširanje instanci i čak integraciju sa framework-ovima poput Slim-a ili Laravel-a.

Korišćenjem PHP-DI-a ne samo da štedite vreme, već dobijate stabilan i proveren sistem koji je testiran u velikom broju produkcionih okruženja. Takođe, zajednica i dokumentacija su izuzetno kvalitetni, što olakšava učenje i rešavanje problema.

U okviru Slim frameworka, integracija PHP-DI-a izgleda jednostavno. U index.php možete registrovati kontejner:

use DI\Container;

use Slim\Factory\AppFactory;

$container = new Container();

AppFactory::setContainer($container);

$app = AppFactory::create();

Ovaj segment prikazuje osnovnu integraciju PHP-DI kontejnera sa Slim frameworkom.

  • use DI\Container; — koristi se PHP-DI, popularna biblioteka za dependency injection u PHP-u.
  • use Slim\Factory\AppFactory; — uključuje se fabrička klasa koja služi za pravljenje Slim aplikacije.
  • $container = new Container(); — kreira se instanca PHP-DI kontejnera.
  • AppFactory::setContainer($container); — povezujemo kontejner sa Slim-om, kako bi framework mogao automatski da ubrizgava zavisnosti u rute, middlewar-e i kontrolere.
  • $app = AppFactory::create(); — kreira se aplikacija koristeći prethodno definisani kontejner.

Ovim koracima omogućavamo da cele aplikacije u Slim-u imaju pristup kontejneru, čime se olakšava modularnost, testiranje i upravljanje zavisnostima.

Zatim, u konfiguracionom fajlu, definišete servise:

$container->set(Logger::class, function() {

    return new Logger();

});

Sada bilo koja klasa kojoj je potreban Logger može ga dobiti automatski, bez dodatne konfiguracije.

Zaključak

Kao što ste videli, dependency injection u PHP-u nije samo još jedan u nizu dizajn šablona — to je jedan od ključnih principa savremenog objektno orijentisanog programiranja. Omogućava vam da pišete modularan, fleksibilan i lako testabilan kod, bez čvrstih veza između klasa.

Bilo da se odlučite da sami napišete sopstveni DI kontejner radi boljeg razumevanja, ili da u projektima koristite zrela rešenja kao što je PHP-DI, razumevanje načina na koji se zavisnosti ubrizgavaju i upravljaju njima pomoći će vam da pravite skalabilne, održive i profesionalne PHP aplikacije.

Upravo ovakva arhitektonska disciplina pravi razliku između koda koji „samo radi“ i koda koji može da raste, da se testira i da traje.

Ostavi komentar

Vaša adresa neće biti objavljena