Scriptorama.nl

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

PHP 5.3: Late static binding

In mijn overzicht van de verschillende nieuwe features in PHP 5.3 zijn we al langs namespaces, verbeterde DateTime functionaliteit en de nieuwe lambda en closure functies geweest. Vandaag bekijken we een van de laatste grote toevoegingen aan de taal PHP in PHP 5.3: Late Static Binding.

Wat is Late Static Binding?

Late Static Binding is een oplossing voor een 'probleem' dat sinds PHP 5 bestaat. Op het moment dat je statische methoden gebruikt was PHP niet helemaal bewust van eventuele inheritance die van toepassing is. Daardoor is het lastig te bepalen waar een aanroep nu precies vandaan komt en dat beperkte de nuttigheid van statische methodes.

In PHP 5.2 zou je bijvoorbeeld de volgende code kunnen hebben:

PHP:
  1. class A {
  2.     public static function test() {
  3.         return __CLASS__;
  4.     }
  5. }
  6.  
  7. class B extends A {  }
  8.  
  9. echo B::test(); // A

Hier is het antwoord A en niet B zoals misschien wel gewenst was. Late Static Binding brengt hier verandering in. Door wijzigingen in manier waarop gegevens worden doorgegeven is het mogelijk te bepalen welke class een methode heeft aangeroepen. Hiervoor gebruiken we de nieuwe functie get_called_class():

PHP:
  1. class A {
  2.     public static function test() {
  3.         return get_called_class();
  4.     }
  5. }
  6.  
  7. class B extends A {  }
  8.  
  9. echo B::test(); // B

Late Static Binding forward de aanroep informatie, die vervolgens door het static:: keyword en de get_called_class() functie gebruikt kunnen worden. Er zijn 3 situaties waarin forwarding plaats vindt:

  1. Forwarding vindt automatisch plaats als een aangeroepen methode niet gedefinieerd staat in de Child klasse, maar wel in de Parent klasse.

  2. Forwarding vindt plaats op het moment dat je een keyword gebruikt om naar een parent te refereren. Het gaat hier om de keywords parent en self.

  3. Tenslotte vindt forwarding plaats als je de functies forward_static_call() of forward_static_call_array() gebruikt. Deze sturen de aanroep informatie expliciet door. Met forward_static_call_array() kun je ook nog een array met argumenten doorgeven.

Forwarding vindt niet plaats op het moment dat je een parent expliciet, daarmee bedoel ik aan de hand van z'n klassenaam, aanroept vanuit een Child. Een code voorbeeld van elke situatie:

PHP:
  1. <?php
  2.  
  3. class A {
  4.     public static function WaarKomIkVandaan()
  5.     {
  6.         return get_called_class();
  7.     }
  8. }
  9.  
  10. class B extends A {
  11.     public static function WaarKomIkVandaan()
  12.     {
  13.         return parent::WaarKomIkVandaan();
  14.     }
  15. }
  16.  
  17. class C extends A {
  18.     public static function WaarKomIkVandaan()
  19.     {
  20.         return A::WaarKomIkVandaan();
  21.     }
  22. }
  23.  
  24. class D extends A {
  25.     public static function WaarKomIkVandaan()
  26.     {
  27.         return forward_static_call(array('parent', 'WaarKomIkVandaan'));
  28.     }
  29. }
  30. class E extends A { }
  31.  
  32. echo A::WaarKomIkVandaan(); // Geen forwarding nodig, resultaat: A
  33. echo B::WaarKomIkVandaan(); // Forwarding via parent keyword, resultaat: B
  34. echo C::WaarKomIkVandaan(); // Geen forwarding vanwege parent aanroep, resultaat: A
  35. echo D::WaarKomIkVandaan(); // Forwarding via forward_static_call(), resultaat: D
  36. echo E::WaarKomIkVandaan(); // Forwarding doordat WaarKomIkVandaan niet in E gedefinieerd staat, resultaat: E

Het gebruik van het static keyword

Dat is niet het hele verhaal, want je kunt vanuit een parent klasse ook weer 'terug' naar een child klasse en dat kan middels het static keyword dat ik net al aanhaalde. Het keyword static, dat we al gebruiken in de definitie van static variabelen en static methodes, zal als er forwarding informatie beschikbaar is, ook een referentie zijn naar de klasse die de aanroep heeft gedaan:

PHP:
  1. class A {
  2.         public $mijnNaam;
  3.         public function __construct ($naam) {
  4.                 $this->mijnNaam = $naam;
  5.         }
  6.         public static function bouw()
  7.         {
  8.                 return new static(static::$naam);
  9.         }
  10. }
  11.  
  12. class B extends A {
  13.         static $naam = 'Ik ben class B';
  14. }
  15.  
  16. class C extends A {
  17.         static $naam = 'Ik ben class C';
  18.  
  19. }
  20.  
  21. $instance = B::bouw();
  22. echo $instance->mijnNaam, "\n"; // Ik ben class B
  23.  
  24. $instance = C::bouw();
  25. echo $instance->mijnNaam, "\n"; // Ik ben class C

