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

Réaliser un système de Timeout pour Symfony2

[MAJ] Une petite précision sur ce que fait réellement cette fonctionnalité. Cela me permet de déconnecter un utilisateur de son interface si l’activité est interrompu pendant plus de 30 minutes. C’est un reset de session automatique.

Symfony ne fournissant pas cette fonction dans son framework, je vais vous montrer sa mise en place avec un listener. J’ai pour habitude d’avoir dans mes développements, un bundle Core qui me permet de centraliser les choses du mon projet.

Nous allons commencer par définir un paramètre dans notre arbre de configuration. Cela nous permettra de le renseigner ensuite dans notre fichier config.yml se trouvant dans le dossier app/config. Voici le code se trouvant dans le fichier CoreBundle/DependencyInjection/configuration.php

namespace Funstaff\CoreBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritDoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('funstaff_core');

        $rootNode
            ->children()
                ->scalarNode('timeout')->defaultValue(3600)
                ->isRequired()->end()
            ->end();

        return $treeBuilder;
    }
}

Nous pouvons maintenant injecter notre paramètre dans le container. Pour cela, nous allons ajouter ce code dans le fichier FunstaffCoreExtension.php

namespace Funstaff\CoreBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

class FunstaffCoreExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
    	$configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\XmlFileLoader($container,
                         new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');

        $container->setParameter('core.timeout', $config['timeout']);
    }
}

Dernière chose à faire avant d’implémenter notre listener, renseigner ce paramètre dans notre fichier config.yml. La valeur est exprimée en seconde.

funstaff_core:
    timeout:    1800 # 30 minutes

Pour respecter l’arborescence des dossiers, j’ai créé mon fichier RequestListener.php dans le path suivant: FunstaffCoreBundle/Request/Listener.

namespace Funstaff\CoreBundle\Request\Listener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class RequestListener implements EventSubscriberInterface
{
    protected $session;

    protected $securityContext;

    protected $timeout;

    /**
     * Construct
     * 
     * @param Session $session
     */
    public function __construct(Session $session,
                                SecurityContext $securityContext,
                                $timeout)
    {
        $this->session = $session;
        $this->securityContext = $securityContext;
        $this->timeout = $timeout;
    }

    /**
     * Get Subscribed Events
     * 
     * @return array event list
     */
    public static function getSubscribedEvents()
    {
        return array(
            'kernel.request' => 'onKernelRequest',
        );
    }

    /**
     * On Kernel Request
     */
    public function onKernelRequest(GetResponseEvent $event)
    {

        if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
            return;
        }

        $meta = $this->session->getMetadataBag();
        $lastused = $meta->getLastUsed();

        if (null !== $lastused && (time() - $lastused) > $this->timeout) {
            $this->securityContext->setToken(null);
            $this->session->invalidate();
        }
    }
}

Il nous reste une dernière chose à faire pour que cela fonctionne. Nous allons attacher notre listener à la request en définissant les éléments dans le fichier Resources/config/services.xml comme ceci:

<?xml version="1.0" ?>

<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="request.listener.class">Funstaff\CoreBundle\Request\Listener\RequestListener</parameter>
    </parameters>

    <services>
        <service id="timeout.request.listener" scope="request">
            <tag name="kernel.event_subscriber"/>
            <argument type="service" id="session" />
            <argument type="service" id="security.context" />
            <argument>%core.timeout%</argument>
        </service>
    </services>
</container>

Je précise pour finir, que je travaille toujours en mode sécurité: anonymous = true. Il vous faudra peut-être modifier un peu le listener ci-dessus dans le cas contraire.

Voilà. Nous en avons terminé avec notre système de timeout. J’espère que cela vous servira dans vos prochains développement.

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

FunstaffTikaBundle: Wrapper pour Tika

Aujourd’hui, je vous propose un petit bundle Symfony2 de ma création. Celui-ci permet d’extraire du contenu et des metadatas sur vos fichiers. Vous avez la liste des fichiers supportés à cette adresse.

