Moderniser une application PHP Zend Framework 1 avec le conteneur de dépendances de Symfony2

Olivier Van Hoof

Dans cet article nous verrons comment intégrer des composants PHP modernes dans une application « legacy » écrite en Zend Framework 1 grâce à Composer et au conteneur d'injection de dépendances de Symfony2. Commentez

- Titre : Moderniser une application PHP Zend Framework 1 avec le conteneur de dépendances de Symfony2

- Auteur : Olivier Van Hoof

- Parution : 28 décembre 2014

- Mise à jour : 19 septembre 2015

- Licence : Le contenu de cet article est rédigé par Olivier Van Hoof et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Pourquoi ?

- III. Séparer la configuration en deux fichiers

- IV. Installer les éléments de base

- IV-A. Composer

- IV-B. Conteneur d'injection de dépendances de Symfony2

- IV-B-1. Autoload des services

- IV-B-2. Intégration du conteneur de services

- IV-B-3. Accéder aux services depuis le contrôleur avec un helper

- V. Installer d'autres composants

- V-A. Doctrine 2

- V-B. JMSSerializer

- V-C. Monolog

- V-D. Un service simple : markdown

- VI. Conclusion

- VII. Remerciements

I - Introduction

La technologie évolue très vite, en particulier dans le domaine du web. Il est parfois difficile de suivre, surtout en entreprise, où de grosses applications développées sur plusieurs années sont malheureusement vite dépassées technologiquement.

Mais PHP est un langage souple, et une application développée en Zend Framework 1 n'est pas forcément condamnée à être figée dans le temps. Grâce au fabuleux outil qu'est Composer et à quelques adaptations de code mineures, nous allons pouvoir intégrer n'importe quel composant PHP moderne à une application ZF1, et ainsi utiliser les outils de développement PHP d'aujourd'hui !

II - Pourquoi ?

Un développeur utilisant ZF1 au quotidien pourrait en effet se poser la question : pourquoi changer ?

ZF1 est un très bon framework, mais qui date de l'époque que j'appellerais « pré PHP 5.3 ». Cette version du langage a initié un tournant dans le développement PHP, car elle a introduit des nouveautés majeures, notamment les fameux namespaces. À partir de là, les frameworks ont commencé à évoluer, non seulement pour organiser leurs classes en espaces de noms, mais aussi pour améliorer l'architecture objet elle-même en utilisant des design patterns plus modernes. C'est ainsi que les versions 2 des frameworks sont apparues (Symfony2, Zend Framework 2… ), incompatibles avec les versions précédentes à cause de ces changements radicaux d'architecture. Parmi tous ces frameworks PHP nouvelle génération, Symfony2 s'impose comme une référence.

Une limitation criante de ZF1 et des frameworks de son époque est son architecture MVC. C'est un bon concept, mais trop limité. Petit rappel pour bien comprendre :

- M : modèle, couche métier : c'est là qu'on place toute la logique de l'application, typiquement l'accès à la base de données, avec le design pattern « table data gateway » dans le cas de ZF1 (en gros : une classe PHP par table) ;

- V : vue, c'est-à-dire la couche de l'interface utilisateur : du code HTML avec le strict nécessaire de code PHP pour afficher le contenu dynamique ;

- C : contrôleur, sensé faire la « glu » entre la couche métier et la vue (typiquement on y place les actions à exécuter suite aux requêtes HTTP GET ou POST, qui vérifient les paramètres passés et appellent les méthodes de la couche modèle pour effectuer l'action adéquate : mise à jour de base de données, sélection de données qu'on passe à la vue pour affichage, etc.).

Présenté comme ça, ça a l'air bien pourtant ! Sauf qu'en pratique la couche métier se résume trop souvent à un ensemble de classes matérialisant les tables de la base de données. On se retrouve vite avec des classes « table » énormes contenant beaucoup trop de responsabilités, ce qui viole le premier principe SOLID.

