Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Lazy Initialization voor Zend_Registry

In de reacties op mijn review van PHP|Architect's guide to Programming with Zend Framework werd gereageerd door de schrijver van het boek, Cal Evans. Een van mijn punten van kritiek was dat de schrijver een heel hoofdstuk gebruikt om een eigen Globals class te introduceren terwijl hier naar mijn mening in Zend Framework al een klasse voor beschikbaar was, namelijk het Zend_Registry.

Uit zijn reactie blijkt dat zijn voornaamste reden voor zijn Globals klasse een principe genaamd Lazy Initialization te zijn. In dit artikel introduceer ik je aan het Zend_Registry en laat ik zien hoe je Lazy Initialization kunt toepassen met Zend_Registry.

Wat is Zend_Registry?

Zend_Registry is een klasse binnen het Zend Framework waarin je verschillende 'globale' variabelen kunt opslaan voor later gebruik. Om een waarde in het registry op te slaan gebruik je de statische methode Zend_Registry::set($key, $value) en om een waarde weer uit 't registry te plukken gebruik je Zend_Registry::get($key).

PHP:
  1. class MyController
  2. {
  3.     public function indexAction()
  4.     {
  5.         $model = new MyModel(Zend_Registry::get('db'));
  6.     }
  7. }
  8.  
  9. class MyModel
  10. {
  11.     private $_db;
  12.    
  13.     public function __construct($db)
  14.     {
  15.         $this->_db = $db
  16.     }
  17. }
  18.  
  19. $db = new PDO(/* ... */);
  20. Zend_Registry::set('db', $db);
  21.  
  22. /* Dit is niet de typische manier om een actie binnen een controller af te vuren, maar daar
  23. * gaat dit voorbeeld dan ook niet om :) */
  24.  
  25. $ctrl = new MyController();
  26. $ctrl->indexAction();

Ik gebruik Zend_Registry meestal om de grote componenten van een webapplicatie, zoals bijv. het database object, of een caching object in op te slaan voor later gebruik. Vervolgens pluk ik de waarde weer uit 't registry op een enigzins centrale plek (in dit geval de controller) zodat niet alle lagen van de applicatie direct gekoppeld zijn aan Zend_Registry.

Wat is lazy initialization?

Stel je voor dat je werkt aan een drukke website. Om de drukte aan te kunnen heb je caching in gebouwd en je hebt dat zó grondig gedaan dat bepaalde bezoeken de database totaal niet aanspreken. Er is nog maar 1 probleem: je legt nog steeds wel van te voren een database connectie, omdat het mogelijk is dat er érgens wel een query gedaan wordt. Dat is zonde, want je legt nu af en toe een connectie voor niks en dat houdt de database onnodig bezig. Enter Lazy Initialization.

Lazy Initialization houdt in dat je een bepaalde resource - of het nu een instantie van een klasse of bijv. een database connectie is - niet van te voren aanmaakt omdat je -denkt- dat je het nodig hebt, maar pas aanmaakt op het moment dat je het daadwerkelijk nodig hebt.

Lazy Initialization met Zend_Registry

Standaard biedt Zend_Registry niet de functionaliteit waar we naar op zoek zijn. Maar, na enig stoeien met het Zend Framework blijkt dat de betreffende klasse: Zend_Registry wel degelijk mogelijkheden biedt.

Het mooie aan Zend_Registry is dat je deze wel statisch aanspreekt maar dat er wordt intern gebruik gemaakt van een instantie van Zend_Registry, om zo de verschillende waarden te kunnen opslaan. Nog mooier is dat Zend_Registry het mogelijk maakt het interne object te overriden met een eigen object. Dit doe je met de statische methode Zend_Registry::setInstance(). Op deze manier biedt Zend_Registry ons een handvat om lazy initialization te implementeren binnen Zend_Registry.

Introducing SOR_Registry

We kunnen Zend_Registry::setInstance() gebruiken om nieuwe functionaliteit te introduceren aan Zend_Registry, zonder dat we de aanroepen naar Zend_Registry ook maar 1 letter hoeven te veranderen.

