Doctrine2: Désactiver temporairement les événements d’un listener

Pré-requis: Symfony2 et Doctrine2

Dans ce bref article, je vais vous montrer la possibilité de désactiver temporairement un ou des événement(s) d’un listener. Nous allons imaginer un scénario ou lorsque l’on modifie une entité, notre listener envoit un email à l’administrateur du site.

Voici un exemple de listener pour illustrer notre cas:


<?php

namespace MyProject\FooBundle\Listener;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;

class FooListener implements EventSubscriber
{
    public function getSubscribedEvents()
    {
        return array(
            Events::prePersist,
            Events::preUpdate,
            Events::postPersist,
            Events::postUpdate
        );
    }

    ...
}

Le listener ci-dessus est défini comme service avec le nom « foo.listener ». Il doit être taggé avec l’attribut « doctrine.event_subscriber » pour que cela fonctionne car nous utilisons les événements de Doctrine2.

Nous allons maintenant mettre en place le code permettant de désactiver les événements du listener lors d’un import de données de masse. Si vous consulter l’API de l’eventManager, vous constaterez qu’il existe la fonction removeEventListener. Elle reçoit comme paramètres un tableau d’événement(s) ainsi que la référence de notre listener.

<?php

...
$em = $this->getContainer()->get('doctrine')->getManager();
$eventManager = $em->getEventManager();

$eventManager->removeEventListener(
    array('prePersist', 'preUpdate', 'postPersist', 'postUpdate'),
    $this->getContainer()->get('foo.listener')
);

Avec le code ci-dessus, tous les événements mentionnés (‘prePersist’, ‘preUpdate’, ‘postPersist’, ‘postUpdate’) seront désactivés.

Voilà. J’espère que cette petite astuce vous servira dans vos prochains développements.

Share

Doctrine 2.2, SoftDeleteable behavior et Symfony 2

Depuis la sortie de la version 2 de Doctrine, il manquait cruellement l’extension SoftDelete qui vous permet de gérer la suppression de vos enregistrements sans réellement les détruire. Depuis peu, Gediminas Morkevicius alias l3pp4rd a implémenter cela dans ses extensions dédiées à Doctrine 2. Ci-dessous, je vais vous montrer comment changer la version de doctrine dans Symfony2 et sa configuration.

Pour cela, nous allons modifier le fichier deps à la racine de votre projet. Nous allons changer les versions de doctrine pour passer à la version 2.2 et ajouter les extensions:

[doctrine-common]
    git=http://github.com/doctrine/common.git
    version=2.2.1

[doctrine-dbal]
    git=http://github.com/doctrine/dbal.git
    version=2.2.1

[doctrine]
    git=http://github.com/doctrine/doctrine2.git
    version=2.2.1

[gedmo-doctrine-extensions]
    git=http://github.com/l3pp4rd/DoctrineExtensions.git

Passons maintenant à la définition du namespace pour ces extensions. Modifions le fichier autoload.php se trouvant dans le dossier app

registerNamespaces(array(
    ...
    'Gedmo' => __DIR__.'/../vendor/gedmo-doctrine-extensions/lib',
));
...

Nous pouvons maintenant déclarer le service dont nous avons besoin. Ouvrons le fichier services.xml dans le dossier « Resources/config »:

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        ...
        <parameter key="gedmo.softdeleteable.listener.class">Gedmo\SoftDeleteable\SoftDeleteableListener</parameter>
    </parameters>
    <services>
        ...
        <service id="gedmo.listener.softdeleteable" class="%gedmo.softdeleteable.listener.class%">
            <tag name="doctrine.event_subscriber" />
            <call method="setAnnotationReader">
                <argument type="service" id="annotation_reader" />
            </call>
        </service>
    </services>
</container>

Chargeons maintenant le filtre qui va modifier vos requêtes lors de l’utilisation de softDelete. Pour cela, nous allons ajouter quelques lignes au boot de notre Bundle. Dans mon cas, j’ai toujours un bundle Core dans mes projets, voici le code à insérer:

<?php

namespace Funstaff\CoreBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class FunstaffCoreBundle extends Bundle
{
    public function boot()
    {
        $doctrine = $this->container->get('doctrine');
        $doctrine->getEntityManager()->getConfiguration()->addFilter(
            'soft-deleteable',
            'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter'
            );

        $em = $doctrine->getEntityManager();
        $em->getFilters()->enable('soft-deleteable');
    }
}

La nouvelle extension est dès maintenant activée.

Pour l’utiliser, nous allons ajouter une annotation sur notre entité comme indiqué ci-dessous:

<?php 