Ce qui manque principalement pour mieux organiser son code métier, c'est un conteneur d'injection de dépendances. Cela permet d'architecturer son code de manière beaucoup plus souple : toute fonctionnalité métier peut être ainsi placée dans un ou plusieurs objets « POPO » (plain old PHP object), donc des objets « purs » ne dépendant d'aucun framework, qui peuvent communiquer entre eux par composition. Le conteneur instancie les objets (qu'on appellera « services ») à la volée en les liant les uns aux autres grâce à un fichier de configuration qui définit les injections de dépendances (par constructeur, par méthode…).

Un cours sur le sujet sortirait du cadre de cet article, aussi je vous invite à lire la documentation officielle de Symfony pour plus d'informations :
http://symfony.com/fr/doc/current/book/service_container.html.

Le conteneur de dépendances est donc la base de notre architecture, qui va servir non seulement à instancier nos propres services, mais aussi à intégrer élégamment des composants tiers (Doctrine 2, Monolog… ) en les exposant en tant que service pour toute l'application. Pour récupérer ces composants, nous utiliserons une application PHP devenue le gestionnaire de dépendances de référence : Composer.

Grâce à la combinaison de ces deux outils, nous allons pouvoir monter une architecture moderne dans un projet ZF1 :

- Composer servira à gérer la liste des composants de notre projet, les télécharger et les maintenir à jour, et utiliser les classes aisément grâce à l'autoloader intégré ;

- le conteneur de dépendances de Symfony2 nous permettra d'initialiser ces composants et les exposer en tant que service dans notre projet ZF1 ainsi que créer nos propres services métier pour mieux structurer notre application.

Le lecteur attentif pourra remarquer que Composer suffit pour utiliser des composants tiers, puisque l'outil fournit une classe d'autoload qu'il complète au fur et à mesure des installations de composants. C'est bien sûr exact, mais utiliser aussi un conteneur d'injection de dépendances présente plusieurs avantages :

- un objet ne sera instancié qu'une seule fois si on l'utilise à plusieurs endroits dans l'application au sein d'une même requête ;

- on pourra intégrer facilement un composant tiers à un objet métier qui nous est propre grâce à un fichier de configuration qui gère les différents modes d'injection de dépendances (par constructeur, par setter… ) entre les services ;

- on pourra initialiser et configurer finement un composant tiers dans un service réutilisable.

Les deux outils sont donc complémentaires.

Le fait d'utiliser le conteneur de dépendances de Symfony2 plutôt qu'un autre (celui de Zend Framework 2, ou Pimple) est un choix personnel, j'ai voulu utiliser celui qui est un des plus répandus aujourd'hui, et aussi montrer aux développeurs habitués à Symfony2 qu'ils peuvent intégrer certains de ses concepts-clés dans un projet ancien en ZF1.

Quant à la problématique de performance, il n'y a pas de souci particulier, car nous n'ajoutons pas tout le framework Symfony2, mais seulement un de ces composants, il n'y a donc que le strict nécessaire, c'est tout l'intérêt d'une programmation orientée objet modulaire.

Dans les chapitres suivants, nous verrons comment mettre en place Composer et le conteneur de services de Symfony2, et nous verrons ensuite comment installer quelques composants classiques d'un projet PHP moderne.

III - Séparer la configuration en deux fichiers

Ce n'est pas vraiment lié à l'installation de nouveaux composants, mais il s'agit d'une amélioration notable d'une appli ZF1, que j'ai donc décidé de vous présenter.

Classiquement dans un projet ZF1, la configuration est placée dans un unique fichier application.ini, organisé en sections, chaque section représentant un environnement.

C'est bien, mais on peut faire mieux : séparer la configuration globale de l'application (en la laissant dans application.ini) et la configuration locale, propre à chaque environnement (test, recette, production, mais aussi le poste local de chaque développeur) dans un nouveau fichier local.ini. Ce fichier local.ini ne devra pas être versionné (il faudra donc le placer dans le fichier .gitignore), ce qui implique deux choses :

- on peut y placer des codes d'accès aux services externes : ces informations sensibles ne seront pas publiquement accessibles. C'est particulièrement important à l'heure des services comme github, où tout le code est public ; une faille de sécurité courante consiste à laisser des mots de passe dans des fichiers de configuration versionnés, donc en clair sur Internet. Autre avantage : chaque développeur peut avoir sa configuration propre et en changer sans être obligé de modifier à chaque fois le fichier application.ini global et le commiter sur le dépôt de code, ce qui produit des commits inutiles (sans même parler de l'aspect confidentialité puisqu'on met tout en clair) ;

- il faudra penser à ajouter un exemple de chaque nouvelle directive de configuration dans un fichier local.ini.dist, qui - lui - sera versionné. Ainsi, tout le monde connaîtra la structure de ce fichier et pourra donc remplir son propre fichier local.ini de manière adéquate.

Le fichier local.ini sera placé à côté de l'application.ini (application/configs) et suivra la même structure, pour rester compatible avec la logique de ZF1. Au bootstrap de l'application, on va fusionner ces deux fichiers en mémoire et passer le résultat à l'objet Zend_Application, ce qui rendra donc l'opération transparente. À cette fin, il faudra modifier légèrement le fichier index.php que je vous reproduis intégralement :

public/index.php

<?php
if (!defined('ROOT_PATH')) {
define('ROOT_PATH', realpath(__DIR__ . '/..'));
}

// Define path to application directory
if (!defined('APPLICATION_PATH')) {
define('APPLICATION_PATH', ROOT_PATH . '/application');
}

// Define application environment
defined('APPLICATION_ENV')
|| define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));

