Anax med Dependency Injection

By . Latest revision .

Vi jobbar vidare med Anax och REM Servern och använder oss av begreppen dependency injection och lazy loading i ett försök att förbättra strukturen på vår kod.

Det blir en övning i refactoring av kod och resultatet blir förhoppningsvis kod som blir enklare att underhålla, vidareutveckla och testa.

Vi har ju sedan tidigare en fungerande REM server integrerad i vår me-sida. Vi fick den koden från en befintlig REM server med koden skriven i routerna och därefter tittade vi på en uppdaterad server där koden var omstrukturerad till att ha koden i kontroller- och modell-klasser.

Vi skall nu titta på samma REM server som nu är uppdaterad att fungera i Anax tillsammans med en DI kontainer.

Förbered dig på refactoring och att strukturera om kod. I slutändan funderar vi på om den nya strukturen blev bättre eller inte.

#Förutsättning

Du har läst artikeln “En REM Server som Kontroller och Modell”.

#Glöm inte .htaccess

Glöm inte att redigera din .htaccess när du publicerar till studentservern.

#En tom Anax

När jag jobbar i denna artikeln vill jag ha en tom webbplats att leka med så jag använder kommandot anax för att scaffolda en sådan.

# Gå till kursrepot
cd me/kmom03/
anax create anax3/ ramverk1-me

Öpnna din webbläsare mot me/kmom03/anax3/htdocs för att kontrollera att webbplatsen fungerar. Kontrollera även att routen htdocs/debug/info fungerar.

#Anax DI

Då skall vi konvertera anax3 till att enbart använda sig av konceptet DI. Vi tar den biten innan vi tittar på REM servern och hur den använder sig av DI konceptet i Anax.

#Modulen anax/di

Det allra första vi behöver till anax3 är modulen anax/di och vi installerar den med composer.

# Gå till kursrepot
cd me/kmom03/anax3
composer require anax/di

Nu finns modulen på plats, du kan se hur den är uppbyggt i katalogen vendor/anax/di. Katalogstrukturen är som en vanlig modul till Anax.

#Konfigurationen av tjänsterna

Tidigare satte vi upp ramverkets tjänster i filen config/service.php. Nu skall vi göra det på ett nytt sätt via en ny konfigurationsfil config/di.php.

Vi kopierar en fil från DI-repot så vi har något att utgå ifrån.

# Du är i katalogen anax3
cp vendor/anax/di/config/di.php config/

Öppna de båda filerna config/service.php och config/di.php för att jämföra koden i dem. Du kan troligen mappa koderna mellan filerna och se hur den nu är strukturerad.

Sammanfattnings kan vi säga att vi nu definierar alla tjänsterna i en array med deras namn och en callback som kan initiera tjänsten när den behövs.

Inledningen i filen config/di.php ser ut så här och det är definitionen av tjänsten request och response vi ser och den utförs på samma sätt som tidigare, det är bara en liten annan struktur.

/**
 * Configuration file for DI container.
 */
