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:

  1. Il exerce Car::getColor() qui est hors de la portée de ce test
  2. 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:

  1. Nous créons un nouveau ReflectionObject représentant notre objet Car
  2. Nous obtenons un ReflectionProperty pour Car::$color (ceci "représente" la variable de Car::$color )
  3. On rend la Car::$color accessible
  4. 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);
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow