PHP
Réflexion
Recherche…
Accéder aux variables membres privées et protégées
La réflexion est souvent utilisée dans le cadre de tests de logiciels, par exemple pour la création / instanciation à l'exécution d'objets fictifs. C'est également idéal pour inspecter l'état d'un objet à un moment donné. Voici un exemple d'utilisation de Reflection dans un test unitaire pour vérifier qu'un membre de classe protégé contient la valeur attendue.
Ci-dessous, une classe très basique pour une voiture. Il possède une variable membre protégée qui contiendra la valeur représentant la couleur de la voiture. Comme la variable membre est protégée, nous ne pouvons pas y accéder directement et utiliser une méthode getter et setter pour récupérer et définir respectivement sa valeur.
class Car
{
protected $color
public function setColor($color)
{
$this->color = $color;
}
public function getColor($color)
{
return $this->color;
}
}
Pour tester cela, de nombreux développeurs vont créer un objet Car, définir la couleur de la voiture en utilisant Car::setColor()
, récupérer la couleur en utilisant Car::getColor()
et comparer cette valeur à la couleur qu'ils définissent:
/**
* @test
* @covers \Car::setColor
*/
public function testSetColor()
{
$color = 'Red';
$car = new \Car();
$car->setColor($color);
$getColor = $car->getColor();
$this->assertEquals($color, $reflectionColor);
}
En apparence, cela semble aller. Car::getColor()
renvoie la valeur de la variable membre protégée Car::$color
. Mais ce test est défectueux de deux manières:
- Il exerce
Car::getColor()
qui est hors de la portée de ce test - Cela dépend de
Car::getColor()
qui peut avoir un bogue lui-même qui peut avoir un faux positif ou négatif
Voyons pourquoi nous ne devrions pas utiliser Car::getColor()
dans notre test unitaire et devrions plutôt utiliser Reflection. Disons qu'un développeur se voit attribuer une tâche pour ajouter "Métallisé" à chaque couleur de voiture. Donc, ils tentent de modifier le Car::getColor()
pour ajouter "Metallic" à la couleur de la voiture:
class Car
{
protected $color
public function setColor($color)
{
$this->color = $color;
}
public function getColor($color)
{
return "Metallic "; $this->color;
}
}
Voyez-vous l'erreur? Le développeur a utilisé un point-virgule à la place de l’opérateur de concaténation pour tenter d’ajouter "Metallic" à la couleur de la voiture. Par conséquent, chaque fois que Car::getColor()
est appelée, "Metallic" sera renvoyé quelle que soit la couleur réelle de la voiture. En conséquence, notre test unitaire Car::setColor()
échouera même si Car::setColor()
fonctionne parfaitement et n’a pas été affecté par cette modification .
Alors, comment pouvons-nous vérifier que Car::$color
contient la valeur que nous définissons via Car::setColor()
? Nous pouvons utiliser Refelection pour inspecter directement la variable membre protégée. Alors comment on fait ça ? Nous pouvons utiliser Refelection pour rendre la variable membre protégée accessible à notre code afin qu'il puisse récupérer la valeur.
Voyons d'abord le code et ensuite le décomposons:
/**
* @test
* @covers \Car::setColor
*/
public function testSetColor()
{
$color = 'Red';
$car = new \Car();
$car->setColor($color);
$reflectionOfCar = new \ReflectionObject($car);
$protectedColor = $reflectionOfForm->getProperty('color');
$protectedColor->setAccessible(true);
$reflectionColor = $protectedColor->getValue($car);
$this->assertEquals($color, $reflectionColor);
}
Voici comment nous utilisons Reflection pour obtenir la valeur de Car::$color
dans le code ci-dessus:
- Nous créons un nouveau ReflectionObject représentant notre objet Car
- Nous obtenons un ReflectionProperty pour
Car::$color
(ceci "représente" la variable deCar::$color
) - On rend la
Car::$color
accessible - Nous obtenons la valeur de
Car::$color
Comme vous pouvez le voir en utilisant Reflection, nous pourrions obtenir la valeur de Car::$color
sans avoir à appeler Car::getColor()
ou toute autre fonction d'accesseur pouvant entraîner des résultats de test non valides. Maintenant, notre test unitaire pour Car::setColor()
est sûr et précis.
Détection de fonctionnalités de classes ou d'objets
La détection des fonctionnalités des classes peut être effectuée en partie avec les fonctions property_exists
et method_exists
.
class MyClass {
public $public_field;
protected $protected_field;
private $private_field;
static $static_field;
const CONSTANT = 0;
public function public_function() {}
protected function protected_function() {}
private function private_function() {}
static function static_function() {}
}
// check properties
$check = property_exists('MyClass', 'public_field'); // true
$check = property_exists('MyClass', 'protected_field'); // true
$check = property_exists('MyClass', 'private_field'); // true, as of PHP 5.3.0
$check = property_exists('MyClass', 'static_field'); // true
$check = property_exists('MyClass', 'other_field'); // false
// check methods
$check = method_exists('MyClass', 'public_function'); // true
$check = method_exists('MyClass', 'protected_function'); // true
$check = method_exists('MyClass', 'private_function'); // true
$check = method_exists('MyClass', 'static_function'); // true
// however...
$check = property_exists('MyClass', 'CONSTANT'); // false
$check = property_exists($object, 'CONSTANT'); // false
Avec une ReflectionClass
, des constantes peuvent également être détectées:
$r = new ReflectionClass('MyClass');
$check = $r->hasProperty('public_field'); // true
$check = $r->hasMethod('public_function'); // true
$check = $r->hasConstant('CONSTANT'); // true
// also works for protected, private and/or static members.
Remarque: pour property_exists
et method_exists
, un objet de la classe d'intérêt peut également être fourni à la place du nom de la classe. En utilisant la réflexion, la classe ReflectionObject
doit être utilisée à la place de ReflectionClass
.
Test de méthodes privées / protégées
Parfois, il est utile de tester les méthodes privées et protégées ainsi que les méthodes publiques.
class Car
{
/**
* @param mixed $argument
*
* @return mixed
*/
protected function drive($argument)
{
return $argument;
}
/**
* @return bool
*/
private static function stop()
{
return true;
}
}
La méthode la plus simple pour tester la méthode d'entraînement consiste à utiliser la réflexion
class DriveTest
{
/**
* @test
*/
public function testDrive()
{
// prepare
$argument = 1;
$expected = $argument;
$car = new \Car();
$reflection = new ReflectionClass(\Car::class);
$method = $reflection->getMethod('drive');
$method->setAccessible(true);
// invoke logic
$result = $method->invokeArgs($car, [$argument]);
// test
$this->assertEquals($expected, $result);
}
}
Si la méthode est statique, vous passez null à la place de l'instance de la classe
class StopTest
{
/**
* @test
*/
public function testStop()
{
// prepare
$expected = true;
$reflection = new ReflectionClass(\Car::class);
$method = $reflection->getMethod('stop');
$method->setAccessible(true);
// invoke logic
$result = $method->invoke(null);
// test
$this->assertEquals($expected, $result);
}
}