PHP Tutorial.nl

Word een PHP expert!!!

Dependencies: Leer hoe je omgaat met afhankelijkheden

Sommige objecten zijn afhankelijk van andere objecten. Zo heeft een telefoon bijvoorbeeld een sim-kaart nodig om te kunnen bellen. In code bestaan deze afhankelijkheden ook, we noemen deze ook wel dependencies.

Voorbeeld: we hebben een tweetal classes MedewerkerLijst en MedewerkerExporter. De exporteer class pakt de medewerkerlijst en exporteert deze lijst vervolgens naar formaat x. We kunnen de MedewerkerExporter class als volgt inrichten:

class MedewerkerExporter {
  
  protected $lijst;

  public function __construct() {
    $this->lijst = new MedewerkerLijst();
  }

  public function exporteer() {
    foreach( $this->lijst->get() as $medewerker ) {
       // Doe wat medewerker logica...
    } 

    // Exporteren naar XML.
  }
}

In het bovenstaande voorbeeld is de MedewerkerLijst een dependency van de class MedewerkerExporter. Het voorbeeld werkt prima, alleen het is niet herbruikbaar.

Dependency injection

We kunnen de lijst bijvoorbeeld ook meegeven aan de constructor. Dit heet dependency injection.  Bij dit principe hoeft een object niet per se meegegeven worden aan de constructor, het mag natuurlijk ook een afzonderlijke methode zijn.

Als iets als parameter wordt meegegeven, dan spreken we van dependency injection. Dus het kan ook een string zijn. 

Het voordeel van dependency injection is dat je een bestaand object kan meegeven. Zo kan de MedewerkerLijst een voegToe methode hebben om zo een medewerker toe te voegen.

<?php
$medewerkerLijst = new MedewerkerLijst();
$medewerkerLijst->voegToe( 'Andy' );

$exporteer = new MedewerkerExporter( $medewerkerLijst ); 
$exporteer->exporteer();

In theorie kunnen we nu elke lijst exporteren. Zolang het meegegeven object de gewenste methodes heeft die nodig zijn in de MedewerkerExporter class, dan gaat het goed. 

Wat nu als we ook de inventaris willen exporteren? We kunnen dan een InventarisLijst en een InventarisExporter maken. Voor het lijst stukje zal misschien iets andere code gebruikt worden, maar een groot deel van de exporter is waarschijnlijk hetzelfde.

  public function exporteer() {
    foreach( $this->lijst->get() as $inventaris ) {
       // Doe wat inventaris logica...
    } 

    // Exporteren naar XML.
  }

Gebruik interfaces

Voor het schrijven van herbruikbare code kunnen we interfaces gebruiken. We kunnen het beste kijken naar welke patronen onze code heeft. In ons geval willen we een lijst exporteren: de ene keer is het inventaris, de andere keer zijn het medewerkers. We hebben dus een lijst interface nodig.

interface Lijst {
   public function get();

   public function voegToe( $item );
}

Onze exporteer class wordt generieker, dus die kunnen we gewoon Exporter noemen.

class Exporter {
  
  protected $lijst;

  public function __construct( $lijst ) {
    $this->lijst = $lijst;
  }

  public function exporteer() {
    foreach( $this->lijst->get() as $item ) {
       // Doe wat item logica...
    } 

    // Exporteren naar XML.
  }
}

Interface specifiek afdwingen

Nu kunnen we feitelijk alles exporteren. In de meeste gevallen weten we dat ook wel juist omdat we de code zelf schrijven.  Maar we willen eigenlijk afdwingen dat de lijst die de Exporter krijgt wel van het juiste type is.

We kunnen het variabele type afdwingen. Dit heet type declaration, ook wel bekend als type hinting

  public function __construct( Lijst $lijst ) {
    $this->lijst = $lijst;
  }

Bij de parameter geven we gewoon de naam van de interface. Als de class bij het instantiëren iets anders krijgt, dan zal PHP een fout geven. 

We kunnen type hinting geven voor verschillende datatypes. Het mag een interface, abstracte class of gewone class zijn. Maar ook array en self zijn toegestaan. Vanaf PHP 7.0 is het ook mogelijk om callable, bool, float, int, iterable (7.1) en string als type hinting te geven. 

Types van dependency injection

Er zijn verschillende manieren om een dependency te injecteren.:

  • Constructor Injection: De dependency wordt meegegeven in de constructor.
  • Setter Injection: Dependency wordt via een setter-methode ingesteld.
  • Interface Injection: De dependency wordt ingesteld via een interface.

Constructor Injection

Bij een constructor injection wordt de dependency via de constructor meegegeven.

class MedewerkerExporter {
  
  public function __construct(protected MedewerkerLijst $lijst) {
  
  }

  public function exporteer() {
    foreach( $this->lijst->get() as $medewerker ) {
       // Doe wat medewerker logica...
    } 

    // Exporteren naar XML.
  }
}

Setter Injection

Bij setter injection wordt de dependency via een setter-methode ingesteld.

class MedewerkerExporter {
    protected $lijst;

    public function setLijst($lijst) {
        $this->lijst = $lijst;
    }

    public function exporteer() {
        foreach($this->lijst->get() as $medewerker) {
            // Doe wat medewerker logica...
        }
        // Exporteren naar XML.
    }
}

$exporter = new MedewerkerExporter();
$exporter->setLijst(new MedewerkerLijst());
$exporter->exporteer();

Interface Injection

Interface injection houdt in dat de dependency via een interface wordt ingesteld, wat minder gebruikelijk is in PHP.

interface Lijst {
    public function get();
}

interface Exporter {
    public function setLijst(Lijst $lijst);
}

class MedewerkerExporter implements Exporter {
    protected $lijst;

    public function setLijst(Lijst $lijst) {
        $this->lijst = $lijst;
    }

    public function exporteer() {
        foreach($this->lijst->get() as $medewerker) {
            // Doe wat medewerker logica...
        }
        // Exporteren naar XML.
    }
}

$lijst = new MedewerkerLijst();
$lijst->voegToe('Andy');

$exporter = new MedewerkerExporter();
$exporter->setLijst($lijst);
$exporter->exporteer();

Voordelen van dependency injection

  1. Losse koppeling: Vermindert afhankelijkheden tussen klassen, wat het hergebruik en testen van code vergemakkelijkt.
  2. Testbaarheid: Gemakkelijker om mock-objecten in te voeren voor unit testing.

Nadelen van dependency injection

  1. Complexiteit: Kan de complexiteit van de codebasis vergroten.
  2. Configuratie: Vereist zorgvuldige configuratie en beheer van dependencies.

Dependency injection containers

Een Dependency Injection Container helpt bij het automatisch beheren en injecteren van dependencies. Het maakt de configuratie eenvoudiger en zorgt voor een betere organisatie van de code.

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();
$container
    ->register('medewerker_lijst', 'MedewerkerLijst');

$container
    ->register('medewerker_exporter', 'MedewerkerExporter')
    ->addArgument(new Reference('medewerker_lijst'));

$exporter = $container->get('medewerker_exporter');
$exporter->exporteer();