return [
    // Services to add to the container.
    "services" => [
        "request" => [
            "shared" => true,
            "callback" => function () {
                $request = new \Anax\Request\Request();
                $request->init();
                return $request;
            }
        ],
        "response" => [
            "shared" => true,
            "callback" => "\Anax\Response\Response",
        ],

Koden ovan ersätter alltså den vi tidigare såg i config/service.php.

Så här såg den tidigare varianten ut.

// Add all resources to $app
$app = new \Anax\App\App();
$app->request    = new \Anax\Request\Request();
$app->response   = new \Anax\Response\Response();

// Configure request
$app->request->init();

Tjänsterna skapas, men ännu ligger de inte i en $app, de är bara förberedda för att läsas in i en DI-kontainer, en $di.

#Delad tjänst och inte

När en tjänst är delad, “shared”, så innebär det att det bara finns en instans av tjänsten i ramverket. Första gången som tjänsten skapas så är det alltid callbacken som anropas. Om någon försöker nå tjänsten igen, via DI-kontainern, så returneras samma instans, samma objekt.

Om tjänsten inte är delad så skapas och returneras en ny instans varje gång man ber om tjänsten via DI-kontainern. Det är skillnaden mellan delade och icke-delade tjänster.

De tjänsterna som vi har sett hittills är alla delade och finns således bara som en instans i ramverket.

Om du är bekant med begreppet och designmönstret Singleton så kan man säga att det är liknande, det finns bara en instans av klassen i hela ramverket. Designmönstret Singleton är en annan variant som uppnår ett liknande upplägg i en applikation.

#Skapa DI-kontainern

Då skall vi skapa DI-kontainern och fylla den med tjänster från konfigurationsfilen. Detta är något vi gjort på olika platser under olika projekt och kurser. Från början använde vi filen htdocs/index.php, sedan flyttade vi koden till config/service.php som vi nu inte använder längre.

Det vi gör nu är att åter titta på htdocs/index.php, det är där som vi skapar objektet $app oavsett var vi lägger till tjänsterna.

Vi uppdaterar htdocs/index.php så att vi skapar objektet $app via new, istället för att inkludera den gamla filen.

// Add all services to $app
//$app = require ANAX_INSTALL_PATH . "/config/service.php";
$app = new \Anax\App\App();

Vi vill nu skapa en instans av $di och här finns flera alternativ hur vi kan göra. Modulen anax/di erbjuder olika varianter av hur vi skapar vår DI-kontainer. Jag tänker använda varianten Anax\DI\DIFactoryConfig som läser in tjänsterna från en konfigurationsfil.

// Add all services to $app
//$app = require ANAX_INSTALL_PATH . "/config/service.php";
$di  = new \Anax\DI\DIFactoryConfig("di.php");
$app = new \Anax\App\App();

Nu har vi $di och $app men det vore enklare om vi kan integrera $di in i $app, ungefär som vi tidigare jobbade med $app-objektet. Det är ingentligen inget vi behöver, men det kan vara enklare med tanke på bakåtkompabilitet.

#Injecta $di in i $app

I modulen anax/di finns ett interface \Anax\DI\InjectionAwareInterface och ett trait \Anax\DI\InjectionAwareTrait som implementerar interfacet. Dessa är till för att användas av en klass som är tänkt att injectas med en DI-kontainer såsom $di.

Kika på källkoden bakom interfacet och traitet så ser du att det handlar om en protected medlemsvariabel $di och en metod setDI() som sätter den skyddade medlemsvariabeln.

Genom att implementera interfacet i $app så säger jag att denna klassen skall kunna ta emot en $di och genom att använda traitet så slipper jag skriva den kodbiten själv.

Jag skulle kunna uppdatera min egen klass i src/App/App.php men för exemplets skull så väljer jag att göra en ny subklass för att visa vilken skillnaden är. Sublassen som jag döper till \Anax\App\AppDI.php ser ut så här.

<?php

namespace Anax\App;

use Anax\App\App;
use Anax\DI\InjectionAwareInterface;
use Anax\DI\InjectionAwareTrait;

/**
 * An App class to wrap the resources of the framework, prepared to use a
 * DI container.
 */
class AppDI extends App implements InjectionAwareInterface
{
    use InjectionAwareTrait;
}

Du finner exempelklassen i modulen anax/di och du kan kopiera den så här.

# Du står i katalogen anax3
cp vendor/anax/di/src/App/AppDI.php src/App

Nu kan vi uppdatera htdocs/index.php så att vi använder den nya klassen till $app som vi kan injecta vår $di in i.

Så här blir det.

// Add all resources to $app
//$app = require ANAX_INSTALL_PATH . "/config/service.php";
$di  = new \Anax\DI\DIFactoryConfig("di.php");
//$app = new \Anax\App\App();
$app = new \Anax\App\AppDI();
$app->setDI($di);

Nu finns $di inuti $app men vi kan inte göra en $app->request som vi kunde tidigare. Tjänsten ligger nu i $app->di->get("request") och $app->di är protected. Man kan också komma åt tjänsterna direkt från $di via $di->get("request").

Det hade varit bekvämt om vi kunde göra ett liknande anrop som vi gjorde tidigare med $app->request, då hade det varit enklare att göra $app bakåtkompatibel och lite enklare att gå från icke DI till ren DI i koden.

#En bakåtkompatibel $app

Låt oss därför introducera de magiska metoderna i PHP, här i form av __get() och __call.

När man lägger till en metod __get() i en klass så kommer den att anropas varje gång man försöker nå en medlemsvariabel i klassen som inte är definierad.

På samma sätt fungerar __call() men den gäller anrop till icke existerande metoder.

De båda magiska metoderna är en form av catch-all för läsning av medlemsvariabler respektive anrop av metoder, när dessa medlemsvariabler och metoder är odefinierade i klassen.

Det låter som dessa kan hjälpa oss och lösa vårt önskemål om bakåtkompatibel $app.

När vi tittar in i modulen anax/di så finner vi ytterligare en klass, \Anax\App\AppDIMagic som använder ett trait \Anax\DI\InjectionMagicTrait som implementerar __get() och __call() mot en $this->di. Låt oss använda den.

Du finner klassen i modulen anax/di och du kan kopiera den så här.

# Du står i katalogen anax3
cp vendor/anax/di/src/App/AppDIMagic.php src/App

Nu kan vi uppdatera htdocs/index.php så att vi använder den nya klassen AppDIMagic.

Så här blir det.

// Add all resources to $app
//$app = require ANAX_INSTALL_PATH . "/config/service.php";
$di  = new \Anax\DI\DIFactoryConfig("di.php");
//$app = new \Anax\App\App();
//$app = new \Anax\App\AppDI();
$app = new \Anax\App\AppDIMagic();
$app->setDI($di);

Nu är vi redo att testa om våra ändringar fungerar. Öppna webbläsaren mot katalogen htdocs och du bör se en standardsida och öppnar du htdocs/debug/info så bör även den sidan fungera.

Vi har nu uppdaterat vår Anax till att i grunden använda Anax DI och samtidigt är min $app bakåtkompatibel.

Stanna gärna upp och fundera vilken kod vi ändrat och vad som skiljer sedan tidigare.

#Bakom kulisserna

Om vi studerar tjänsterna i config/di.php så kan vi se, om vi tittar noggrant, att klassen för routern och klassen för vy-kontainern är ändrade till nya klassnamn. Det stämmer. De tidigare klasserna använde sig av $app som injectades in i dem, de nya klasserna baseras på att $di injectas in i dem.

En annan uppdatering är tjänsten viewRenderFile som är en ny tjänst som är den som ansvarar för att rendera en view genom att inkludera template-filen och göra data tillgängligt för vyn. Den klassen har egentligen inget med DI-konceptet att göra utan jag tog bara tillfället i akt och exponerade den som en ramverkstjänst. Nu kan man enkelt byta ut den tjänsten mot en egen renderingsklass, så länge renderingsklassen uppfyller interfacet \App\View\ ViewRenderFileInterface. Så även om denna tjänsten inte är direkt relevant så är den ändå ett exempel på hur man kan exponera något som sker inuti View-klasserna och erbjuda en egen implementation genom att bara konfigurera tjänsten som skapas i.

Nåväl, en bakåtkompatibel $app och en ny arbetshäst i form av $di.

Tidigare skickade vi runt en referens till $app men nu är tanken att vi skickar runt $di istället. Man skulle kunna säga att båda två är variationer av konceptet Service Container.

En fördel med $di är att den löser lazy loading. Lazy loading innebär att tjänsterna är förskapade men laddas inte förrän de behövs. Det är en bra feature i en DI kontainer.

#En REM server

I förra artikeln jobbade vi med en klonad version av REM servern i katalogen me/kmom02/remserver. Du kan fortsätta att använda den katalogen för källkoden till REM servern.

Det du behöver göra är att vara säker på att branchen di är den aktiva branchen.

# Gå till kursrepot
cd me/kmom02/remserver
git fetch
git checkout di
git branch
composer update

Om du öppnar me/kmom02/remserver/htdocs i webbläsaren så bör du se manualen för REM server och routen htdocs/api/users bör också fungera och visa ett JSON objekt.

#Studera ändringarna i REM servern

Låt oss studera och stegvis kopiera över REM servern till vår anax3. Det blir en övning i att se hur vi nu använder oss av en modul tillsammans med Anax DI.

#Initiera som ramverkstjänster

REM servern lägger till sig själv via config/di.php. Kika längst ned i filen så ser du följande kod.

        "rem" => [
            "shared" => true,
            "callback" => function () {
                $rem = new \Anax\RemServer\RemServer();
                $rem->inject($this->get("session"));
                return $rem;
            }
        ],
        "remController" => [
            "shared" => false,
            "callback" => function () {
                $rem = new \Anax\RemServer\RemServerController();
                $rem->setDI($this);
                return $rem;
            }
        ],
    ],
];