// Ensure library/ is on include_path
set_include_path(implode(PATH_SEPARATOR, array(
realpath(APPLICATION_PATH . '/../library'),
get_include_path(),
)));

/** Zend_Application */
require_once 'Zend/Application.php';
require_once 'Zend/Config/Ini.php';

// ce qui suit est une astuce vue sur StackOverflow pour séparer la config en 2 fichiers :
// http://stackoverflow.com/questions/6852392/zend-framework-separating-developer-specific-environment-info-from-config
// charge la config globale de l'appli
$config = new Zend_Config_Ini(
APPLICATION_PATH . '/configs/application.ini',
APPLICATION_ENV,
array('allowModifications' => true)
);
// charge la config locale (propre à chaque environnement)
if (file_exists(APPLICATION_PATH . '/configs/local.ini')) {
$localConf = new Zend_Config_Ini(
APPLICATION_PATH . '/configs/local.ini',
APPLICATION_ENV
);
// ajoute la config locale à la config globale
$config->merge($localConf);
}

$application = new Zend_Application(APPLICATION_ENV, $config);
$application->bootstrap()
->run();

IV - Installer les éléments de base

Pour pouvoir bénéficier des bibliothèques PHP modernes dans votre projet ZF1, il faut d'abord y intégrer deux briques de base :

1. Composer ;

2. Le conteneur d'injection de dépendances de Symfony2.

Les deux chapitres suivants y sont consacrés.

IV-A - Composer

Composer est l'outil PHP moderne pour installer des bibliothèques PHP tierces. De manière standard, les dépendances se placent dans le répertoire /vendor du projet et Composer génère automatiquement un fichier d'autoload qui permet de charger les dépendances aisément dans votre projet PHP.

Outre les dépendances installables depuis la plateforme Packagist, Composer peut aussi gérer l'autoload de vos propres classes.

Composer se présente sous la forme d'une archive phar, à télécharger et placer où vous voulez, classiquement à la racine du projet. Ce fichier composer.phar ne devra bien sûr pas être versionné.

L'outil est utilisable en ligne de commande, installer un nouveau composant dans un projet PHP se résume donc bien souvent à cette simple commande :

php composer.phar require author/component

On suit la logique des namespaces PHP, où chaque composant doit avoir le nom de son auteur et le nom du composant dans son namespace.

Des options plus poussées sont bien entendu possibles, par exemple pour installer une version spécifique d'un composant. Pour cela, consultez la documentation de Composer sur le site officiel : https://getcomposer.org/doc/.

Composer va créer un fichier composer.lock contenant des informations internes à l'outil pour connaître la structure des paquets installés, et un fichier composer.json contenant la liste des dépendances installées. Ces deux fichiers doivent être commités. Lors de chaque instruction Composer, un fichier vendor/autoload.php sera aussi créé (la première fois) ou mis à jour avec les instructions d'autoload pour le nouveau composant.

La seule adaptation de notre application ZF1 sera donc d'intégrer l'autoload généré par Composer au démarrage. On commence par ajouter une constante ROOT_PATH représentant comme son nom l'indique le chemin absolu vers la racine de notre projet, en ajoutant ce bout de code au tout début du fichier public/index.php (c'est facultatif, mais je trouve cette constante pratique) :

public/index.php

if (!defined('ROOT_PATH')) {
define('ROOT_PATH', realpath(__DIR__ . '/..'));
}

Ensuite dans le bootstrap de l'application, on ajoute la ligne suivante au tout début pour charger le fichier autoload généré par Composer :

application/Bootstrap.php

require_once ROOT_PATH . '/vendor/autoload.php';

Et c'est tout ! Les dépendances Composer seront maintenant utilisables dans notre projet.

IV-B - Conteneur d'injection de dépendances de Symfony2

C'est le composant principal, si vous ne deviez en choisir qu'un, ce serait celui-ci ! C'est en effet la pierre angulaire de tout développement PHP moderne, le chargement des autres composants passera par lui.

L'installation est très simple avec Composer, et on va d'ailleurs installer en même temps le composant Symfony Config qui va nous permettre de charger la configuration des services dans différents formats (xml, yml, php) :

php composer.phar require symfony/dependency-injection
php composer.phar require symfony/config