namespace Funstaff\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as GEDMO;

/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 * @GEDMO\SoftDeleteable(fieldName="deletedAt")
 */
class User
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    ...
}

J’espère que cette petite explication vous servira pour vos prochains développements.

Pour en savoir plus sur l’utilisation de celle-ci, veuillez consulter la documentation officielle qui se trouve ici.

Bon tests 😉

Share

Multiples connexions doctrine et le chargement des modèles

Si comme moi, vous avez besoin de travailler avec les multiples connexions de doctrine, vous avez été probablement confronté au bug de la gestion des modèles. Comme les fichiers modèles ne sont pas chargés et que le tableau des loadedModelFiles est vide, doctrine n’instancie pas correctement la connexion, vous vous retrouvez avec la dernière définition d’accès à votre base. Je vous propose ci-dessous une solution à ce problème.

Nous allons donc nous connecter à l’événement « doctrine.configure » dans la configuration de notre projet et définir une fonction qui fera le chargement des modèles. Voici le code à intégrer:

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    ...
    $this->dispatcher->connect('doctrine.configure', array($this, 'listenToConfigureDoctrineEvent'));
  }
  
  public function listenToConfigureDoctrineEvent(sfEvent $event)
  {
    if (!Doctrine_Core::getLoadedModelFiles())
    {
      self::loadModelFiles();
    }
  }
  
  
  protected static function loadModelFiles()
  {
    $dir = sfConfig::get('sf_lib_dir').'/model/doctrine';
    $it = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir),
                RecursiveIteratorIterator::LEAVES_ONLY);

    foreach ($it as $file)
    {
      $className = str_replace($dir . DIRECTORY_SEPARATOR, null, $file->getPathName());
      $className = substr($className, 0, strpos($className, '.'));
      Doctrine_Core::loadModel(basename($className), $file->getPathName());
    }
  }
}

Voilà. Avec cette nouvelle configuration, vous ne devriez plus avoir de problèmes.

Share

Travailler avec les relations Many to Many de doctrine

Dans ce nouvelle article, j’ai décidé d’expliquer comment travailler avec les relations M:M de doctrine. La définition de celle-ci dans le fichier schema.yml est assez spéciale. Il faut bien comprendre son fonctionnement pour pouvoir optimiser vos requêtes. Pour cela, j’ai monté un petit exemple concret.

Nous allons commencer par l’écriture de notre modèle au format yml:

---
User:
  tableName:          user
  actAs:
    Timestampable:    ~
  columns:
    id:
      type:           integer(4)
      primary:        true
      autoincrement:  true
      unsigned:       true
    lastname:
      type:           string(80)
      notnull:        true
    firstname:
      type:           string(80)
      notnull:        true
  relations:
    MCategories:
      class:          Category
      local:          user_id
      foreign:        category_id
      refClass:       UserCategory
      foreignAlias:   Users

UserCategory:
  tableName:          user_category
  columns:
    user_id:
      type:           integer(4)
      unsigned:       true
      primary:        true
    category_id:
      type:           integer(4)
      unsigned:       true
      primary:        true
  relations:
    User:
      onDelete:       CASCADE
    Category:
      onDelete:       CASCADE

Category:
  tableName:          category
  actAs:
    Timestampable:    ~
  columns:
    id:
      type:           integer(4)
      primary:        true
      autoincrement:  true
      unsigned:       true
    name:
      type:           string(80)
      notnull:        true
  relations:
    MUsers:
      class:          User
      local:          category_id
      foreign:        user_id
      refClass:       UserCategory
      foreignAlias:   Categories

Comme vous pouvez le voir sur la définition de nos relations, nous n’écrivons pas sur la table de liaison mais sur la table principale (ici User et Category). Nous commençons par leur donner un nom. Ensuite, nous allons insérer toutes les options:

  • Class: correspond au modèle de la liaison finale (Category)
  • local et foreign: correspondent aux champs définis dans votre table de liaison.
  • refClass: Nom du modèle de liaison (UserCategory)
  • foreignAlias: Le nom de l’alias qui sera donné à notre table finale (Category)

Un petit fichier de fixtures pour avoir des données de test:

---
User:
  user_1:
    firstname:    Adrien
    lastname:     Loutier
    MCategories:   [cat_1, cat_2, cat_3]
  user_2:
    firstname:    Simon
    lastname:     Jacquemoud
    MCategories:   [cat_2, cat_4]
  user_3:
    firstname:    Raphaëlle
    lastname:     Tabouret
    MCategories:   [cat_1, cat_2, cat_3, cat_4]
  user_4:
    firstname:    Justine
    lastname:     Simonin
    MCategories:   [cat_2, cat_3, cat_4, cat_5]

