PHP5: Debuggen met Magic Methods
Iemand kwam van de week naar me toe met een probleempje. Deze persoon had een object waarop een eigenschap gewijzigd werd, maar hij kon - omdat het niet zijn eigen code was - niet zo 1-2-3 vinden waar dat gebeurde. Scriptorama to the rescue :-) !
Ik raadde hem aan om een nieuwe lege klasse aan te maken met een paar zogenaamde magic methods om zo de toegang tot de eigenschappen te kunnen uitlezen. Dat was -al zeg ik het zelf - een aardig idee, maar zou stranden op het moment dat het object eerst de nodige logica dient uit te voeren voordat de eigenschap in kwestie veranderd werd.
Dit artikel legt uit wat magic methods zijn en laat zien hoe we ze kunnen gebruiken om het probleem op te lossen.
Wat zijn de magic methods?
Een leuke naam 'magic methods', maar wat zijn het? Magic methods zijn methodes die je kunt implementeren in elke klasse maar welke dan een speciaal stukje functionaliteit bieden. De magic methods waar wij het vandaag over hebben zijn __call(), __get() en __set(). Dit zijn drie magic methods die te maken hebben met het aanroepen van methodes en het wijzigen/uitlezen eigenschappen van een object.
De eerste, __call(), wordt aanroepen op het moment dat er een niet bestaande methode wordt aangeroepen op een object. Op het moment dat de methode niet bestaat, en de __call() methode is geimplementeert in het object lust PHP de aanroep dus door naar de __call() methode. __call() krijgt 2 argumenten: de methode naam en een array met de argumenten.
Dit principe wordt bijvoorbeeld gebruikt in SOAP en XMLRPC implementaties. De functies die een SOAP webservice aanbiedt kun je altijd direct aanroepen op de klasse SoapClient ,terwijl deze methodes niet geimplementeerd zijn in die klasse.
De andere magic methods, __get() en __set() worden aangeroepen op het moment dat een eigenschap van een object wordt opgevraagd of gewijzigd die op dat moment nog niet bestaat. __get() wordt aangeroepen voor het uitlezen van een eigenschap, en krijgt de naam van de eigenschap mee. __set() wordt aangeroepen op het moment dat een eigenschap wordt geschreven die op dat moment niet bestaat en krijgt de naam van de eigenschap en de opgegeven waarde mee.
-
__call($methodName, $methodArguments);
-
__get($propertyName);
-
__set($propertyName, $propertyValue);
Dit zijn niet de enige magic methods. Het complete overzicht:
- __call($methodName, $methodArguments)
- Wordt aangeroepen op het moment dat een niet bestaande methode wordt aangeroepen. Zou evt. een functie resultaat moeten terug geven.
- __get($propertyName)
- Wordt aangeroepen op het moment dat een niet bestaande eigenschap wordt opgevraagd. Zou de juiste waarde moeten terug geven.
- __set($propertyName, $propertyValue)
- Wordt aangeroepen op het moment dat een niet bestaande eigenschap wordt gewijzigd
- __sleep()
- Wordt aangeroepen op het moment dat het object wordt geserialized. Dit is ook wanneer een object in een sessie geplaatst wordt. Moet een array met eigenschap namen terug geven die in de serializatie meegenomen moeten worden
- __wakeup
- Wordt aangeroepen op het moment dat het object ge-unserialized() is. Dit is ook wanneer een object weer ingeladen wordt bij gebruik in een sessie.
- __clone()
- Wordt aangeroepen op het moment dat een object expliciet wordt gevraagd zichzelf te klonen dmv. de clone
- __isset($propertyName)
- Wordt aangeroepen op het moment dat er getest wordt of een bepaalde eigenschap bestaat in het object.
- __unset($propertyName)
- Wordt aangeroepen op het moment dat een niet bestaande eigenschap ge-unset() wordt op het object.
- __toString()
- Wordt aangeroepen op het moment dat het object in een string-context gebruikt wordt. Oftewel, wordt aanroepen wanneer je een object echo'ed of gebruikt in een string of cast naar een string. Moet een string representatie van het object terug geven.
- __set_state()
- Wordt gebruikt in combinatie met var_export(). var_export() exporteert een variabele als PHP code. Wanneer je een klasse door var_export() haalt zal deze code een (statische) aanroep naar __set_state() bevatten. In deze aanroep worden alle publieke eigenschappen meegegeven zodat een nieuw object geinitialiseerd kan worden op het moment dat de genereerde code weer uitgevoerd wordt.
- __callStatic($methodName, $methodArguments)
- Nieuw in PHP 5.3: de statische tegenhanger van __call(). Wordt aangeroepen op het moment dat een niet bestaande statische methode wordt aangeroepen op een klasse.
- __invoke($arg1, $arg2, ...)
- Nieuw in PHP 5.3: wordt gebruikt om een object direct als 'functie' te kunnen gebruiken. Lees meer in PHP 5.3: Lambda functies en closures.
Debuggen met magic methods
Even terug naar het probleem. We willen graag weten waar een bepaalde eigenschap vanaf werd benaderd. Om toegang tot een willekeurige eigenschap te kunnen afvangen kunnen we dus de 2 magic methods __get() en __set() implementeren maar dan hebben we nog 1 probleem: deze magic methods worden alleen aangeroepen op het moment dat de eigenschap niet bestaat en de eigenschap bestaat dus wel al.
Dit kunnen we oplossen door een losse klasse te maken die fungeert als doorgeef luik, maar welke ondertussen even bijhoudt waar elke eigenschap werd aangesproken:
-
<?php
-
-
/**
-
* Trace publieke aanroepen naar methodes en properties op een object
-
*
-
* @author Mathieu Kooiman <mathieu@scriptorama.nl>
-
* http://www.scriptorama.nl/
-
*/
-
-
class ObjectAccessTracer
-
{
-
const ACCESS_TYPE_METHOD = 'method';
-
const ACCESS_TYPE_PROPERTY = 'property';
-
-
private $_subject = NULL;
-
-
public function __construct($subject)
-
{
-
$this->_subject = $subject;
-
}
-
-
private function _get_call_location($type)
-
{
-
$depth = ($type == self::ACCESS_TYPE_METHOD ? 2 : 1);
-
-
for ($i = 0; $i <$depth; $i++)
-
-
-
return $call_data['file'] . ':' . $call_data['line'];
-
}
-
-
public function getTrace()
-
{
-
return $this->_traces;
-
}
-
-
public function __call($target, $args)
-
{
-
$call = $this->_get_call_location(self::ACCESS_TYPE_METHOD);
-
$this->_traces[] = array ('type' => 'method_call', 'name' => $target, 'args' => $args, 'call' => $call);
-
}
-
-
public function __set($target, $value)
-
{
-
$call = $this->_get_call_location(self::ACCESS_TYPE_PROPERTY);
-
-
$this->_traces[] = array('type' => 'set_property', 'name' => $target, 'value' => $value, 'call' => $call);
-
$this->_subject->$target = $value;
-
}
-
-
public function __get($target)
-
{
-
$call = $this->_get_call_location(self::ACCESS_TYPE_PROPERTY);
-
-
$this->_traces[] = array('type' => 'get_property', 'name' => $target, 'value' => ($this->_subject->$target), 'call' => $call);
-
return $this->_subject->$target;
-
}
-
}
ObjectAccessTracer vereist bij z'n constructor een object $subject. Dit is het object waarnaar alle operaties uiteindelijk doorgestuurd moeten worden. Bij elke aanroep worden er gegevens verzameld, zoals om welke methode of eigenschap het gaat en doo rmiddel van een aanroep naar ObjectAccessTracer::_get_call_location() wordt bepaald vanaf waar dit alles werd aangeroepen.
Om de aanroep locatie te bepalen gebruiken we debug_backtrace() (zie ook Stacktraces in PHP). Om te bepalen waar de aanroep vandaan kwam moeten we voor eigenschappen 1 stap terug in de trace terwijl we voor methode aanroepen 2 stappen terug moeten.
Nadat de gegevens in ObjectAccessTracer zijn opgeslagen kunnen we de aanroep doorsturen naar de bedoelde ontvanger. Voor een methode aanroep gebruiken we call_user_func_array() (zie ook Dynamisch functies aanroepen en Callbacks in PHP) en voor properties gebruiken we gewoon variabele variabelen.
We kunnen nu met ObjectAccessTracer een andere klasse omvatten en een rapportje genereren over waar welke eigenschap vanaf werd benaderd:
-
class MijnTestKlasse
-
{
-
public $mijnEigenschap;
-
-
public function hello($something)
-
{
-
}
-
}
-
-
$testCls = new ObjectAccessTracer(new MijnTestKlasse);
-
-
$world = 'World';
-
-
$testCls->hello($world);
-
$testCls->mijnEigenschap = 'Scriptorama.nl';
-
-
foreach ( $testCls->getTrace() as $trace )
-
{
-
}
Dit script geeft dan het resultaat:
-
test.php:79 - method_call - hello - array ( 0 => 'World',)
-
test.php:80 - set_property - mijnEigenschap - 'Scriptorama.nl'
Conclusie & Beperkingen
De ObjectAccessTracer klasse laat leuk zien wat er mogelijk is met magic methods, maar als debugging faciliteit heeft het wel zo z'n beperkingen.
Zo kan ObjectAccessTracer alleen maar aanroepen afvangen die van buiten het object ($subject) worden gedaan. De aanroepen die de klasse intern doet lopen niet via ObjectAccessTracer.
Er is ook nog een tweede, wat belangrijker probleem. ObjectAccessTracer valt niet onder de inheritance tree van z'n $subject. Dit houdt in dat als je streng controleert of een bepaalde klasse wel van een bepaald type is voordat je het gebruikt je stuk loopt op het gebruik van ObjectAccessTracer.
-
$testCls = new ObjectAccessTracer(new MijnTestKlasse);
-
-
echo $testCls instanceof MijnTestKlasse ?
-
"Instance van MijnTestKlasse" :
-
"Geen instance van MijnTestKlasse";
-
-
// Geen instance van MijnTestKlasse
Dat kan vervelend zijn. Er is wel om heen te werken door bijv. een gedeelde lege interface te implementeren of van een gedeelde lege klasse over te erven, maar waarschijnlijk is het op dat punt verstandiger om je eens in de geavanceerdere features (specifiek: conditional breakpoints) van debuggers te verdiepen.
Volg Scriptorama via RSS!
Reageer ook!
Ziet er goed uit dit artikel. Had me laatst er al een beetje in zitten verdiepen :)
Door Marten
op 03.09.08 @ 1:30 pm | Permalink
Conditional breakpoints? *gaat aan de google*
Door Alexander
op 03.09.08 @ 2:31 pm | Permalink
Mooi artikel! Je noemt het niet bij naam, maar in feite is dit ook een mooi voorbeeld van de toepassing van het 'proxy' design pattern.
Door Ivo Jansch
op 03.10.08 @ 8:48 am | Permalink
Thanks!
Door Mathieu Kooiman
op 03.10.08 @ 9:19 am | Permalink
Inderdaad, mooi artikel. Het is natuurlijk een kwestie van logica, maar je moet het maar bedenken :)
Door berry__
op 03.13.08 @ 3:18 pm | Permalink
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>