Installation

Clone:

git clone https://github.com/Funstaff/FunstaffTikaBundle vendor/bundles/Funstaff/TikaBundle

Ajout en submodule:

git submodule add https://github.com/Funstaff/FunstaffTikaBundle vendor/bundles/Funstaff/TikaBundle

Télécharger le binaire Tika (runnable jar) à cette adresse et déplacer le dans le path de votre choix que vous renseignerez dans votre configuration.

Nous allons ajouter le namespace « Funstaff » dans le fichier autoload.php

$loader->registerNamespaces(array(
    ...
    'Funstaff' => __DIR__.'/../vendor/bundles',
));

Activons maintenant le bundle:

public function registerBundles()
{
    $bundles = array(
        ...
        new Funstaff\TikaBundle\FunstaffTikaBundle(),
    );
}

Sa configuration est très simple. Il vous suffit de déclarer ces éléments dans votre fichier config.yml:

funstaff_tika:
    tika_path:      /path/to/tika-app-1.0.jar
    output_format:  ~
    logging:        ~

Options possibles pour ces paramètres:
tika_path: Chemin sur le binaire Tika
output_format: xml, html ou text (défaut: xml)
jogging: true ou false (si non défini, utilise le paramètres jogging du Symfony2)

Utilisation

Dès maintenant, vous avez accès au service « funstaff.tika ». Voici comment l’utiliser.

$tika = $this->get('funstaff.tika')
        ->setOutputFormat('text')
        ->addDocument('foo', '/path/to/foo')
        ->extractContent();

Dans l’exemple ci-dessus, nous avons fixé le format de sortie au format texte, ajouté le document foo et lancer l’extraction. Nous allons maintenant pouvoir récupérer les informations:

foreach ($tika->getDocuments() as $document) {
    $content = $document->getContent();
}

Vous pouvez ajouter plusieurs documents en rajoutant plusieurs lignes « addDocument ».

Fonctions existantes pour l’extraction:
extractContent: Uniquement le texte
extractMetadata: Uniquement les metadata
extractAll: Texte et métadata

Exemple avec la récupération du texte et des metadatas:

foreach ($tika->getDocuments() as $document) {
    $content = $document->getContent();
    $metadata = $document->getMetadata();
    $author = $metadata->get('Author');
}

Voilà, nous avons effectué le tour du propriétaire. Si ce bundle vous intéresse, vous le trouverez sur github à l’adresse suivante: FunstaffTikaBundle

Si vous désirez me laisser votre feedback: Github issue

Share

Symfony2, Assetic, less et yui compressor: Installation sur Mac

Après un petit moment de silence, voici une petite publication concernant l’installation de LESS pour pouvoir l’utiliser avec Symfony2 et Assetic. Pour cela, nous allons utiliser MacPorts. Vous devez posséder les droits administrateur pour le faire.

L’utilisation de LESS demande une installation de Node.js ainsi que Node Package Manager.

sudo port install nodejs
sudo port install npm

Nous allons contrôler que nos 2 éléments ci-dessous soient bien installés.

$ node --version
v0.4.11
$ npm --version
1.0.26

Avec les deux commandes ci-dessous, vous avez maintenant la base. Il nous reste à installer LESS:

sudo npm install -g less

Vous avez la possibilité de voir les packages installés avec la commande suivante:

npm list -g

Il nous reste à installer YUI-Compressor. Vous pouvez télécharger la dernière version chez yahoo. Copier le fichier « yuicompressor-2.4.6.jar » du dossier build dans le dossier app/Resources/java.

Passons maintenant à la configuration d’assetic. Nous allons ajouter quelques lignes dans le fichier se trouvant dans app/config/config.yml:

# Assetic Configuration
assetic:
    debug:          %kernel.debug%
    use_controller: false
    filters:
        cssrewrite: ~
        less:
            node:       /opt/local/bin/node
            node_paths: [/opt/local/lib/node, /opt/local/lib/node_modules]
        yui_css:
            jar: %kernel.root_dir%/Resources/java/yuicompressor-2.4.6.jar
        yui_js:
            jar: %kernel.root_dir%/Resources/java/yuicompressor-2.4.6.jar

Il nous reste à ajouter quelques lignes dans notre layout de base dans la partie head:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        {% stylesheets
            '@FunstaffCoreBundle/Resources/assets/less/foo.less'
             filter='less,?yui_css'
             output='css/foot.css'
             %}
            <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
        {% endstylesheets %}
        ...
    </head>
    <body>
        ...
    </body>
</html>

Vous nouvelle configuration est prête à être utilisé.

Encore une dernière chose, si vous voulez générer votre css pour votre site en production, il suffit d’exécuter cette commande:

./app/console assetic:dump --no-debug

Voici les sources qui m’ont aidé à écrire cet article:

Share

Symfony2: Génération d’un nouveau projet version 2

Bonjour,

Avec un peu de retard, suite à des problèmes de santé, je vous présente brièvement la version 2 du bootstrapper qui vous permet de générer facilement un projet Symfony2. Vous pouvez décharger cette nouvelle version à l’adresse suivante: Symfony2Project. Ce nouveau générateur est basé sur les components de Symfony2.

La première chose à faire est de copier le fichier default.xml.dist et le renommer default.xml. Ce fichier xml renseigne les éléments nécessaire pour le bon fonctionnement du générateur. Vous le trouvez dans le répertoire Resources/Profile.

Pour connaître toutes ces options, vous pouvez taper dans votre terminal:

./symfony2project generate:project

Vous devez au minimum renseigner les éléments suivants:
– Nom du bundle (AppName)
– Le namespace que vous avez choisi (VendorName)
– Et le répertoire ou vous allez générer votre projet (Path)

Ensuite, selon les éléments que vous voulez mettre en place, vous avez des options que voici:
– controller: Permet de renommer le controleur générer lors de l’initialisation du projet
– protocol: Protocole utilisé par git (git, http)
– session-start: Démarrage automatique de la session
– session-name: Nom de la session
– orm: Type d’orm (doctrine ou propel)
– odm: Type d’odm (mongodb)
– assetic: Activation du bundle Assetic
– swiftmailer: Activation du bundle swiftmailer
– doctrine-migration: Activation du bundle de migration
– doctrine-fixtures: Activation du bundle de fixtures
– template-engine: Genérateur de modèle (php ou twig). Le défaut est fixé sur twig
– profile: Nom du fichier profile (dans le cas ou vous avez réaliser un nouveau fichier xml). Le défaut est fixé sur default
– assets-symlink: création du lien symbolique sur les ressources des bundles
– force-delete: Force la suppression d’un ancien projet résidant dans le path

Profile:
Le fichier profile peut-être personnalisé selon vos désirs. Il existe une section User qui vous permet d’ajouter vos éléments. Vous pouvez également déposer ce fichier sur un serveur http et renseigner la directive –profile. Exemple:

./symfony2project generate:project AppName VendorName Path
--profile=http://myserver/profile/default

Attention: Ne pas spécifier l’extension (.xml)

Voici une ligne de commande avec toutes les options:

./symfony2project generate:project Core Funstaff /path/to/your/project
--controller=Main --session-start --session-name=funstaff --orm=doctrine
--assetic --swiftmailer --doctrine-migration --doctrine-fixtures
--assets-symlink --force-delete

J’espère que ce nouvel outil vous facilitera la vie.

Bonne utilisation 😉

Share

Symfony2: Génération d’un nouveau projet

ATTENTION: Nouvelle version du script. Un nouvel article en préparation.