Zend_Registry is gebouwd op de SPL interface ArrayObject. ArrayObject maakt het mogelijk om objecten te benaderen alsof het een array is. Al deze benaderingen worden door de functies die de ArrayObject interface definieërt afgehandeld. Enkele gebruiksvoorbeelden vind je in de handleiding voor Zend_Registry. Dit betekent dus dat Zend_Registry enkele methodes implementeert die we eventueel kunnen overriden voor ons doel.

Een van die methodes is offsetExists(), een van de methodes uit ArrayObject, welke aangeroepen wordt wanneer er gekeken wordt of een bepaald element wel bestaat in de 'array'. Deze wordt ook altijd aangeroepen op het moment dat Zend_Registry::get() wordt aangeroepen dus we kunnen deze in een eigen klasse overriden om er zo eigen functionaliteit aan toe te voegen.

SOR_Registry.php:

PHP:
  1. <?php
  2.  
  3. class SOR_Registry extends Zend_Registry
  4. {
  5.     private $_initFuncs = array();
  6.    
  7.     public function registerInitFunc($key, $callback)
  8.     {
  9.         $this->_initFuncs[$key] = $callback;
  10.     }
  11.    
  12.     public function offsetExists($index)
  13.     {   
  14.         if (array_key_exists($index, $this))
  15.             return TRUE;
  16.             
  17.         foreach ($this->_initFuncs as $prefix => $callback)
  18.         {
  19.             if (strpos($index, $prefix) === 0)
  20.             {
  21.                 $arguments = array ($index);
  22.                 $result = call_user_func_array($callback, $arguments);
  23.            
  24.                 $this->offsetSet($index, $result);
  25.                
  26.                 return TRUE;
  27.             }
  28.         }
  29.        
  30.         return FALSE;
  31.     }
  32. }

Om dezelfde functionaliteit als Zend_Registry te bieden extend ik SOR_Registry vanaf Zend_Registry. Vervolgens doet SOR_Registry dan 2 dingen.

Ten eerste biedt SOR_Registry::registerInitFunc() je de mogelijkheid om een callback functie te registreren met een deel van een key. Op deze manier is het mogelijk om alle aanvragen voor keys die beginnen met bijvoorbeeld db. naar een functie initDBConnection laten gaan zodat deze aan de hand van de key een database connectie kan opbouwen.

Ten tweede implementeer ik een eigen versie van de methode offsetExists(); een methode die Zend_Registry zelf ook implementeert. Aangezien deze methode wordt aangeroepen voordat de bijpassende offsetGet() methode (ook uit ArrayObject) wordt uitgelezen is dit de ideale plek om de waarde te initialiseren.

In SOR_Registry::offsetExists() kijk ik eerst of de opgezochte key ($index) niet al gewoon bestaat in het registry en retourneer de waarde als dit het geval blijkt te zijn. Dit is overigens de code die ook in Zend_Registry gebruikt wordt, dus er is vrijwel geen overhead op het moment dat een bepaalde key al bestaat:

PHP:
  1. if (array_key_exists($index, $this))
  2.     return TRUE;

Als de code hier doorheen valt betekent dat dus dat de opgevraagde key nog niet bestaat in het registry. Om te kijken of de waarde alsnog ergens vandaan gehaald kan worden doorlopen we de geregistreerde callback functies en kijken we of de opgegeven $index misschien begint met een van de geregistreerde prefixes en roepen de bijhorende callback functie aan als dit het geval blijkt te zijn.

Als we een waarde terug krijgen van de callback functie, zetten we de waarde alsnog in het registry en geven we TRUE terug.

PHP:
  1. foreach ($this->_initFuncs as $prefix => $callback)
  2. {
  3.     if (strpos($index, $prefix) === 0)
  4.     {
  5.         $arguments = array ($index);
  6.         $result = call_user_func_array($callback, $arguments);
  7.  
  8.         $this->offsetSet($index, $result);
  9.        
  10.         return TRUE;
  11.     }
  12. }

Als er geen callback functie kan worden gevonden dan retourneren we FALSE wat aan de rest van Zend_Registry aangeeft dat er geen waarde in gevonden.

Een voorbeeld: Meerdere database connecties

Nu hoeven we het geheel alleen nog maar samen te laten komen in een script. In dit voorbeeld zal ik laten zien hoe je met Zend_Registry en SOR_Registry automatisch objecten kunt laten aanmaken op het moment dat je ze nodig hebt.