Category:
  cat_1:
    name:         Niveau 1
  cat_2:
    name:         Niveau 2
  cat_3:
    name:         Niveau 3
  cat_4:
    name:         Niveau 4
  cat_5:
    name:         Niveau 5

J’ai ensuite généré un module « user » pour pouvoir y insérer mon code. Je passe sur les explications de cette génération.

Nous avons maintenant deux solutions. Soit nous laissons travailler doctrine sans optimisation, soit nous définissons notre requête pour avoir un minimum d’appels sur la base de données.

Dans le premier exemple, nous allons laisser faire doctrine. Nous allons simplement appeler nos users dans le fichier actions.class.php de notre module:

class userActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
    $this->users = Doctrine_Core::getTable('User')
    ->createQuery()
    ->execute();
  }
}

Affichage de nos données dans notre template. Ici indexSuccess.php

<h1>Liste des utilisateur</h1>
<?php foreach ($users as $user): ?>
<p>
  <?php echo $user->firstname; ?> <?php echo $user->lastname; ?>
  <ul>
  <?php foreach($user->getCategories() as $categorie): ?>
  <li class="list"><?php echo $categorie->name; ?></li>
  <?php endforeach; ?>
  </ul>
</p>
<?php endforeach; ?>

Dans le code ci-dessus, j’utilise le get{Categories} (foreignAlias de la relation sur la table User) pour récupérer mes catégories. Vous n’avez pas besoin d’appeler les enregistrements de la table de liaison.

Nous avons le résultat suivant en html:

Nous allons maintenant visualiser le nombre de requêtes effectuées par doctrine dans notre barre de debug (Cliquez sur l’image pour visualiser):

Nous constatons que doctrine exécute 5 requêtes (une par personne pour récupérer les catégories).

Nous allons maintenant optimiser notre récupération de données en écrivant une requête DQL dans le modèle User. Le fichier se trouve dans lib/model/doctrine/UserTable.class.php:

class UserTable extends Doctrine_Table
{
  public function getActiveCategories()
  {
    return $this->createQuery('u')
    ->leftJoin('u.Categories g')
    ->execute();
  }
}

Dans la requête ci-dessus, nous utilisons également le nom de la foreignAlias pour écrire notre jointure.

Nous allons changer notre précédente requête dans l’action index par celle-ci:

class userActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
    $this->users = Doctrine_Core::getTable('User')->getActiveCategories();
  }
}

Nous retournons dans notre barre de debug pour visualiser les requêtes (Cliquez sur l’image pour visualiser):

Comme vous pouvez le constater ci-dessus, avec l’optimisation du DQL, Doctrine exécute une seule requête.

Voilà. J’espère que ce petit exemple pourra vous servir pour vos prochains développements. N’hésitez pas à me laisser vos commentaires.

Share

Symfony: Contrôle de la disponibilité de la base de données avec un event

Symfony, depuis la version 1.2, offre un système d’événements (sfEventDispatcher et sfEvent). Cela va permettre la mise en place de notre contrôle. Pour cela nous allons nous connecter à l’événement « doctrine.configure_connection ». Nous allons effectuer cela dans notre application configuration (frontend) avec le code ci-dessous:

class frontendConfiguration extends sfApplicationConfiguration
{
  public function configure()
  {
    $this->dispatcher->connect('doctrine.configure_connection', array($this, 'listenToAddCheckDatabaseConnection'));
  }
  
  public function listenToAddCheckDatabaseConnection(sfEvent $event)
  {
    $actions = $event->getSubject();
    $actions->getCurrentConnection()->connect();
    sfConfig::set('sf_database_is_connected', $actions->getCurrentConnection()->isConnected());
  }
}

Nous allons ensuite intercepter notre nouvelle configuration en personnalisant notre frontController. Pour cela, nous allons ajouter une nouvelle classe dans notre projet sous lib/controller/mySfFrontWebController.class.php

class mySfFrontWebController extends sfFrontWebController
{
  public function dispatch()
  {
    if (!sfConfig::get('sf_database_is_connected', true))
    {
      $request    = $this->context->getRequest();
      $moduleName = $request->getParameter('module');
      $actionName = $request->getParameter('action');
      
      if ((!$module = sfConfig::get('app_database_not_connected_module')) ||
      (!$action = sfConfig::get('app_database_not_connected_action')))
      {
        throw new sfConfigurationException('Missing parameter(s): app_database_not_connected_module and app_database_not_connected_action are required in app.yml');
      }
      else
      {
        if (($moduleName != $module) && ($actionName != $action))
        {
          $this->forward($module, $action);
          exit;
        }
      }
    }
    parent::dispatch();
  }
}