Du behöver alltså lägga till den koden till din egen config/di.php. Förutsatt att din inte ändrat något i din egen config/di.php så kan du kopiera den som ligger i REM servern.

# Gå till din katalog för anax3
cp ../../kmom02/remserver/config/di.php config/

Dubbelkolla att det ser bra ut i filen.

#Ändringar i RemServer

Det var en mindre ändring i hur tjänsten rem initieras. Det är metodnamnet och parametern som gjorts någon mindre ändring i, men principen är densamma att klassen vill ha en koppling till ramverkstjänsten för sessionen.

Det var egentligen det enda som uppdaterats. Källkoden i modellklassen RemServer är i övrigt densamma som tidigare.

Klassen var ju enbart beroende av sessionen och har således ingen annan koppling till de ändringar vi gjort med DI.

#Ändringar i RemServerController

Kontrollerklassen RemServerController fick lite fler uppdateringar men alla rörde sig att byta ut referensen från $app till $di som nu injectas in i klassen.

När man tittar på de ändringar som gjorts så ser vi följande kodkonstruktioner.

/**
 * Start the session and initiate the REM Server.
 *
 * @return void
 */
public function anyPrepare()
{
    $session = $this->di->get("session");
    $rem     = $this->di->get("rem");

    $session->start();

    if (!$rem->hasDataset()) {
        $rem->init();
    }
}

