I. Prérequis

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

Zend Framework 2.0 RequirementsZend Framework 2.0 Requirements

II. Qu'est-ce qu'un module

La notion de module a évolué depuis la première version du framework. Les modules ont pour objectif de pouvoir être indépendant du reste de l'application afin de pouvoir être utilisé de projet en projet. Chaque module possède sa propre configuration, ses contrôleurs et vues, ainsi que tous les fichiers nécessaires à son fonctionnement (PHP, templates, images, Javascript, etc.). Un module réussi est un module qui peut s'adapter facilement dans un autre projet, moyennant quelques réglages de configuration.

Examinons le processus de chargement des modules du Zend Framework 2 afin de cibler les points forts comme les points faibles.

III. Chargement des modules

Avant de procéder à l'initialisation de l'application avec le bootstrap, le gestionnaire de modules est le premier composant à intervenir en se chargeant d'instancier les classes de modules, ainsi qu'en compilant leur configuration.

L'initialisation du gestionnaire de modules se réalise depuis le fichier index de notre application :

index.php
Sélectionnez
$appConfig = include 'config/application.config.php';

$listenerOptions  = new Zend\Module\Listener\ListenerOptions($appConfig['module_listener_options']);
$defaultListeners = new Zend\Module\Listener\DefaultListenerAggregate($listenerOptions);

$moduleManager = new Zend\Module\Manager($appConfig['modules']);
$moduleManager->events()->attachAggregate($defaultListeners);
$moduleManager->loadModules();

Le composant Zend\Module\Listener\DefaultListenerAggregate attache la liste des écouteurs nécessaires à l'initialisation des modules :

Zend\Module\Listener\DefaultListenerAggregate.php
Sélectionnez
public function attach(EventCollection $events)
{
    $options = $this->getOptions();
    $configListener = $this->getConfigListener();
    $locatorRegistrationListener = new LocatorRegistrationListener($options);
    $moduleAutoloader = new ModuleAutoloader($options->getModulePaths());

    $this->listeners[] = $events->attach('loadModules.pre', array($moduleAutoloader, 'register'), 1000);
    $this->listeners[] = $events->attach('loadModule.resolve', new ModuleResolverListener, 1000);
    $this->listeners[] = $events->attach('loadModule', new AutoloaderListener($options), 2000);
    $this->listeners[] = $events->attach('loadModule', new InitTrigger($options), 1000);
    $this->listeners[] = $events->attachAggregate($locatorRegistrationListener);
    $this->listeners[] = $events->attachAggregate($configListener);

    return $this;
}

L'objet $configListener, de type ConfigListener, s'occupe de mettre à jour la configuration globale des modules. Il écoute, entre autres, l'évènement de fin de chargement d'un module et reçoit sa configuration afin de la fusionner avec les autres. Il est aussi responsable de la mise à jour du cache de configuration, si celui-ci est activé. L'objet LocatorRegistrationListener est responsable du partage des instances de modules au sein du gestionnaire d'instances et l'écouteur de type InitTrigger gère l'initialisation des modules depuis leur méthode init(). Les deux écouteurs de type ModuleAutoloader et ModuleResolverListener sont responsables de l'instanciation du module, alors que l'objet de type AutoloaderListener enregistre les autoloaders définis depuis les modules.

Chaque composant est responsable d'une tâche précise et ceci pour chacun des modules à charger. Le gestionnaire de modules n'a alors plus qu'à lancer les évènements correspondant à chacune des tâches :

Zend\Module\Manager.php
Sélectionnez
public function loadModules()
{
    if (true === $this->modulesAreLoaded) {
        return $this;
    }

    $this->events()->trigger(__FUNCTION__ . '.pre', $this, $this->getEvent());

    foreach ($this->getModules() as $moduleName) {
        $this->loadModule($moduleName);
    }

    $this->events()->trigger(__FUNCTION__ . '.post', $this, $this->getEvent());

    $this->modulesAreLoaded = true;
    return $this;
}

Évidemment, tout ce processus se réalise pour chacun des modules. Seule la fusion des configurations peut être évitée si l'utilisation du cache interne est activée.

Nous comprenons alors que tous les modules seront initialisés et chargés en mémoire, même si ceux-ci ne sont pas utilisés.

IV. Chargement sur demande