L'utiliser dans notre projet ZF1 va demander un peu de travail. En fait, ZF1 possède déjà un conteneur de services, qui n'est autre que Zend_Application_Bootstrap, matérialisé par le fichier application/Bootstrap.php. Pour initialiser un service à la manière ZF1, appelé d'ailleurs « ressource », il faut créer une méthode _initMonService() qui doit retourner une instance de la classe de service. Cela présente l'inconvénient notoire de charger tous les services au démarrage, alors qu'ils ne seront pas nécessairement utilisés à chaque requête. De plus, la récupération des services est plutôt lourde puisqu'il faut chaque fois récupérer le singleton du contrôleur frontal pour récupérer le bootstrap, et enfin le service.

Nous allons néanmoins nous servir du Bootstrap ZF pour y stocker un seul service : le conteneur d'injection de dépendances de Symfony2 ! Et pour accéder à nos services facilement depuis les contrôleurs, nous réaliserons un petit plugin d'action.

IV-B-1 - Autoload des services

Outre la configuration du conteneur, il faut adapter l'autoload de notre projet pour qu'il retrouve nos classes personnalisées et en namespaces dans un répertoire déterminé. L'arborescence que je vous propose d'utiliser pour nos classes de services est la suivante :

- library
- Zend (le framework lui-même, comme dans toute appli ZF1)
- App (c'est ici que nous allons placer nos classes)
- Application
- Domain
- Entity
- Proxy
- Repository
- ValueObject
- Service
- Infra

Cette structure est librement inspirée du Domain Driven Design. On y trouve trois répertoires principaux :

- library/App/Application : services fortement liés à l'application et au ZF1 (par exemple : classes de filtres ZF personnalisés, classes d'authentification, etc.) ;

- library/App/Domain : services métier à proprement parler, donc souvent liés à la base de données. Les sous-répertoires Domain/Entity et Domain/Repository ainsi que Domain/ValueObject sont typiquement prévus pour l'ORM Doctrine2, que nous verrons plus bas. Le sous-répertoire Domain/Service contiendra les services métier en tant que tels, qui seront disponibles depuis le conteneur de services ;

- library/App/Infra : les services « extérieurs », par exemple, service d'envoi d'e-mail, connexion à un ldap…

Nous avons vu précédemment que Composer pouvait gérer l'autoload de nos propres classes, nous plaçons donc ces lignes à la fin du fichier de configuration composer.json pour ajouter notre espace de nom à l'autoload de notre application :

composer.json

"autoload": {
"psr-0": {
"App\\": "library/"
}
}

Grâce à la convention PSR-0, Composer sait que le nom complet (FQN, fully qualified name) des classes de notre namespace « App » correspondent aux fichiers et sous-répertoires se trouvant dans library/App. Composer est aussi capable de gérer d'autres conventions pour le chargement des classes, reportez-vous à sa documentation.

Pour que ce changement soit pris en compte, il faut regénérer le script d'autoload de Composer, ce qui s'effectue avec la commande suivante :

php composer.phar dump-autoload

Notez que toute commande de modification des dépendances de l'appli (install, require, remove... ) regénère aussi automatiquement le script d'autoload.

IV-B-2 - Intégration du conteneur de services

Le conteneur de Symfony2 peut instancier les services de deux manières :

- par une configuration définie dans un fichier pouvant être au format YAML, XML ou PHP. YAML étant le format préféré de Symfony, nous définirons nos services dans un fichier services.yml que nous placerons à côté du fichier application.ini dans application/configs ;

