Tous droits réservés

Configurer Doctrine pour la production

Maintenant que notre application est performante, nous allons voir la configuration optimale à mettre en place avant de la mettre en production.

Nous allons voir dans ce chapitre les différents paramètres de configuration recommander pour que Doctrine fonctionne de manière optimale.

Lorsque nous avons mis en place la configuration de Doctrine, nous avons passé sous silence une bonne partie de ses mécanismes internes. Le fichier de configuration a été écrit pour un environnement de développement.

Pour une mise en production, nous devons revoir un certain nombre de points.

La gestion du cache

Quelles sont les informations mises en cache par Doctrine ?

Pour son bon fonctionnement, Doctrine gère trois (3) types de données :

  • les réponses des requêtes exécutées ;
  • les requêtes SQL (une fois le DQL traduit en SQL) ;
  • et enfin les annotations sur les entités (une fois parsées).

La mise en cache de ses éléments est nécessaire pour le bon fonctionnement de Doctrine. Avec notre configuration actuelle, ces informations sont juste stockées dans un tableau PHP. Donc à chaque exécution de nos scripts, le cache doit être régénéré.

En production, un tel comportement n’est pas envisageable. Dès lors, Doctrine fournit nativement le support de plusieurs systèmes de cache plus persistant qu’un simple tableau PHP. On peut citer entre autres :

Type de cache Implémentation Doctrine Description
APCu - APC User Cache Doctrine\Common\Cache\ApcuCache Extension PHP de cache mémoire
redis Doctrine\Common\Cache\RedisCache Serveur de cache mémoire
MongoDB Doctrine\Common\Cache\MongoDBCache Base de données NoSQL

Si vous avez des doutes sur le choix à faire, l’extension APCu fera largement l’affaire si vous utilisez PHP 7.

C’est un usage moins courant mais il est aussi possible de créer soi-même un système de cache en implémentant l’interface Doctrine\Common\Cache\Cache.

Les proxies

Lorsque nous récupérons une classe depuis les repositories, Doctrine peut utiliser en interne des classes intermédiaires qui étendent les nôtres. Ces classes sont appelées des proxies. C’est d’ailleurs grâce à ces proxies que le système de lazy-loading peut fonctionner. Et c’est l’une de leur principale utilité.

Pour s’en rendre compte, nous allons modifier la configuration de Doctrine. Dans l’entité utilisateur, supprimons tous les fetch EAGER que nous avions configurés et exécutons le code suivant.

<?php
# get-user-class.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$userRepo = $entityManager->getRepository(User::class);

$user = $userRepo->find(1);
echo get_class($user->getAddress());

La réponse est :

DoctrineProxies\__CG__\Tuto\Entity\Address.

Sans nos optimisations, la classe Address est chargée en mode lazy-loading. Doctrine utilise donc un proxy.

Cette classe est déclarée dans le dossier temporaire de votre système (/tmp sur linux, C:\Users\votre_utilisateur\AppData\Local\Temp sur Windows). Vous pouvez trouver ce dossier en utilisant la fonction PHP sys_get_temp_dir.

Voici un extrait de son code :

<?php
namespace DoctrineProxies\__CG__\Tuto\Entity;

/**
 * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR
 */
class Address extends \Tuto\Entity\Address implements \Doctrine\ORM\Proxy\Proxy
{
    // ...

    /**
     * {@inheritDoc}
     */
    public function getStreet()
    {
        $this->__initializer__ && $this->__initializer__->__invoke($this, 'getStreet', []);

        return parent::getStreet();
    }
}

La méthode getStreet comme toutes les méthodes de notre entité, ont été réécrites par Doctrine. Le secret du lazy-loading réside dans ce code.

Mais pourquoi parle-t-on des proxies ?

Avec la configuration actuelle, Doctrine est obligé de régénérer ces classes à chaque exécution. Les performances d’une application en production en pâtiraient énormément. En production, nous devons désactiver cette génération automatique et créer les proxies manuellement grâce à la commande Doctrine appropriée : vendor/bin/doctrine orm:generate-proxies.

Exemple de configuration

Un exemple valant mieux qu’un long discours, voici une configuration qui utilise APCu pour le système de cache et où les proxies sont stockés dans le dossier proxies de notre projet.

<?php
# bootstrap.php

require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'vendor', 'autoload.php']);

use Doctrine\ORM\Tools\Setup;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Proxy\AbstractProxyFactory;
use Doctrine\ORM\EntityManager;
use Doctrine\DBAL\Logging\DebugStack;

$entitiesPath = [
    join(DIRECTORY_SEPARATOR, [__DIR__, "src", "Entity"])
];

$isDevMode = false;
$proxyDir = join(DIRECTORY_SEPARATOR, [__DIR__, "proxies"]);
$cache = null;
if ($isDevMode == false) {
    $cache = new ApcuCache();
}

$useSimpleAnnotationReader = false;

// Connexion à la base de données
$dbParams = [
    'driver'   => 'pdo_mysql',
    'host'     => 'localhost',
    'charset'  => 'utf8',
    'user'     => getenv("TUTO_DATABASE_USER"),
    'password' => getenv("TUTO_DATABASE_PASSWORD"),
    'dbname'   => 'poll',
];

$config = Setup::createAnnotationMetadataConfiguration(
    $entitiesPath,
    $isDevMode,
    $proxyDir,
    $cache,
    $useSimpleAnnotationReader
);

if ($isDevMode) {
    $config->setSQLLogger(new DebugStack());
}

if ($isDevMode == false) {
    $config->setAutogenerateProxyClasses(AbstractProxyFactory::AUTOGENERATE_NEVER);
}

$entityManager = EntityManager::create($dbParams, $config);

return $entityManager;

La variable $isDevMode permet de revenir sur la configuration en mode développement facilement.

En essayant d’exécuter le script get-user-class.php, une erreur fatale est affichée avec un message du style : Fatal error: require(): Failed opening required '/dossier/proxies/__CG__TutoEntityAddress.php'.

Nous devons générer manuellement les proxies :

 #vendor/bin/doctrine orm:generate-proxies 

Processing entity "Tuto\Entity\Address"
Processing entity "Tuto\Entity\Answer"
Processing entity "Tuto\Entity\Choice"
Processing entity "Tuto\Entity\Participation"
Processing entity "Tuto\Entity\Poll"
Processing entity "Tuto\Entity\Question"
Processing entity "Tuto\Entity\User"

Proxy classes generated to "/dossier/proxies"

Notre application est maintenant prête pour une mise en production. Les configurations présentées ici sont obligatoires si vous voulez avoir une application performante.

Surtout n’oubliez pas de régénérer les proxies et à vider le système de cache à chaque nouvelle mise en production pour éviter les bugs et incohérences.