Pour charger notre nouvelle classe, nous allons le faire dans le fichier factories.yml de notre application (frontend):

all:
  controller:
    class: mySfFrontWebController

Nous allons définir nos nouveaux paramètres dans le fichier app.yml de notre application (frontend):

all:
  database_not_connected:
    module:       main
    action:       notconnected

Il nous reste maintenant à définir notre nouvelle action dans le module main (ou autre).

Voilà. La mise en place du contrôle de connexion sur notre base de données est terminée.

J’espère que ce petit bout de code vous servira.

Share

Symfony: Afficher un message en cas de non disponibilité de la base de données

Symfony ne proposant pas une fonctionnalité me permettant de définir un message en cas de non disponibilité de la base de données, j’ai réalisé un filtre pour contrôler cela. Pour le rendre flexible, j’ai ajouté deux options permettant la définition du module et de l’action appelé lors de l’erreur.

J’ai commencé par créer dans mon module default, une action checkAvailibility me permettant de réaliser un template pour l’affichage du message (je passe sur cette étape car je pense que vous savez le faire). Ensuite, il suffit de les définir dans le fichier app.yml:

all:
  checkdb:
    module: default
    action: checkAvailibility

J’ai ensuite créé mon fichier filtre appelé « checkAvailibilityDbFilter.class.php » dans le dossier /lib de mon projet:

class checkAvailibilityDbFilter extends sfFilter
{
  public function execute($filterChain)
  {
    if ($this->isFirstCall())
    {
      $context = $this->getContext();

      $module = sfConfig::get('app_checkdb_module', 'default');
      $action = sfConfig::get('app_checkdb_action', 'error404');

      if (($module != $context->getModuleName()) || ($action != $context->getActionName()))
      {
        $configuration = sfProjectConfiguration::getActive();
        $db = new sfDatabaseManager($configuration);
        
        foreach ($db->getNames() as $connection)
        {
          try
          {
            @$db->getDatabase($connection)->getConnection();
          } 
          catch(Exception $e)
          {
             $context->getController()->forward($module, $action);
             exit;
          }
        }
      }
    }
    
    $filterChain->execute();
  }
}

Il reste encore à activer ce filtre pour que cela fonctionne. J’ai ajouté les lignes suivantes dans le fichier filters.yml du dossier config de l’application:

rendering: ~
security:  ~

# insert your own filters here
db:
  class:  checkAvailibilityDbFilter

cache:     ~
common:    ~
execution: ~

J’espère que cette petite astuce vous sera utile.

Share

Le cache de résultat avec doctrine

Note préalable: Nous utilisons dans cette article le système de cache php APC. Vous devez installer celui-ci sur votre configuration pour que l’exemple ci-dessous fonctionne.

Doctrine a en standard la possibilité de mettre en cache les résultats d’une requête. Il propose entre autre Memcached, APC, Sqlite. Nous allons l’utiliser dans le cadre d’un exemple symfony.

Pour commencer, nous allons initialiser une variable contenant la durée de vie du cache dans le fichier app.yml de notre application:

all:
  cache:
    lifetime: 3600

Nous allons maintenant initialiser le cache APC de doctrine. Le code ci-dessous est à mettre dans la classe de configuration de votre application:

class frontendConfiguration extends sfApplicationConfiguration
{ 
  public function configure()
  {
  }

  public function configureDoctrine(Doctrine_Manager $manager)
  {
    /* Initialisation du cache Doctrine APC */
    $cacheDriver = new Doctrine_Cache_Apc();
    $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime'));
    $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime'));
  }
}

Pour cette exemple, j’utilise le modèle suivant:

---
dUser:
  tableName:  d_user
  columns:
    id:
      type: integer(3)
      primary:  true
      autoincrement:  true
    fullname:
      type: string(160)
      notnull:  true
    street:
      type: string(160)
    zip:
      type: string(10)
    city:
      type: string(80)
    country:
      type: string(60)

Dans la classe du modèle ci-dessus, nous allons utiliser le cache. La directive useResultCache est ajouté à la requête.

class dUserTable extends Doctrine_Table
{
  public function findFullnameOrderByAsc()
  {
      return $this->createQuery('u')
                         ->select('u.fullname, u.city')
                         ->orderBy('fullname')
                         ->useQueryCache()
                         ->useResultCache()
                         ->execute();
  }
}

Il vous suffit ensuite d’exécuter votre requête dans votre action avec le code suivant pour récupérer le résultat.