- en les instanciant à la main (cas des initialisations complexes, nécessitant du code spécifique, autre qu'un simple new MaClasse avec quelques arguments). C'est le mode dit « synthétique » dans le jargon Symfony (cf. doc officielle). Nous verrons un exemple plus bas avec l'installation de l'ORM Doctrine 2.

Pour mettre cela en place proprement, nous allons réaliser une nouvelle classe de ressources de bootstrap que nous nommerons « dic » (dependency injection container) et qui se chargera d'initialiser le conteneur en lisant les dépendances configurées dans le fichier spécifié dans le paramètre resources.dic.configfile (classiquement services.yml). On parse aussi la configuration de l'application ZF (pour rappel, c'est la fusion des fichiers application.ini et local.ini) en inspectant toutes les clés préfixées resources.dic.params pour définir les paramètres qui pourront être utilisés dans la définition des services (sous la forme '%nom.du.parametre%').

Nous commençons par définir l'emplacement de nos classes de ressource de bootstrap personnalisées dans application.ini, ainsi que le paramètre pointant sur le fichier de définition des services.

application/configs/application.ini

pluginPaths.My_Resource = APPLICATION_PATH "/resources"
resources.dic.configfile = APPLICATION_PATH "/configs/services.yml"

Voici le code de la classe de ressource spécifique :

application/resources/Dic.php

<?php
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

class My_Resource_Dic extends Zend_Application_Resource_ResourceAbstract
{
/**
* Initialise le conteneur d'injection de dépendances de Symfony
* et le mémorise dans le container du bootstrap Zend
* @return \Symfony\Component\DependencyInjection\ContainerBuilder
*/
public function init()
{
$options = $this->getOptions();
$container = new ContainerBuilder();
// charge le fichier de définition des services suivant son format
$configFile = basename($options['configfile']);
$extension = substr($configFile, strrpos($configFile, "."));
switch ($extension) {
case '.yml':
$loader = new YamlFileLoader($container, new FileLocator(dirname($options['configfile'])));
break;
case '.xml':
$loader = new XmlFileLoader($container, new FileLocator(dirname($options['configfile'])));
break;
case '.php':
$loader = new PhpFileLoader($container, new FileLocator(dirname($options['configfile'])));
break;
default: throw new Exception("DIC services config file format not supported");
}
$loader->load($configFile);

// ajoute tous les paramètres définis dans resources.dic.params
$this->_addContainerParameters($container, $options['params']);

// indispensable pour l'héritage de services, merci stackoverflow !
// http://stackoverflow.com/questions/11415998/symfony2-dependency-injection-with-parent-services-does-not-work
$container->compile();
return $container;
}

/**
* Ajoute un ou plusieurs paramètres au container d'injection de dépendances. Si la valeur du paramètre
* est un array on va le parcourir pour obtenir un nom de paramètre toto.titi.tata
* @param ContainerBuilder $container
* @param array|string $value
* @param string $name
*/
protected function _addContainerParameters(ContainerBuilder $container, $value, $name = null)
{
// si la valeur est un array, on parcourt tous ses éléments en construisant le nom du paramètre
// en concaténant les clés avec un .
if (is_array($value)) {
foreach ($value as $key => $val) {
$paramName = $key;
if (!empty($name)) {
$paramName = $name . "." . $paramName;
}
$this->_addContainerParameters($container, $val, $paramName);
}
// la valeur n'est pas un array, donc on a fini de récurser, on peut ajouter le paramètre
} else {
$container->setParameter($name, $value);
}
}
}

Les services de type synthétiques pourront être initialisés comme d'habitude (par une méthode _initXX du bootstrap ou par un plugin de ressource spécialisé), mais il faudra les placer dans le conteneur manuellement :

application/Bootstrap.php

protected function _initMonService()
{
//$monService = new ...
$this->bootstrap('dic');  // s'assure que le DIC est initialisé
$this->getResource('dic')->set('mon.service', $monService);
}

Et voici un exemple de configuration de services, utilisant des paramètres définis dans nos fichiers application.ini et local.ini :

application/configs/services.yml

services:
em:
synthetic: true

log:
synthetic: true

config:
synthetic: true

domain.service.abstract:
abstract: true
arguments: ['@em']
calls:
- [setLog, ['@log']]

domain.service.exemple:
class: App\Domain\Service\Exemple
parent: domain.service.abstract

infra.ldap:
class: App\Infra\Ldap\Ldap
arguments:
- '%auth.ldap.host%'
- '%auth.ldap.port%'
- '%auth.ldap.username%'
- '%auth.ldap.password%'
- '%auth.ldap.dn%'

Je vous renvoie une fois de plus à la documentation officielle du composant pour plus de détails.

IV-B-3 - Accéder aux services depuis le contrôleur avec un helper

Comme expliqué plus haut, nous créons également un helper d'action de contrôleur ZF1 pour pouvoir accéder facilement à nos services.

La première étape consiste à déclarer à ZF le répertoire où il pourra trouver les helpers d'action de contrôleur :

application/configs/application.ini

resources.frontController.actionHelperPaths.Zend_Controller_Action_Helper = APPLICATION_PATH "/helpers/action"

Ensuite le code du helper lui-même, tout simple, qui accède au conteneur d'injection de dépendances en le récupérant depuis le bootstrap (lui-même récupéré depuis le contrôleur frontal) :

application/helpers/action/Dic.php

<?php
/**
* Helper d'action permettant d'accéder facilement au conteneur d'injection de dépendances
* depuis les contrôleurs de l'application.
*
*/
class Zend_Controller_Action_Helper_Dic extends Zend_Controller_Action_Helper_Abstract
{
/**
* Stocke l'instance du conteneur d'injection de dépendances
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
*/
protected $_container;

/**
* Récupère le conteneur d'injection de dépendances depuis le boostrap et le mémorise dans l'objet
*/
public function init()
{
$this->_container = $this->getFrontController()->getParam('bootstrap')->getResource('dic');
}

/**
* Retourne le conteneur d'injection de dépendances pour pouvoir lui appliquer ses méthodes
* @return \Symfony\Component\DependencyInjection\ContainerBuilder
*/
public function direct()
{
return $this->_container;
}
}

Ce qui nous permet donc d'utiliser un service comme ceci depuis une méthode de contrôleur :

$serviceDoc = $this->_helper->dic()->get('domain.service.document');

V - Installer d'autres composants

V-A - Doctrine 2

Zend Framework dispose de classes pour s'interfacer avec les bases de données, utilisant le design pattern Table Data Gateway. En gros, on crée une classe par table qui décrit la structure de chaque table (colonnes, associations… ) et qui hérite d'une classe de base contenant les méthodes d'accès à la base de données.

Doctrine 2 quant à lui est un ORM plus puissant, c'est l'équivalent en PHP de Hibernate du monde Java, utilisant le design pattern Data Mapper. Les entités (classes table sous ZF) sont cette fois indépendantes (objets purs, ne devant hériter d'aucune classe de base), toutes les opérations de base de données s'effectuant depuis un objet « entity manager » (équivalent de la « session » Hibernate). Je n'entrerai pas dans les détails sur l'intérêt de cet ORM, encore une fois cela sort du cadre de cet article.

Son intégration dans un projet ZF1 nécessitera, outre l'installation par Composer, d'instancier l'objet « entity manager », qui est l'objet central permettant d'accéder à tous les objets de l'ORM. S'agissant d'un composant complexe, son initialisation va nécessiter un peu de code, raison pour laquelle nous l'ajouterons en tant que service synthétique au conteneur de services Symfony2.

Tout d'abord, installons la bibliothèque :

php composer.phar require doctrine/orm

Ensuite, créez la classe de ressource de boostrap suivante :

application/resources/Doctrine.php

<?php
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;

class My_Resource_Doctrine extends Zend_Application_Resource_ResourceAbstract
{
/**
* Initialise l'ORM Doctrine en mémorisant l'entity manager dans le DIC
*/
public function init()
{
$confDoctrine = $this->getOptions();

if ('production' == APPLICATION_ENV) {
$cache = new \Doctrine\Common\Cache\ApcCache;
} else {
$cache = new \Doctrine\Common\Cache\ArrayCache;
}

AnnotationRegistry::registerLoader('class_exists');
$reader = new AnnotationReader();

$config = new Configuration;
$config->setMetadataCacheImpl($cache);
$driverImpl = new AnnotationDriver($reader, ROOT_PATH . '/library/App/Domain/Entity');
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCacheImpl($cache);
$config->setProxyDir(ROOT_PATH . '/library/App/Domain/Entity/Proxy');
$config->setProxyNamespace('App\Domain\Entity\Proxy');

if (true == $confDoctrine['proxies']['autogenerate']) {
$config->setAutoGenerateProxyClasses(true);
} else {
$config->setAutoGenerateProxyClasses(false);
}

$em = EntityManager::create($confDoctrine['conn'], $config);
$this->_bootstrap->bootstrap('dic');
$this->_bootstrap->getResource('dic')->set('em', $em);
}
}

Cette ressource utilise les paramètres suivants :

application/configs/application.ini

resources.doctrine.conn.driver           = 'pdo_mysql'
resources.doctrine.conn.host             = 'localhost'
resources.doctrine.conn.user             = 'root'
resources.doctrine.conn.password         = 'root'
resources.doctrine.conn.dbname           = 'mydatabase'
resources.doctrine.log.filename          = ROOT_PATH "/data/log/sql.log"
resources.doctrine.log.active            = false
resources.doctrine.proxies.autogenerate  = false

Au niveau du conteneur d'injection de dépendances de Symfony2, le service est donc défini comme suit :

application/configs/services.yml

services:
em:
synthetic: true

Vous pourrez maintenant créer vos entités dans le répertoire library/App/Domain/Entity et vos repositories dans library/App/Domain/Repository. La configuration a été faite de telle manière que vous puissiez utiliser un préfixe pour les annotations de l'ORM dans vos entités, ceci afin de pouvoir mélanger plusieurs types d'annotations (nous verrons l'intérêt avec le composant JMSSerializer plus bas). C'est la manière recommandée d'utiliser les annotations.

Voici un extrait d'une entité pour illustrer les annotations préfixées :

namespace App\Domain\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="Societe")
*
*/
class Societe
{
/**
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id @ORM\GeneratedValue(strategy="IDENTITY")
* @var int
*/
protected $id;

// etc.

V-B - JMSSerializer

JMSSerializer est un composant qui permet de sérialiser un objet en différents formats au moyen d'annotations. Un usage fréquent est la transformation de vos entités en JSON pour la réalisation d'API REST.

Commençons comme d'habitude par installer le composant avec Composer :

php composer.phar require jms/serializer

Pour sérialiser un objet, JMSSerializer parcourt toutes ses propriétés et les exporte au format désiré (souvent JSON, mais il est aussi possible de sortir du XML ou du YAML). Cela a l'air simple, mais toute la puissance de l'outil réside dans ses capacités d'adaptation : on peut ainsi lui dire de transformer le nom des propriétés (ce qu'il appelle une « naming strategy »), d'exclure certaines propriétés qu'on ne veut pas publier, de transformer les valeurs de sortie, de créer des propriétés virtuelles basées sur le résultat d'une méthode, etc.

Un détail va ici nous intéresser : pour le format JSON, par défaut, JMSSerializer applique une stratégie de nommage qui transforme le nom de toutes les propriétés en minuscules avec un underscore pour séparer les mots (convention « snake case »). Or classiquement dans une classe PHP nous utilisons plutôt la notation « camelCase ». Mais le sérialiseur nous le transformera en « camel_case » ! Cela se comprend, car cette convention est en général appliquée pour les formats JSON. Mais si nous voulons conserver les noms de propriétés originaux, il va falloir créer une classe de stratégie de nommage personnalisée. Rassurez-vous, ce n'est pas très compliqué.

Enfin, il est aussi possible de créer différentes catégories de sérialisation, en plaçant les propriétés dans des groupes. Dans certains cas en effet, vous voudrez sérialiser un même objet différemment suivant le contexte applicatif.

Pour simplifier l'utilisation du composant JMSSerializer, nous allons créer un service dédié « application.serializer » :

library/App/Application/Serializer/Serializer.php

<?php
namespace App\Application\Serializer;

use JMS\Serializer\SerializerBuilder;
use JMS\Serializer\SerializationContext;

/**
* Wrapper autour du serializer de JMS pour faciliter son usage
*/
class Serializer
{
/**
* Instance du sérialiseur JMS
* @var JMS\Serializer\Serializer
*/
protected $_jmsSerializer = null;

/**
* Doit-on sérialiser aussi les propriétés nulles ?
* @var bool
*/
protected $_serializeNull = true;

/**
* Construit l'instance du sérialiseur JMS
*/
public function __construct()
{
$this->_jmsSerializer = SerializerBuilder::create()
->setPropertyNamingStrategy(new NamingStrategy())
->build();
}

/**
* Spécifie si on doit sérialiser les propriétés nulles
* @param bool $serializeNull
* @return \App\Application\Serializer\Serializer
*/
public function setSerializeNull($serializeNull)
{
if (is_null($serializeNull)) {
throw new \InvalidArgumentException("Vous devez spécifier une valeur pour le paramètre serializenull");
}
$this->_serializeNull = $serializeNull;
return $this;
}

/**
* Sérialise un objet en spécifiant éventuellement des groupes pour obtenir une vue spécifique
* @param array|object $object
* @param string $format Format de sérialisation (défaut: json)
* @param array $groups
* @return string Objet sérialisé
*/
public function serialize($object, $format = 'json', array $groups = null)
{
// si on ne précise pas de groupe, on prend le groupe par défaut
if (empty($groups)) {
$groups = array('Default');
}
return $this->_jmsSerializer->serialize(
$object,
$format,
SerializationContext::create()
->setSerializeNull($this->_serializeNull)
->setGroups($groups)
);
}
}

Ce service se déclare donc comme suit, en précisant d'appeler la méthode setSerializeNull pour configurer cette option :

application/configs/services.yml

application.serializer:
class: App\Application\Serializer\Serializer
calls:
- [setSerializeNull, ['%serializer.serializenull%']]

Le service se base sur ce paramètre de configuration pour savoir s'il faut inclure ou pas les propriétés de valeur nulle dans la sérialisation (par défaut dans JMSSerializer cette valeur est à false) :

application/configs/application.ini

resources.dic.params.serializer.serializenull = true

Et voici la classe de stratégie de nommage :

library/App/Application/Serializer/NamingStrategy.php

<?php
namespace App\Application\Serializer;

use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
* Classe pour spécifier la stratégie de nommage des propriétés du JMSSerializer
* Car la classe qu'on utilise d'habitude (IdenticalPropertyNamingStrategy) ignore l'annotation SerializedName
*/
class NamingStrategy implements PropertyNamingStrategyInterface
{
public function translateName(PropertyMetadata $property)
{
if (!empty($property->serializedName)) {
return $property->serializedName;
}
return $property->name;
}
}

Bien évidemment, si la stratégie de nommage par défaut vous convient, il suffit de ne pas appeler la méthode setPropertyNamingStrategy lors de l'initialisation de JMSSerializer ;-)

Vous pourrez dès lors utiliser les annotations JMSSerializer dans vos entités, voici un exemple :

<?php
namespace App\Domain\Entity;

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMSSerializer;

/**
* @ORM\Entity
* @ORM\Table(name="Societe")
*
*/
class Societe
{
/**
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id @ORM\GeneratedValue(strategy="IDENTITY")
* @JMSSerializer\Exclude
* @var int
*/
protected $id;

/**
* @ORM\Column(name="raisonSociale", type="string", length=255, nullable=false)
* @JMSSerializer\Groups({"Default", "modif"})
* @var string
*/
protected $raisonSociale;

// etc.

Et voici comment utiliser notre service de sérialisation depuis un contrôleur :

// récupération du service
$serializer = $this->_helper->dic()->get('application.serializer');

// exemple de sérialisation simple
$serializer->serialize($monEntite);

// exemple de sérialisation basée sur un contexte particulier (ici « modif »)
$json = $serializer->serialize($monEntite, 'json', array('modif'));

V-C - Monolog

Réalisé par le créateur de Composer, Monolog est un service de log très évolué, bien plus puissant que Zend_Log. Il gère de multiples options de formatage, de multiples formats de sortie (fichier texte, mais aussi logs système, logs distants compatibles… ), bref c'est un composant très intéressant.

L'installation est triviale avec Composer :

php composer.phar require monolog/monolog

Ici aussi nous réaliserons une ressource de boostrap spécialisée pour l'initialiser :

application/resources/Log.php

<?php
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\MemoryPeakUsageProcessor;
use Monolog\Processor\MemoryUsageProcessor;
use Monolog\Processor\WebProcessor;

class My_Resource_Log extends Zend_Application_Resource_ResourceAbstract
{
/**
* Initialise le logger Monolog et le mémorise dans le DIC
*/
public function init()
{
$conf = $this->getOptions();
$logger = new Logger('app');

// n'utilise le split journalier que pour les environnements "live" (pas le dev)
if (substr($this->_bootstrap->getEnvironment(), 0, 3) != 'dev') {
$handler = new RotatingFileHandler($conf['file'], $conf['maxfiles']);
} else {
$handler = new StreamHandler($conf['file']);
}

$handler->setFormatter(new LineFormatter(null, null, true));
$logger->pushHandler($handler);

$logger->pushProcessor(new IntrospectionProcessor());
$logger->pushProcessor(new MemoryPeakUsageProcessor());
$logger->pushProcessor(new MemoryUsageProcessor());
$logger->pushProcessor(new WebProcessor());

$this->_bootstrap->bootstrap('dic');
$this->_bootstrap->getResource('dic')->set('log', $logger);
}
}

Comme d'habitude, je vous renvoie à la documentation officielle pour les détails de l'initialisation du composant.

Cette ressource utilise les paramètres suivants :

application/configs/application.ini

resources.log.file = ROOT_PATH "/data/log/app.log"
resources.log.maxfiles = 60   ; conserve un fichier de log par jour pendant 60 jours

Ensuite, dans l'application, écrire dans le log se fait en récupérant le service « log » et en utilisant les méthodes debug(), warning() ou error() exactement comme on le ferait avec Zend_Log.

V-D - Un service simple : markdown

Certains composants simples ne nécessitent même pas de configuration en tant que service. C'est le cas du composant php-markdown de Michel Fortin, qui permet donc de transformer une chaîne de caractères du format Markdown au format HTML. Son utilisation est toute simple, comme le montre cet extrait de la documentation :

use Michelf\Markdown;
$my_html = Markdown::defaultTransform($my_text);

Dans le cadre d'une application Zend Framework, le plus simple est d'embarquer ce code dans un helper de vue. Grâce à l'inclusion de l'autoloader de Composer dans le bootstrap de notre application, un simple use suffira à utiliser la classe.

Commençons d'abord par installer le composant avec Composer comme d'habitude :

php composer.phar require michelf/php-markdown

Déclarons le chemin où sont situés nos helpers de vue dans la configuration de l'application :

application/configs/application.ini

resources.view.helperPath = APPLICATION_PATH "/helpers/view"

Enfin, créons le helper de vue suivant :

application/helpers/view/Markdown.php

<?php
use Michelf\Markdown;

class Zend_View_Helper_Markdown extends Zend_View_Helper_Abstract
{
public function markdown($text)
{
return Markdown::defaultTransform($text);
}
}

L'utilisation se fera par un simple $this->markdown($text) dans une vue phtml.

VI - Conclusion

Nous avons pu voir dans cet article comment intégrer des briques logicielles modernes dans une application écrite avec Zend Framework 1, ce qui vous permettra de faciliter l'évolution de vos projets « obsolètes » avec des technologies d'aujourd'hui.

VII - Remerciements

Merci à MaitrePylos et Claude Leloup pour leur relecture attentive.