I metoden ovan hämtar man de tjänsterna som behövs för att utföra metodens arbete, de lagras undan i lokala variabler och används därefter.

Vi kan även titta på ett annat exempel.

/**
 * Delete an item from the dataset.
 *
 * @param string $key    for the dataset
 * @param string $itemId for the item to delete
 *
 * @return void
 */
public function deleteItem($key, $itemId)
{
    $this->di->get("rem")->deleteItem($key, $itemId);
    $this->di->get("response")->sendJson(null);
    exit;
}

I metoden ovan ser vi att man använder $di direkt och hämtar ut tjänsten och använder den.

Vilken av dessa sätt man använder beror på hur man vill se sin kod. Det kan vara enklare att lagra undan i en lokal variabel om man använder samma tjänst flera gånger. Men annars går det bra att hämta den direkt, och använda den, via ett anrop $this->di->get("the-service-name").

#Kopiera REM servern till Anax

I förra artikeln gick vi stegvis igenom hur man kopierade över REM servern till en Anax-installation. Här är den snabba versionen av hur du gör kopieringen. Se det som en repetition.

# Gå till kursrepot
cd me/kmom03/anax3

# Manualen
rsync -av ../../kmom02/remserver/content/ content/remserver/
rsync -av ../../kmom02/remserver/htdocs/css/style.css htdocs/css/remserver.css

