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 :
<?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 :
- Composer ;
- 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) :
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 :
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 :
"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.
pluginPaths.My_Resource = APPLICATION_PATH "/resources"
resources.dic.configfile = APPLICATION_PATH "/configs/services.yml"
Voici le code de la classe de ressource spécifique :
<?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 :
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 :
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 :
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) :
<?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 :
<?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 :
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 :
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 :
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 » :
<?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.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) :
resources.dic.params.serializer.serializenull = true
Et voici la classe de stratégie de nommage :
<?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 :
<?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 :
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 :
resources.view.helperPath = APPLICATION_PATH "/helpers/view"
Enfin, créons le helper de vue suivant :
<?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.