Le cache de configuration permet de passer l'étape de la fusion des configurations. L'instanciation et l'initialisation des modules seront, par contre, toujours effectuées.
Cependant, la majorité des requêtes effectuées par l'utilisateur concernent une page de la partie front de l'application. Pourquoi est-il nécessaire de charger le module d'administration, le module de notre blog ou encore le module spécialement créé pour nos tâches automatisées ? L'extension ZFMLL (Zend Framework Module Lazy Loading) permet, suivant des règles spécifiques, de ne charger que les modules dont aura besoin l'utilisateur. Par exemple, nous pouvons associer le module d'administration à un accès au port 443 ou depuis un accès en HTTPS. Il est aussi possible de définir le chargement du module concernant la crontab uniquement depuis la version CLI de PHP, etc. Voici la liste des règles disponibles :

- nom de l'interface utilisée (SAPI) ;
- argument depuis la ligne de commande ;
- nom de domaine ;
- accès HTTPS ;
- numéro de port utilisé ;
- URL d'accès ;
- adresse IP de l'internaute.

Toutes ces règles vont permettre le chargement des modules uniquement sur la vérification de ces conditions. L'ajout de cette extension ne nécessite aucun changement du côté du développeur, si ce n'est le remplacement des namespaces « Zend » par « ZFMLL », dans le fichier index.php:

index.php
Sélectionnez
$appConfig = include 'config/application.config.php';

$listenerOptions  = new ZFMLL\Module\Listener\ListenerOptions($appConfig['module_listener_options']);
$defaultListeners = new ZFMLL\Module\Listener\EnvironmentListenerAggregate($listenerOptions);

$moduleManager = new ZFMLL\Module\Manager($appConfig['modules']);
$moduleManager->events()->attachAggregate($defaultListeners);
$moduleManager->loadModules();

Voici maintenant notre nouveau fichier de configuration:

application.config.php
Sélectionnez
<?php
return array(
    'modules' => array(
        'Application',
        'Cron',
        'Administration',
        'Blog',
    ),
    'module_listener_options' => array( 
        'config_cache_enabled' => false,
        'cache_dir'            => 'data/cache',
        'module_paths' => array(
            'Application' => './module/Application',
            'Cron' => './module/Cron',
            'Administration' => './module/Administration',
            'Blog' => './module/Blog',
        ),
        'lazy_loading' => array(
            'Cron' => array(
                'getopt' => array('cron=s' => 'cron url'),
                'sapi' => 'cli',
            ),
            'Administration' => array(
                'port' => 443,
                'remote_addr' => array('127.0.0.1'),
            ),
            'Blog' => array(
                'domain' => 'blog.zend-framework-2.fr',
                'url'  => array('regex' => '/blog/.*' ),
            ),
        ),
    ),
);

La définition de ces règles permettent de ne charger que le module « Application » lors de l'accès à la page d'accueil par exemple. Le module d'administration sera initialisé uniquement si le port utilisé est le port 443, et que l'adresse IP de l'utilisateur fait partie de la liste autorisée.

D'après le benchmark que j'ai effectué, les gains, avec ces quatre modules d'exemple, peuvent atteindre 100%, soit diviser le temps de chargement par deux. Cependant, dans un cas réel, le nombre de modules peut être plus important et les instructions PHP dans les classes de modules plus nombreuses, ce qui aurait pour conséquence d'obtenir un gain plus important. Le benchmark a été effectué sans l'activation du cache de configuration interne. Ces chiffres concernent uniquement la partie front, partie la plus demandée par les utilisateurs.

V. Conclusion et ressources

Cette nouvelle notion des modules est un concept qui manquait fortement au Zend Framework 1. Il est possible de retrouver la liste des modules disponibles depuis l'adresse Image non disponiblemodules.zendframework.commodules.zendframework.com. Vous pouvez aussi retrouver mon extension sur mon compte Github Zend Framework Module Lazy LoadingZend Framework Module Lazy Loading en téléchargement libre.

Cet article est tiré en partie du Image non disponible livre "Au cœur de Zend Framework 2"Livre "Au coeur de Zend Framework 2", disponible dans le début du quatrième trimestre de l'année 2012. Dans ce livre, vous retrouverez en détail le fonctionnement des modules et de leurs configurations.

Retrouvez mes modules ou autres fonctionnalités, créés pour le Zend Framework 2, sur mon compte github à l'adresse https://github.com/blanchonvincenthttps://github.com/blanchonvincent.