I. Prérequis

La version du Zend Framework 2 utilisée pour cet article est la 2.0.0RC02. L'utilisation du Zend Framework 2 nécessite au minimum la version 5.3.3 de PHP.

Zend Framework 2.0 RequirementsZend Framework 2.0 Requirements

II. Composant et classes standards

Le composant Zend\Stdlib est une bibliothèque de classes permettant de résoudre les problèmes récurrents que l'on rencontre sur les projets web. Ce composant se veut être complémentaire à la SPL (bibliothèque standard de PHP) que l'on connaît. Les classes de ce composant s'appuient aussi souvent sur ce standard afin d'enrichir un concept ou objet existant. Le framework s'appuie beaucoup sur ce composant qui permet d'introduire une certaine homogénéisation dans le framework. Nous allons présenter dans cet article quelques classes que l'on retrouvera beaucoup au sein du framework et que vous pourrez utiliser au sein de vos projets.

III. La classe AbstractOptions

La classe abstraite AbstractOptions fournit des méthodes de base aux classes permettant de gérer la configuration d'un composant. Cette classe offre un mécanisme d'initialisation qui, à partir d'un tableau d'options, permet de distribuer aux méthodes d'altérations, les données qu'elles doivent recevoir. Afin de pouvoir automatiser cette tâche, la classe formate les noms de clés du tableau de paramètres en nom de méthode correspondant à cette clé en la préfixant du mot « set » :

Zend/Stdlib/AbstractOptions.php
Sélectionnez
public function __construct($options = null)
{
    if (null !== $options) {
        $this->setFromArray($options);
    }
}
Zend/Stdlib/AbstractOptions.php
Sélectionnez
public function setFromArray($options)
{
    if (!is_array($options) && !$options instanceof Traversable) {
        […]
    }
    foreach ($options as $key => $value) {
        $this->__set($key, $value);
    }
}
Zend/Stdlib/AbstractOptions.php
Sélectionnez
protected function assembleSetterNameFromKey($key)
{
    $parts = explode('_', $key);
    $parts = array_map('ucfirst', $parts);
    $setter = 'set' . implode('', $parts);
    if (!method_exists($this, $setter)) {
        […]
    }
    return $setter ;
}

Le constructeur fait appel à la méthode « setFromArray() » qui s'occupe d'injecter les options depuis un tableau afin que chaque valeur de la configuration soit traitée par la méthode magique « __set() » .Cette méthode en recherche une autre du nom de « setMaVariable() » où « ma_variable » est une clé du tableau de configuration. Par exemple, la clé « config_cache_key » est injectée depuis la méthode « setConfigCacheKey() » dont le nom est formaté en « CamelCase » tout en supprimant les underscores du nom de la variable. Nous comprenons donc que le fait de ne pas respecter cette règle, en nommant ou en insérant une clé qui ne correspond à aucune méthode interne, lèvera une exception :

Zend/Stdlib/Options.php
Sélectionnez
protected function assembleSetterNameFromKey($key)
{
    […]
    if (!method_exists($this, $setter)) {
        throw new Exception\BadMethodCallException(
            'The option "' . $key . '" does not '
            . 'have a matching ' . $getter . ' getter method '
            . 'which must be defined'
        );
    }
    return $setter ;
}

L'exception levée dans notre exemple
Sélectionnez
Fatal error: Uncaught exception 'Zend\Stdlib\Exception\BadMethodCallException' with message 'The option "ma_cle_inconnu" does not have a matching setMaCleInconnu

Il devient alors très simple de composer une classe responsable de la gestion d'options d'un composant. Il suffit d'écrire les « setters » et « getters », méthodes de récupération et de modification, correspondant aux noms des attributs et la classe devient fonctionnelle. Cette classe est utilisée à de nombreuses reprises dans le framework : Zend\Cache, Zend\Mail, Zend\Serializer, etc.

IV. La classe ArrayUtils

La classe ArrayUtils offre de nombreuses fonctions afin de manipuler au mieux les tableaux. La classe offre des méthodes afin de connaître le type des clés d'un tableau, comme par exemple en testant s'il existe des clés du type de la chaîne de caractères :

Zend/Stdlib/ArrayUtils.php

Sélectionnez
public static function hasStringKeys($value, $allowEmpty = false)
{
    if (!is_array($value)) {
        return false;
    }

    if (!$value) {
        return $allowEmpty;
    }
    return count(array_filter(array_keys($value), 'is_string')) > 0;
}