PHP:
  1. require 'sor_registry.php';
  2.  
  3. function initializeDbConnection($key)
  4. {
  5.     /* Zet de keys om naar een Zend_Config waarde. Een key 'db.conn1' proberen
  6.      * we uit te lezen als: $config->db->conn1 */
  7.     $config = Zend_Registry::get('config');
  8.     $keys = explode('.', $key);
  9.     
  10.     $tmp_obj = $config;
  11.     foreach($keys as $keypart)
  12.     {
  13.         if (is_object($tmp_obj) && isset($tmp_obj->$keypart))
  14.             $tmp_obj = $tmp_obj->$keypart;
  15.         else
  16.             throw new InvalidArgumentException("Config key '$key' was not found in the configuration file");
  17.     }
  18.     /* We hebben een config key gevonden, lees hem uit en probeer een DB connectie op te zetten */
  19.     $db = Zend_Db::Factory(
  20.         $tmp_obj->driver,
  21.         array (
  22.             'host'     => $tmp_obj->host,
  23.             'username' => $tmp_obj->username,
  24.             'password' => $tmp_obj->password,
  25.             'dbname'   => $tmp_obj->dbname
  26.         )
  27.     );
  28.      
  29.     return $db;
  30. }
  31. $config   = new Zend_Config_ini ( APP_ROOT . 'application/config/config.ini');
  32. Zend_Registry::set('config', $config);
  33.  
  34. $registry = new SOR_Registry();
  35. $registry->registerInitFunc('db.', 'initializeDbConnection');
  36.  
  37. Zend_Registry::setInstance($registry);
  38. Zend_Registry::get('db.conn1');

Laten we met het belangrijkste beginnen:

PHP:
  1. $registry = new SOR_Registry();
  2. $registry->registerInitFunc('db.', 'initializeDbConnection');
  3.    
  4. Zend_Registry::setInstance($registry);

In dit stuk instantieer ik eerst een nieuw object van de klasse SOR_Registry. Op dit object registreer ik een nieuwe initialiser functie dmv. SOR_Registry::registerInitfunc(). Daarin geef ik aan dat alle keys die niet bestaan en die beginnen met db. via de functie initializeDbConnection() moeten proberen toch nog een waarde te verkrijgen.

Als laatste vertel ik Zend_Registry met Zend_Registry::setInstance() dat ik de instantie van SOR_Registry wil gebruiken als opslag klasse. Vanaf dat moment zullen alle Zend_Registry::set() en Zend_Registry::get() calls via SOR_Registry lopen.

De functie initializeDbConnection() is een voorbeeld van hoe je een registry key ook kunt gebruiken om de juiste configuratie opties te vinden. Het voorbeeld verwacht een 'config.ini' bestand, met daarin (op z'n minst):

CODE:
  1. db.conn1.username = jouw_gebruikersnaam
  2. db.conn1.password = jouw_password
  3. db.conn1.dbname = jouw_database
  4. db.conn1.host = localhost
  5. db.conn1.driver = pdo_mysql

Nu wordt pas op het moment dat we Zend_Registry::get('db.conn1'); aanroepen een database connectie gelegd.

Conclusie

Het is belangrijk je te realiseren dat lazy initialization in het geval van database connecties wel een lastigheidje met zich mee brengt: iedere Zend_Registry::get() call kan eventueel een exceptie opleveren. Hier zul je dus rekening mee moeten houden.

Het Zend Framework biedt soms onverwachte flexibiliteit, zoals je hebt kunnen zien bij Zend_Registry. Kijk daarom altijd eerst even in de handleiding voordat je een eigen implementatie maakt van iets, wie weet kom je iets onverwachts tegen. I did.

Reageer ook!

Zo wow :D Go Zend!
Niet dat ik er gebruik van maakt, maar wel heel goed bedacht :)

Zoiets heb ik ook al eens gemaakt, maar op een iets andere manier. Zonder gebruik van het Registry, maar een Factory voor mijn database verbindingen.

Dit is wellicht handiger gezien je al je objecten netjes bij elkaar hebt staan en op dezelfde consistente manier kan ophalen.

Handig artikel, ik zal er zeker gebruik van maken als ik weer eens wat ga bouwen met ZF.

Trouwens, goed bezig, er komen best veel blogs de laatste tijd vind ik. En een leuke poll :)

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>