Pcms container in opbouw - 1
Een nadeel van de eerder beschreven Dependency Injection Container is dat hij nog steeds weinig flexibel is. Een nieuwe service toevoegen vereist een aanpassing in de Container klasse. Mooier zou het zijn als een ander object de container zou kunnen configureren. De container moet daarvoor dynamisch worden. De services moeten via calls geconfigureerd worden.
Een mooi simpel voorbeeld van zo'n dynamische container is Pimple. Omdat ik hem iets te simpel vind, hij niet mooi om kan gaan met uitbreidingen op services en ik toch wel een beetje narcistisch ben, zal ik de container bespreken die ik geschreven heb als basis van mijn nog te voltooien (mocht dat lukken) cms, genaamd Pcms :). (Een koekje voor degene die kan bedenken hoe ik op die naam ben gekomen)
Ik zal nu stapje voor stapje bespreken hoe ik tot het eindresultaat ben gekomen.
De kern is als volgt:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Container
{
protected $values;
public function set($key, $value)
{
$this->values[$key] = $value;
}
public function get($key)
{
// Check of de waarde bestaat
if(!isset($this->values[$key]))
throw new \InvalidArgumentException(sprintf(
"Value %s has not been set",
$key
));
$value = $this->values[$key];
// Als het om een service gaat
if(is_callable($value))
return $value($this);
// Als het om een parameter gaat
else
return $value;
}
}
?>
De container bevat een array met waarden. Met de set() functie kunnen die ingesteld worden. Twee typen waarden zijn toegestaan:
- Parameters
Dit zijn gewoon waarden zoals string, ints en arrays, die als configuratie voor de services dienen. Ze worden in de get() functie gewoon onveranderd teruggegeven.
- Factories
Dit zijn php callables of callbacks. Oftwel waarden die uitgevoerd kunnen worden met $var(). Dit kan een string met de naam van een functie zijn, een object dat __invoke() implementeerd of een array met ($object, 'methodenaam') en nog een paar mogelijkheden. Ik gebruik vooral de Closure oftewel anonymous function.
$var = function() { return 'test'; };
echo $var(); // geeft: test
Zie voor de details bijvoorbeeld hier.
De closure is dan hier een factory, een methode die bedoeld is om een object aan te maken en ziet er als volgt uit:
Maar een factory kan ook het object configureren. Het eerder beschreven Zend_Mail object krijg je zo:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$factory = function() {
$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => 'foo',
'password' => 'bar',
'ssl' => 'ssl',
'port' => 465,
));
$mailer = new Zend_Mail();
$mailer->setDefaultTransport($transport);
return $mailer;
};
$mailer = $factory();
?>
Het is dus mogelijk om zo'n factory bij set() mee te geven, bijvoorbeeld zo:
2
3
4
5
6
7
$container = new Container();
$container->set('mailer', function() {
return new Zend_Mailer();
};
$mailer = $container->get('mailer');
?>
In de get() functie wordt $this als argument meegegeven aan de factory. Deze beschikt dus over de container en kan zo beschikken over andere services of parameters.
Het volledige Zend_Mail object kan dan zo gemaakt worden:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$c = new Container();
// Stel de parameters in
$c->set('mailer.username', 'foo');
$c->set('mailer.password', 'bar');
$c->set('mailer.class', 'Zend_Mail');
// Stel de transport service in
$c->set('mailer.transport', function($c) {
return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => $c->get('mailer.username'),
'password' => $c->get('mailer.password'),
'ssl' => 'ssl',
'port' => 465,
));
});
// Stel de mailer service in
$c->set('mailer', function($c) {
$class = $c->get('mailer.class');
$mailer = new $class();
$mailer->setDefaultTransport($c->get('mailer.transport'));
return $mailer;
});
// De mailer roep je dan zo aan:
$mailer = $c->get('mailer');
?>
Merk ook op dat de services 'lazy' gemaakt worden. De factory wordt pas aangeroepen in de get() functie en dus pas wanneer de service nodig is.
Nu kan je gedeelde serivices implementeren op dezelfde wijze als in de hardcoded container:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Container
{
protected $values = array();
protected $shared = array();
protected $instances = array();
public function set($key, $value, $shared = false)
{
$this->values[$key] = $value;
$this->shared[$key] = $shared;
}
public function get($key)
{
if(!isset($this->values[$key]))
throw new \InvalidArgumentException(sprintf(
"Value %s has not been set",
$key
));
$value = $this->values[$key];
// Als het een service betreft
if(is_callable($value)) {
// Als de service gedeeld is en al eerder opgebouwd
if($this->shared[$key] && isset($this->instances[$key]))
return $this->instances[$key];
// Creƫer de service door de factor aan te roepen
$instance = $value($this);
// Sla gedeelde services op
if($this->shared[$key])
$this->instances[$key] = $instance;
return $instance;
// Als het een parameter betreft
} else {
return $value;
}
}
// Maak een gedeelde service niet-gedeeld of vice versa
public function setShared($key, $shared)
{
$this->shared[$key] = $shared;
}
}
?>
Een gedeelde service maak je dan zo:
2
3
4
5
6
$c = new Container();
$c->set('mailer', function() {
return new Zend_Mailer();
}, true);
?>
Inhoudsopgave
- Inleiding
- Dependency Injection
- Dependency Injection Container
- Pcms container in opbouw - 1
- Pcms container in opbouw - 2
- De Pcms container
- Conclusie