La méthode « hasStringKeys() » applique un filtre sur la liste des clés du tableau afin de récupérer les clés de type chaîne de caractères. Deux autres méthodes similaires existent afin de tester s'il existe des clés de type entier ou numérique.

La méthode statique « iteratorToArray() » permet de compléter la fonction native de PHP « iterator_to_array() » en permettant de parcourir récursivement le tableau ou l'objet implémentant l'interface Traversable passé en paramètre. En effet, la méthode laisse la possibilité de passer un tableau en paramètre, auquel cas il retournera simplement le tableau si l'on n'a pas demandé de le parcourir récursivement :

Zend/Stdlib/ArrayUtils.php

Sélectionnez
public static function iteratorToArray($iterator, $recursive = true)
{
    if (!is_array($iterator) && !$iterator instanceof Traversable) {
        throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable object');
    }
    if (!$recursive) {
        if (is_array($iterator)) {
            return $iterator;
        }
        return iterator_to_array($iterator);    
     }
    […]
}

La méthode vérifie ensuite si l'objet ne comporte pas une méthode « toArray() », auquel cas elle est utilisée avant de parcourir le tableau récursivement :

Zend/Stdlib/ArrayUtils.php

Sélectionnez
public static function iteratorToArray($iterator, $recursive = true)
{
    […]
    if (method_exists($iterator, 'toArray')) {
        return $iterator->toArray();
    }
    $array = array();
    foreach ($iterator as $key => $value) {
        if (is_scalar($value)) {
            $array[$key] = $value;
            continue;
        }
        if ($value instanceof Traversable) {    
             $array[$key] = static::iteratorToArray($value, $recursive);
            continue;
        }
        if (is_array($value)) {
            $array[$key] = static::iteratorToArray($value, $recursive;
            continue;
        }
        $array[$key] = $value;
    }
    return $array ;
}

À chaque fois que la boucle rencontre un tableau ou un objet de type Traversable, la méthode est appelée récursivement. Nous comprenons l'intérêt de la souplesse de la méthode sur le type en premier paramètre. Cette méthode permet de gérer facilement les compositions de variables entre tableaux, objet de type Traversable et objet implémentant la méthode « toArray() ».

V. La classe CallbackHandler

La classe CallbackHandler permet de surcharger le comportement des fonctions natives de PHP « call_user_func() » ou encore « call_ user_ func_ array() » qui permettent d'appeler des fonctions de rappels. La classe offre la possibilité de prendre en paramètre tout ce qui peut être appelé en tant que fonction :

Zend/Stdlib/CallbackHandler.php
Sélectionnez
public function __construct($callback, array $metadata = array())
{
    $this->metadata  = $metadata;
    $this->registerCallback($callback);
}
Zend/Stdlib/CallbackHandler.php

Sélectionnez
protected function registerCallback($callback)
{
    if (!is_callable($callback)) {
        throw new Exception\InvalidCallbackException('Invalid callback provided; not callable');
    }
    […]
}

Il est donc possible de passer de nombreux paramètres au constructeur de la classe : tableau, closure, chaîne de caractères, etc. Voici quelques exemples d'utilisation :

Utilisation du CallbackHandler

Sélectionnez
class Callback
{
    public function doSomething()
    {
        echo "ok\n";
    }
    public function doSomethingWithArg($arg)
    {
        echo $arg . "\n" ;
    }
}
function doSomething()
{
    echo "ok\n";
}
$callbackObject = new Callback();
$callback = new Stdlib\CallbackHandler(array('Callback', 'doSomething'));
$callback->call();

$callback = new Stdlib\CallbackHandler(array($callbackObject, 'doSomething'));
$callback->call();

$callback = new Stdlib\CallbackHandler(function() { echo "ok\n"; });
$callback->call();

$callback = new Stdlib\CallbackHandler(array($callbackObject, 'doSomethingWithArg'));
$callback->call(array('ok'));

$callback = new Stdlib\CallbackHandler('doSomething');
$callback->call();

$callback = new Stdlib\CallbackHandler('\Callback::doSomething');
$callback->call();

Comme on le voit, la méthode « call() » permet de fournir des arguments à la méthode de callback. Évidemment, cette méthode s'appuie sur les fonctions « call_user_func() » et « call_ user_ func_ array() » de PHP que l'on connaît :

Zend/Stdlib/CallbackHandler.php

Sélectionnez
public function call(array $args = array())
{
    […]
    $argCount = count($args);
    […]
    switch ($argCount) {
        case 0:
            if (self::$isPhp54) {
                return $callback();
            }
            return call_user_func($callback);
        case 1:
            if (self::$isPhp54) {
                return $callback(array_shift($args));
            }
            return call_user_func($callback, array_shift($args)) ;
        case 2:
            $arg1 = array_shift($args);
            $arg2 = array_shift($args);
            if (self::$isPhp54) {
                return $callback($arg1, $arg2);    
             }
            return call_user_func($callback, $arg1, $arg2);
        case 3:
            $arg1 = array_shift($args);
            $arg2 = array_shift($args);
            $arg3 = array_shift($args);
            if (self::$isPhp54) {
                return $callback($arg1, $arg2, $arg3);
            }
            return call_user_func($callback, $arg1, $arg2, $arg3;
        default:
            return call_user_func_array($callback, $args);    
    }
}

La classe offre aussi la possibilité d'utiliser l'instance de l'objet CallbackHandler directement comme une fonction :

Zend/Stdlib/CallbackHandler.php
Sélectionnez
public function __invoke()
{
    return $this->call(func_get_args()) ;
}

Nous pouvons donc écrire ces instructions :

Utilisation de l'instance comme fonction
Sélectionnez
$callback = new Stdlib\CallbackHandler(array($callbackObject, 'doSomething'));
$callback();

Ce composant peut s'avérer particulièrement utile pour vos gestions de fonctions de rappel afin de fournir un peu plus de flexibilité dans vos applications.

VI. La classe ErrorHandler

La classe ErrorHandler fournit des méthodes de gestion pour capturer les erreurs de l'application. Voici un exemple de capture de notice :

Capture d'erreur

Sélectionnez
class ErrorHandlerTest
{
    public function doSomething()
    {
        trigger_error(‘lancement de notice’, \E_USER_NOTICE);
    }
}
Stdlib\ErrorHandler::start(\E_ALL);
$errHandler = new ErrorHandlerTest();
$errHandler->doSomething();
$err = Stdlib\ErrorHandler::stop();
echo $err->getMessage();

La capture d'erreur est démarrée par la méthode « start() » qui fait appel à la méthode « set_error_handler() » de PHP afin de modifier la fonction qui gère les erreurs :

Zend/Stdlib/ErrorHandler.php

Sélectionnez
public static function start($errorLevel = \E_WARNING)
{
    if (static::started() === true) {
        throw new Exception\LogicException('ErrorHandler already started';
    }
    static::$started        = true;    static::$errorException = null ;
    set_error_handler(array(get_called_class(), 'addError'), $errorLevel);
}

La méthode prend le type d'erreur à gérer en paramètre et enregistre sa méthode « addError() » comme méthode de gestion d'erreurs. Cette méthode se contente d'enregistrer l'erreur dans l'attribut « $errorException » que l'on peut récupérer automatiquement sous forme d'exception depuis la méthode statique « stop() » :

Zend/Stdlib/ErrorHandler.php
Sélectionnez
public static function stop($throw = false)
{
    if (static::started() === false) {
        throw new Exception\LogicException('ErrorHandler not started');
    }
    $errorException = static::$errorException;
    static::$started        = false;
    static::$errorException = null;    
     restore_error_handler();
    if ($errorException && $throw) {
        throw $errorException;
    }
    return $errorException;
}

La classe ErrorHandler possède une API relativement facile et agréable à prendre en main qui permet de se détacher de la gestion des erreurs que l'on implémente dans chaque projet.

VII. La classe Glob

La classe Glob permet d'utiliser la fonction native « glob() » avec les systèmes qui ne gèrent pas la méthode « glob() » avec l'utilisation du flag « GLOB_BRACE ». Une fonction a été spécialement portée de la bibliothèque GLIBC afin de rendre l'utilisation de « GLOB_BRACE » possible :

Zend/Stdlib/Glob.php

Sélectionnez
public static function glob($pattern, $flags, $forceFallback = false)
{
    if (!defined('GLOB_BRACE') || $forceFallback) {
        return self::fallbackGlob($pattern, $flags);
    } else {
        return self::systemGlob($pattern, $flags);
    }
}

Nous remarquons que cette fonction « fallbackGlob() » est utilisée si le système ne définit pas la constante « GLOB_BRACE ». Cette classe permet donc d'utiliser cette fonctionnalité sans bloquer les plates-formes qui ne sont pas compatibles. Il est évidemment conseillé d'utiliser ce composant lorsque vous souhaitez utiliser la fonction « glob() » avec le flag « GLOB_BRACE ».

VIII. Les hydrateurs

Les hydrateurs permettent d'extraire les données et de peupler les attributs d'un objet suivant une stratégie définie. Les hydrateurs de la Stdlib se trouvent dans le namespace Zend\Stdlib\Hydrator et offrent plusieurs stratégies :

• extraction et modification depuis les méthodes « getXXX() » et « setXXX() » de l'objet ;

• extraction et modification depuis les propriétés publiques de l'objet ;

• extraction et modification depuis une analyse, à l'aide de la classe reflectionClass, de l'objet ;

• extraction et modification depuis les méthodes « getArrayCopy() » et « exchangeArray() » propres aux objets travaillant avec les tableaux.

Voici un exemple d'utilisation de l'hydrateur basé sur les « getters » et « setters » de l'objet :

Utilisation de l'hydrateur ClassMethod
Sélectionnez
class Exemple
{
    protected $ma_propriete = 'ma valeur';
    public function getMaPropriete()
    {
        return $this->ma_propriete;
    }
    public function setMaPropriete($ma_propriete)    {
        $this->ma_propriete = $ma_propriete;
        return $this;
    }
}
$exemple = new Exemple();
$hydrateur = new Stdlib\Hydrator\ClassMethods();
$datas = $hydrateur->extract($exemple);

L'hydrateur implémente l'interface Zend\Stdlib\Hydrator\HydratorInterface qui définit le contrat de base que doit respecter les hydrateurs :

Zend/Stdlib/Hydrator/HydratorInterface.php

Sélectionnez
interface HydratorInterface
{
    public function extract($object);
    public function hydrate(array $data, $object);
}

La méthode « extract() » permet d'extraire les données de l'objet suivant la stratégie de l'hydrateur et la méthode « hydrate() » permet de peupler l'objet depuis les données fournies en paramètre.

Dans notre exemple, nous utilisons l'hydrateur basé sur les méthodes de l'objet. Les méthodes « getMaPropriete() » et « setMaPropriete() » vont lui permettre d'extraire de l'objet, un tableau avec comme entrée une clé « ma_propriete » avec la valeur « ma valeur ».

Examinons le fonctionnement de l'hydrateur Zend\Stdlib\Hydrator\ClassMethods :

Zend/Stdlib/Hydrator/ClassMethods.php
Sélectionnez
public function extract($object)
{
    if (!is_object($object)) {
        throw new Exception\BadMethodCallException(sprintf(
            '%s expects the provided $object to be a PHP object)',
 __METHOD__        ));
    }
    $transform = function($letters) {
        $letter = array_shift($letters);
        return '_' . strtolower($letter);
    };
    $attributes = array();
    $methods = get_class_methods($object);
    foreach ($methods as $method) {    
           if (preg_match('/^get[A-Z]\w*/', $method)) {
            $setter = preg_replace('/^get/', 'set', $method);
            if (!in_array($setter, $methods)) {
                continue;
            }
            $attribute = substr($method, 3);
            $attribute = lcfirst($attribute);
            if ($this->underscoreSeparatedKeys) {
                $attribute = preg_replace_callback('/([A-Z])/', $transform, $attribute);
            }
            $attributes[$attribute] = $this->extractValue($attribute, $object->$method());    
        }
    }
    return $attributes;
}

L'hydrateur récupère tout d'abord la liste des méthodes de l'objet avant de les parcourir :

Zend/Stdlib/Hydrator/ClassMethods.php

Sélectionnez
public function extract($object)
{
    […]
    $methods = get_class_methods($object);
    foreach ($methods as $method) {    
        […]
    }
}

Comme nous souhaitons extraire des données, l'hydrateur se base alors sur le « getter » de l'objet et donc ses méthodes commençant par « get », tout en vérifiant que le « setter » correspondant existe bel et bien :

Zend/Stdlib/Hydrator/ClassMethods.php

Sélectionnez
public function extract($object)
{
    foreach ($methods as $method) {    
        if (preg_match('/^get[A-Z]\w*/', $method)) {
            $setter = preg_replace('/^get/', 'set', $method);    
             if (!in_array($setter, $methods)) {
                continue;
            }
            […]
        }
    }
}

Une fois les vérifications faites, le nom de l'attribut est formaté afin d'être enregistré dans le tableau de retour :

Zend/Stdlib/Hydrator/ClassMethods.php

Sélectionnez
public function extract($object)
{
    […]
    foreach ($methods as $method) {    
         […]
        $attribute = substr($method, 3);
        $attribute = lcfirst($attribute);
        if ($this->underscoreSeparatedKeys) {
            $attribute = preg_replace_callback('/([A-Z])/', $transform, $attribute);
        }
        $attributes[$attribute] = $this->extractValue($attribute, $object->$method());
    }
    […]
}

Nous remarquons que nous avons la possibilité de transformer les noms de clés au format « CamelCase » ou un format avec underscore grâce à l'attribut « $underscoreSeparatedKeys » que nous pouvons définir depuis le constructeur :

Zend/Stdlib/Hydrator/ClassMethods.php

Sélectionnez
public function __construct($underscoreSeparatedKeys = true)
{
    parent::__construct();
    $this->underscoreSeparatedKeys = $underscoreSeparatedKeys ;
}

Par défaut, la classe utilise les underscores. Nous aurons donc la clé « ma_propriete » pour la méthode « getMaPropriete() ». Si nous passons le paramètre à « false », nous obtiendrons la clé « maPropriete ».

Lors de l'hydratation, ou peuplement de l'objet, la méthode « extract() » utilisera les méthodes commençant par « set » de l'objet si celles-ci existent :

Exemple d'hydratation
Sélectionnez
$hydrateur = new Stdlib\Hydrator\ClassMethods();
$datas = array('ma_propriete' => 'nouvelle valeur');
$hydrateur->hydrate($datas, $exemple);

Revenons à l'extraction et remarquons que l'hydrateur utilise la méthode « extractValue() » pour enregistrer la valeur plutôt qu'une affectation classique. Ceci permet d'ajouter une surcouche lors de l'extraction ou de l'hydratation. Voici comment se comporte la méthode « extractValue() » :

Zend/Stdlib/Hydrator/AbstractHydrator.php
Sélectionnez
public function extractValue($name, $value)
{
    if ($this->hasStrategy($name)) {
        $strategy = $this->getStrategy($name);
        $value = $strategy->extract($value);    
     }
    return $value;
}

L'AbstractHydrator dont héritent les hydrateurs vérifie si une stratégie particulière existe sur la propriété à hydrater afin de l'utiliser. Par défaut, la valeur est simplement retournée. Voici un exemple de stratégie ajoutée sur l'hydrateur :

Ajout de stratégie

Sélectionnez
class FilterStrategy implements StrategyInterface
{
    public function extract($value)    {
        return strip_tags($value);
    }
    public function hydrate($value)
    {
        return "<p>" . $value . "</p>";    
     }
}
$exemple->setMaPropriete('<p>ma valeur</p>');
$hydrateur = new Stdlib\Hydrator\ClassMethods();
$hydrateur->addStrategy('ma_propriete', new FilterStrategy);
$datas = $hydrateur->extract($exemple);

La stratégie, permet ici de supprimer les tags de la clé correspondant à « ma_propriete » lors de l'extraction et de les ajouter lors de l'hydratation.

Attention, nous utilisons les underscores pour les clés. Si nous passons au format CamelCase, cela ne fonctionnera plus, sauf si nous modifions la clé pour la stratégie :

Utilisation du format CamelCase
Sélectionnez
$exemple->setMaPropriete('<p>ma valeur</p>');
$hydrateur = new Stdlib\Hydrator\ClassMethods(false);
$hydrateur->addStrategy('maPropriete', new FilterStrategy;
$datas = $hydrateur->extract($exemple);

Les hydrateurs sont très utilisés avec les formulaires et peuvent l'être dans de nombreuses applications. Ils sont très extensibles et très simples d'utilisation. Les autres hydrateurs fonctionnent de la même manière, ils ne diffèrent que dans la récupération des données de l'objet depuis ses méthodes de classes ou directement depuis ses propriétés.

IX. Conclusion et ressources

Complète et simple d'utilisation, la librairie standard du Zend Framework 2 vous permettra de réaliser les opérations usuelles tout en conservant souplesse et performance.
Cet article est tiré en partie du livre « Au cœur de Zend Framework 2 »livre "Au cœur de Zend Framework 2" disponible dans le début du quatrième trimestre de l'année 2012.