# Klasserna
rsync -av ../../kmom02/remserver/src/RemServer src 

# Konfigurationen
rsync -av ../../kmom02/remserver/config/{remserver.php,remserver} config/

# Routes
rsync -av ../../kmom02/remserver/config/route/remserver.php config/route

Du behöver lägga till så att stylesheeten laddas, sist gjorde vi det i src/App/App.php.

Du behöver editera filen config/route.php för att inkludera REM serverns route fil config/route/remserver.php.

Nu bör din Anax fungera tillsammans med REM servern, precis som i förra kursmomentet.

Öpnna din webbläsare mot me/kmom03/anax2/htdocs/api/users för att kontrollera att REM servern är integrerad och fungerar, du bör se ett JSON-objekt. Om du öppnar me/kmom02/remserver/htdocs/remserver i webbläsaren så bör du se manualen för REM server och via routen htdocs/debug/info bör du se att ett antal routers är laddade.

Det gäller att hålla tungan rätt i munnen då det är många filer. Men när man vänjer sig så blir man mer och mer bekväm med var filerna finns och det blir enklare att felsöka. Oavsett ramverk så handlar det om att hålla koll på var filerna ligger och vad de gör.

#Blev det bättre?

Nu har du en uppdaterad anax3 som använder sig av Anax DI och du har integrerat REM servern som också använder sig av DI. Blev det bättre elle rbara annorlunda?

Kanske är det svårt att se, eller greppa, efter så här kort tid. Om man har en service kontainer i form av $app eller $di kan ju kvitta lika. Men begreppet DI är känt i ramverkssammanhang oavsett språk så det är nog ett gott tips att anamman den.

Vår $app som tidigare innehöll all viktig information gör det fortfarande, men vi ser, och kommer fortsätta se, att dess viktighet blir mindre och mindre när vi bygger ut klasser i ramverket. Det blir mer fokus på $di och att injecta delar av $di in i de klasser som behöver det.

Bara för man har en service kontainer så behöver man inte nödvändigtvis injecta den i alla klasser. Se på RemServer modellen som enbart behövde tillgång till sessionen, där väljer vi att enbart injecta det beroendet. Men i RemServerController så används frekvent de tjänster som ligge i $di vilket gör det rimligt att injecta hela $di.

Anax DI blir nu ett lim som håller samman modulerna i ramverket. En del av beroendena löses med $di. Det innebär också att beroendena till koden som löser DI-delen ökar. Det finns inget som säger att man måste ha just modulen anax/di som löser DI funktionen. Det finns ett arbete i PHP FIG PSR-11 där man försöker definiera en generell DI kontainer. Anax DI implementation är gjord enligt de interface som finns i PSR-11 vilket innebär att den kan bytas ut mot godtycklig kontainer som stödjer PSR-11. Se det som ett exempel på hur moduler kan göras mer oberoende av den miljö de arbetar i.

#Scaffolding av Anax med DI

Nu när vi gått igenom stegen för att göra en installation av Anax med Anax DI så kan vi lika gärna spara undan den mallen så att den blir enkel att scaffolda i framtiden. Sagt och gjort. Det går nu att scaffolda en grund av Anax DI via mallen ramverk1-di.

anax create di ramverk1-di

Mallen innehåller samma som vi gjort i anax3 i denna artikel, bortsett från integrationen med REM servern.

#Avslutningsvis

Vi har gått igenom hur Anax jobbar med konceptet dependency injection och vi har sett en modul som löser DI kontainern enligt PSR-11. Samtidigt har vi sett hur REM serverns kontroller och modell anpassats till DI-konceptet.

Denna artikel har en egen forumtråd som du kan ställa frågor i, eller bidra med tips och trix.

#Revision history

  • 2017-08-11: (A, mos) Första utgåvan.

Document source.

Category: anax, remserver, php, kursen ramverk1.