Je ziet hier in A::bouw() twee manieren waarop we met het static keyword terug refereren naar de aanroepende klasse. Ten eerste gebruiken we static::$naam om een statische variabele uit te lezen uit de aanroepende klasse en daarna gebruiken we new static() om een nieuwe instantie aan te maken van de aanroepende klasse.

Kortom, hoewel A geen idee heeft welke exacte klasse hem nou heeft aangeroepen, middels het static keyword is hij toch in staat er iets mee te doen.

Hét LSB voorbeeld: ActiveRecord

Allemaal leuk en aardig maar met simpele klassenamen als B en C wordt het nut nog niet echt duidelijk. Een grote motivatie voor het implementeren van late static binding en daarom tevens hét voorbeeld van Late Static Binding lijkt het ActiveRecord design pattern te zijn.

Het ActiveRecord is een design pattern waarbij 1 object gelijk staat aan 1 rij uit een tabel. Daarbij biedt het object alle functionaliteiten aan om het record verder te bewerken en verwerken. Met Late Static Binding, ben je in staat om alle benodigde functionaliteit te definieëren in de ActiveRecord class, terwijl je voor elke tabel enkel een klasse hoeft te maken die ervan extend:

PHP:
  1. class ActiveRecord
  2. {
  3.     protected $_data;
  4.    
  5.     public function __construct($data = array())
  6.     {
  7.         $this->_data = $data;
  8.     }
  9.        
  10.     public function __set($field, $value)
  11.     {
  12.         if (!in_array($field, $this->_columns))
  13.             throw new Exception("Unknown column: $field");
  14.     }
  15.    
  16.     public function __get($field)
  17.     {
  18.         if (empty($this->_data))
  19.             throw new Exception("No record of " . get_class($this) . " loaded.");
  20.        
  21.         if (!in_array($field, $this->_columns))
  22.             throw new Exception("Unknown column: $field");
  23.            
  24.            
  25.         return $this->_data[$field];       
  26.     }
  27.    
  28.     public static function getById($id)
  29.     {
  30.         $db = Registry::get('db');
  31.  
  32.         $tableName = get_called_class();
  33.         $query = "SELECT * from {$tableName}s WHERE ID = $id";
  34.        
  35.         $res  = $db->query($query);
  36.         $data = $db->fetch_assoc($res);
  37.        
  38.         return new static($data);
  39.     }
  40.    
  41.     public function save() { /* [...] */ }
  42.     public function delete() { /* [...] */ }
  43. }
  44.  
  45. class User extends ActiveRecord {
  46.     protected $columns = array(
  47.         'UserID',
  48.         'UserName',
  49.         'Emailaddress'
  50.     );
  51. }
  52.  
  53. $user = User::getByID(10);
  54. echo $user->UserName; // De Username
  55. echo $user->Password// Exception: unknown column

De belangrijkste methode uit het voorbeeld is natuurlijk ActiveRecord::getById(). Deze static methode gebruikt de verschillende features die late static binding biedt om een generieke getById() functionaliteit te bieden, dus zonder expliciete kennis van de tabelnamen te hebben.

Ten eerste gebruik ik get_called_class() om te bepalen welke klasse de aanroep deed. Het resultaat daarvan gebruik ik vervolgens in de SQL query waar ik ook nog een 's' achter de tabel naam plaats. Een kleine conventie: een User vinden we in de tabel Users. Als we dan de gegevens uit de Database hebben gebruik ik het static keyword om een nieuwe instantie te maken van de aanroepende klasse met de data uit de database.

De klasse ActiveRecord is standalone niet nuttig, dus we zullen per tabel een klasse moeten maken, die volgens onze conventie de tabelnaam definieert. Die zie je hierboven als de User klasse. Het enige wat we hoeven te doen voor deze klasse is overerven van ActiveRecord, de columns te definieëren zodat de magic methods hun werk kunnen doen, en dan hebben we alle functionaliteit binnen handbereik:

PHP:
  1. $user = User::getByID(10);
  2. echo $user->UserName; // De Username
  3.  
  4. $user->UserName = 'Joost';
  5. $user->save();

Conclusie

Late Static Binding voegt wat missende functionaliteit toe wanneer je gebruik maakt van static aanroepen. Hoewel het misschien even lastig is om helemaal te begrijpen waarom dit nou nuttig is, iets waar ik zeker last van had, komt dat vanzelf als je er even mee speelt. Vooral het automatische forwarding via parent voelt alsof het er altijd al in had moeten zitten :).

Reageer ook!

dit is nou echt iets wat ik echt nodig heb, ik werd helemaal gek ervan!

Volgens mij werd heel php'end Nederland (en de rest van de wereld) hier inderdaad echt knettergek van! Ik heb zelf een half jaar geleden alle toeters en bellen uit de kast gehaald die ik maar kon verzinnen om onze database objecten te optimaliseren, vreselijk!

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>