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

Apache et symfony: optimisation

Je vais vous présentez dans ce billet, les méthodes que j’utilise pour optimiser les réponses d’apache et par la même occasion le rewrite de symfony.

Pour pouvoir effectuer tous les réglages, voici les modules que je charge dans la configuration d’apache

LoadModule deflate_module modules/mod_deflate.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule rewrite_module modules/mod_rewrite.so

Je vais commencer par épurer le log apache. Pour cela, je vais ajouter à la fin de mon fichier de configuration httpd.conf ou apache2.conf (selon les install), les lignes suivantes:

SetEnvIf Request_URI "\.(css|gif|ico|jpg|js|png|txt|xml)$" dontlog
SetEnvIf Request_Method HEAD dontlog

Je vais tout simplement ignorer les fichiers qui contiennent les extensions listées ci-dessus. Ensuite, il suffit d’attribuer cette variable d’environnement à notre paramètre « CustomLog » de notre virtualhost comme ceci:

CustomLog logs/mywebsite.com-access_log combined env=!dontlog

Dès maintenant, Apache prendra moins de temps à écrire ces logs car il ne tiendra plus compte de ces fichiers.

Passons maintenant à la compression des fichiers entre le serveur et le client. Pour cela, j’ai utilisé le code suivant dans la configuration du virtualhost:

<Location />
# ------------- DEFLATE -------------
# http://httpd.apache.org/docs/2.0/mod/mod_deflate.html
<IfModule mod_deflate.c>
  SetOutputFilter DEFLATE
  BrowserMatch ^Mozilla/4 gzip-only-text/html
  BrowserMatch ^Mozilla/4\.0[678] no-gzip
  BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
  SetEnvIfNoCase Request_URI \
  \.(?:gif|jpe?g|png)$ no-gzip dont-vary
  Header append Vary User-Agent env=!dont-vary
</IfModule>
# ------------- END DEFLATE -------------
</Location>

Cela vous permettra d’économiser un peu de bande passante 😉

Nous allons maintenant nous occuper du cache Apache:

# ------------- EXPIRES RULE -------------
# http://httpd.apache.org/docs/2.0/mod/mod_expires.html
<IfModule mod_expires.c>
  ExpiresActive On

  ExpiresByType text/css "access plus 7 days"
  ExpiresByType application/javascript "access plus 7 days"
  ExpiresByType image/gif "access plus 7 days"
  ExpiresByType image/jpeg "access plus 7 days"
  ExpiresByType image/png "access plus 7 days"
</IfModule>
# ------------- END EXPIRES RULE -------------

Comme vous pouvez le constater ci-dessous, vous avez la possibilité avec la directive « ExpiresByType » de mettre différentes validités selon le type de fichier. Attention quand même lorsque vous utilisez ce genre de cache car il se peut que le serveur ne rende pas toujours le résultat actualisé. Il faut effectuer des tests avant son passage en production.

Passons maintenant à la partie symfony. Par défaut, la configuration du mod_rewrite est située dans le fichier .htaccess à la racine de votre site (dossier web). Apache va lire ce fichier à chaque requête, ce qui prend du temps. Nous allons donc tranférer cela dans notre configuration virtualhost pour l’avoir en mémoire et désactiver la lecture physique:

<Location />
# ------------- REWRITE -------------
<IfModule mod_rewrite.c>
  RewriteEngine On
  
  # Level 0 to 9 [0 = no logging, 9 = all actions] (Default: 0)
  RewriteLogLevel 0
  
  # we check if the .html version is here (caching)
  RewriteRule ^$ index.html [QSA]
  RewriteRule ^([^.]+)$ $1.html [QSA]

  # no, so we redirect to our front web controller
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
# ------------- END REWRITE -------------
</Location>

Dans la directive « <Directory …> », nous allons passer le paramètre « AllowOverride » à none. Vous pouvez ensuite supprimer le fichier .htaccess de votre dossier web.

Pour finir, je vous donne la représentation complète de mon fichier virtualhost:

<VirtualHost *:80>
  ServerName mywebsite.com
  DocumentRoot /www/virtualhosts/mywebsite.com/web
  ErrorLog logs/mywebsite.com-error_log
  CustomLog logs/mywebsite.com-access_log combined env=!dontlog
  RewriteLog logs/mywebsite.com-rewrite.log
  
  Alias /sf /www/virtualhosts/mywebsite.com/lib/vendor/symfony/data/web/sf
  
  <Directory "/www/virtualhosts/mywebsite.com/web">
    Options Indexes FollowSymLinks SymLinksifOwnerMatch
    AllowOverride none
    Allow from All
  </Directory>
  
  <Directory "/www/virtualhosts/mywebsite.com/lib/vendor/symfony/data/web/sf">
    Options Indexes FollowSymLinks SymLinksifOwnerMatch
    AllowOverride none
    Allow from All
  </Directory>

  <Directory "/path/to/my/sfProject/web/uploads">
    php_flag engine off
  </Directory>

  <Location />
    # ------------- REWRITE -------------
    <IfModule mod_rewrite.c>
      RewriteEngine On
      
      # Level 0 to 9 [0 = no logging, 9 = all actions] (Default: 0)
      RewriteLogLevel 0
      
      # we check if the .html version is here (caching)
      RewriteRule ^$ index.html [QSA]
      RewriteRule ^([^.]+)$ $1.html [QSA]

      # no, so we redirect to our front web controller
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteRule ^(.*)$ index.php [QSA,L]
    </IfModule>
    # ------------- END REWRITE -------------
    
    # ------------- DEFLATE -------------
    # http://httpd.apache.org/docs/2.0/mod/mod_deflate.html
    <IfModule mod_deflate.c>
      SetOutputFilter DEFLATE
      BrowserMatch ^Mozilla/4 gzip-only-text/html
      BrowserMatch ^Mozilla/4\.0[678] no-gzip
      BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
      SetEnvIfNoCase Request_URI \
      \.(?:gif|jpe?g|png)$ no-gzip dont-vary
      Header append Vary User-Agent env=!dont-vary
    </IfModule>
    # ------------- END DEFLATE -------------
  </Location>

  # ------------- EXPIRES RULE -------------
  # http://httpd.apache.org/docs/2.0/mod/mod_expires.html
  <IfModule mod_expires.c>
    ExpiresActive On

    ExpiresByType text/css "access plus 7 days"
    ExpiresByType application/javascript "access plus 7 days"
    ExpiresByType image/gif "access plus 7 days"
    ExpiresByType image/jpeg "access plus 7 days"
    ExpiresByType image/png "access plus 7 days"
  </IfModule>
  # ------------- END REWRITE RULE -------------
  
</VirtualHost>

J’espère que ces petites optimisations pourront vous servir un jour. N’hésitez pas à me faire des remarques et éventuellement me communiquer d’autres astuces Apache/symfony.

Share