$this->users = Doctrine::getTable('dUser')->findFullnameOrderByAsc();

Si vous utilisez la debug toolbar, vous constaterez qu’à la première requête, vous aurez 1 appel à la base de donnée mais que si vous rechargez votre page html, aucune nouvelle requête n’est lancée. Le cache fonctionne.

J’espère que ce point de départ vous aidera dans vos découvertes. N’hésitez pas à poster vos découvertes dans les commentaires.

Merci à [MA]Pascal pour la piste de configuration.

Références:
Documentation Doctrine sur le cache
La documentation du cache PHP alternatif APC
L’extension php apc

Article:
Performance Tuning in PHP

Share

Initialisation d’un projet symfony/doctrine sous subversion

La première phase consiste à créer notre dossier http. Mon path par défaut pour cette exemple est /www/virtualhosts. Les noms entre crochets sont utilisés pour un élément variable.

mkdir /www/virtualhosts/[projet]

Nous allons maintenant nous déplacer dans ce dossier et le déchargement de la base depuis subversion

cd /www/virtualhosts/[projet]
svn co http://[svn.server]/repos/[project]/trunk .

Nous pouvons maintenant installer le framework symfony dans notre dossier http

/www/svn/symfony/1.2/data/bin/symfony generate:project [NomDuProjet]

Nous allons maintenant configurer la base de donnée avec doctrine:

./symfony configure:database --name=doctrine --class=sfDoctrineDatabase "mysql:host=localhost;dbname=database" user pass

Modification du fichier databases.yml:

cd config/
Suppression des lignes concernant Propel dans le fichier databases.yml
(ne laisser que la config doctrine)

Nous renommons le fichier databases.yml avant de l’inclure dans le repository

mv databases.yml databases.yml_dist

Activation du plugin sfDoctrinePlugin

Ouvrir le fichier ProjectConfiguration.class.php et changer sfDoctrinePlugin par sfPropelPlugin

Suppression des fichiers inutiles

rm -fr propel.ini
rm -fr schema.yml

Création du dossier doctrine qui va recevoir les fichiers schéma

mkdir doctrine

Suppression du dossier sfPropelPlugin du dossier web et activation du dossier sfDoctrinePlugin

cd ../web/
unlink sfPropelPlugin
cd ..
./symfony plugin:publish-assets

Prochaine étape, vider les dossiers cache et log

rm -fr cache/*
rm -fr log/*

Ajout de notre projet dans subversion

svn add *

Nous allons ignorer les fichiers des dossiers cache et log

svn pe svn:ignore cache
> *
svn pe svn:ignore log
> *

Nous mettons également le fichier databases.yml en ignore pour ne pas tenir compte de la configuration locale

svn pe svn:ignore config
> databases.yml

Création du dossier sql recevant les fichiers sql de doctrine. Je considère que ces fichiers ne sont pas à versionner. Nous allons également les ignorer.

mkdir data/sql
svn pe svn:ignore data/sql
> *

Nous allons inclure symfony dans notre projet

svn mkdir lib/vendor
svn pe svn:externals lib/vendor
> symfony http://svn.symfony-project.com/branches/1.2

Transfert de notre structure initiale sur le serveur subversion

svn ci -m 'Projet initial'

Nous allons maintenant lancer un update pour charger le framework symfony qui a été précédemment accroché dans le dossier lib/vendor

svn up

Nous allons changer le chemin d’accès aux librairies symfony en modifiant le fichier ProjectConfiguration.class.php du dossier config du projet

Changer:
require_once '/www/svn/symfony/1.2/lib/autoload/sfCoreAutoload.class.php'
Par:
require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';

Nous allons publié notre modification sur le serveur subversion

svn ci -m "Changement de la configuration"

Il nous reste une dernière chose à faire pour que notre projet fonctionne. Nous allons copier le fichier databases.yml_dist et le renommer avant de le modifier pour tenir compte de notre configuration locale

cp config/databases.yml_dist config/databases.yml

Il ne reste plus qu’à initialiser notre application avant de pouvoir développer

./symfony generate:app frontend --csrf-secret=CrSfS3Cr3t --escaping-strategy=on

Ajout de l’application à subversion

svn add test/functional/frontend apps/frontend web/frontend_dev.php web/index.php

Publication sur le serveur subversion

svn ci -m "Initialisation de l'application frontend"

J’espère que cette démarche vous permettra de simplifier l’installation de vos projets. Je reste à votre disposition si vous avez des questions sur le sujet.

Références:
Le Framework Symfony
l’ORM Doctrine
Subversion

Share