Ce soir, juste une petite publication, pour vous annoncer la publication d’un script maison. Symfony2 ne proposant pas une tâche pour générer un nouveau projet, j’ai décidé de réaliser un script de mise en place des éléments. Vous le trouvez sur github en cliquant sur le lien ci-dessous

Symfony2Project

Voici la syntaxe pour son utilisation:

php symfony2project.php –app=AppName –vendor=VendorName [–path=/your/destination/path] [–controller=controllerName] [–protocol=git|http] [–session-start=false|true] [–session-name=sessionName] [–symfony-repository=fabpot|symfony] [–with-db=false|true] [–template-engine=twig|php]

–app: Le nom de votre application (en faite le Bundle principal)
–vendor: Nom du « vendor » (obligatoire)
–path: Destination (Ex: /www/virtualhosts/foo)
–controller: Si vous l’indiquez le script génèrera un controller et un template
–protocol: git ou http (selon le cas d’utilisation)
–session-start: false ou true (démarrage automatique de la session) (défaut: false)
–session-name: Nom de la session (défaut: Nom de l’application)
–symfony-repository: fabpot ou symfony (défaut: symfony)
–with-db: false ou true (défaut: true)
–template-engine: twig ou php (default: twig)

J’ai encore quelques améliorations dans ma ToDo list. Si vous essayez ce script et qu’il vous convient, merci de m’encourager par un petit commentaire 🙂 Vous pouvez également y participer en soumettant un « Pull Request« .

Maintenant, je vais me remettre à la découverte de Symfony2 qui est fondamentalement différent de la version 1.

[MAJ]
27.11.2010: Ajout de l’option auto_start sur la session (réf)
28.11.2010: Ajout de l’option symfony-repository permettant de choisir entre 2 dépôt (fabpot ou symfony)
01.12.2010: Ajout des options session-name et with-db
23.01.2011: Ajout de l’option template-engine
31.01.2011: Ajout de l’option vendor

Share

sfDoctrineGuardPlugin: Ajout de permissions et de groupes après un register

Dans cet article, je vais vous montrer une solution pour ajouter des permissions et des groupes lors de la création d’un nouvel utilisateur avec le formulaire « Register » du plugin sfDoctrineGuardPlugin.

Pour cela nous allons personnaliser le modèle sfGuardUser en y ajoutant une fonction « addDefaultPermissionsAndGroups ». Vous trouvez cette classe dans le dossier lib/model/doctrine/sfDoctrineGuardPlugin/sfGuardUser.class.php. Voici le code à insérer:

class sfGuardUser extends PluginsfGuardUser
{
  public function addDefaultPermissionsAndGroups(Array $a_permissions, Array $a_groups)
  {
    $permissions = Doctrine_Query::create()->from('sfGuardPermission')->whereIn('name', $a_permissions)->execute();
    foreach ($permissions as $permission)
    {
      $this->sfGuardUserPermission[]->permission_id = $permission->id;
    }
    
    $groups = Doctrine_Query::create()->from('sfGuardGroup')->whereIn('name', $a_groups)->execute();
    foreach ($groups as $group)
    {
      $this->sfGuardUserGroup[]->group_id = $group->id;
    }
    
    $this->save();
  }
}

Nous allons maintenant utiliser ce code dans notre action:

$this->form = new sfGuardRegisterForm();

if ($request->isMethod('post'))
{
  $this->form->bind($request->getParameter($this->form->getName()));
  if ($this->form->isValid())
  {
    $user = $this->form->save();
    $user->addDefaultPermissionsAndGroups(
      array('read', 'write'),
      array('moderator')
    );
    
    $this->getUser()->signIn($user);

    $this->redirect('@homepage');
  }
}

Comme vous pouvez le voir dans le code ci-dessus, nous passons un premier tableau contenant le nom des permissions et un second avec le nom du(des) groupe(s). Il suffit ensuite d’effectuer le login de l’utilisateur pour lui assigner les autorisations.

J’espère que ce petit exemple vous servira dans les prochains développements.

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