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

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

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

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

Symfony: Réaliser une identification ajax avec sfDoctrineGuardPlugin

Dans cette publication, nous allons voir comment mettre en place un système d’identification basé sur le plugin sfDoctrineGuard. Pour que cela fonctionne, il vous faut également l’excellente librairie jquery. Dans cette article, je n’aborde pas l’installation du plugin, ni l’installation et le chargement de jquery.

Nous allons commencer par générer un nouveau module « user » dans notre projet avec la commande ci-dessous:

./symfony generate:module frontend user

Nous allons dès maintenant modifier notre module « user » pour y mettre notre propre code. Pour cela nous allons inclure la classe « BasesfGuardAuthActions » dans notre classe et changer l’extends:

require_once(sfConfig::get('sf_plugins_dir').'/sfDoctrineGuardPlugin/modules
/sfGuardAuth/lib/BasesfGuardAuthActions.class.php');

class userActions extends BasesfGuardAuthActions
{
  ...
}

J’avais un problème a résoudre lors de l’affichage de la page signin lors de l’appel d’une page protégée. J’ai choisi d’y mettre uniquement une information utilisateur lui indiquant de s’identifier avec le formulaire. J’ai créé le fichier signinSuccess.php dans le dossier templates.

Nous allons maintenant monter le formulaire d’identification dans un component.

class userComponents extends sfComponents
{
  public function executeSignin(sfWebRequest $request)
  {
    $class = sfConfig::get('app_sf_guard_plugin_signin_form', 'sfGuardFormSignin');
    $this->form = new $class();
  }
}

template: _signin.php

<div id="form_message">&nbsp;</div>
<form id="guard" action="<?php echo url_for('@sf_guard_signin') ?>" method="post">
  <?php echo $form->renderHiddenFields(); ?>
  <label>Utilisateur:</label>
  <?php echo $form['username']->render(); ?>
  <label>Mot de passe:</label>
  <?php echo $form['password']->render(); ?>
  <?php echo $form['remember']->render(array('id' => 'remember')); ?>
  Se souvenir de moi

  <input type="submit" value="S'identifier" />
  <a href="<?php echo url_for('@sf_guard_password') ?>">Mot de passe oublié ?</a>
</form>

<script type="text/javascript">
  $('#guard').submit(function() {
    $.post("<?php echo url_for('@sf_guard_signin'); ?>", $('#guard').serialize(), function(response) {
      switch(response.status) {
        case 'success':
        $('#form_message').html(response.message);
        $(location).attr('href',response.url);
        break;
        case 'failure':
        $('#form_message').html(response.message);
        $('#form_message').show();
        break;
      }
    }, 'json');
    return false;
  });
</script>

Nous allons rajouter un peu de css pour cacher notre zone « form_message »:

#form_message {
  display: none;
  font-weight: bold;
  color: red;
}

Prochaine phase, désactiver les routes et y mettre nos propres paramètres. Nous allons pour cela toucher 3 fichiers:

app.yml

all:
  sf_guard_plugin:
    routes_register: false

routing.yml

sf_guard_signin:
  url:      /login
  param:    { module: user, action: signin }

sf_guard_signout:
  url:      /logout
  param:    { module: user, action: signout }

sf_guard_password:
  url:      /password
  param:    { module: user, action: password }

settings.yml

all:
  .actions:
    login_module:           user
    login_action:           signin
        
    secure_module:          user
    secure_action:          secure

La dernière phase de cette réalisation est l’implémentation de notre fonction signin. Voici le code utilisé pour un dialogue ajax:

class userActions extends BasesfGuardAuthActions
{
  public function executeSignin($request)
  {
    $user = $this->getUser();
    if ($user->isAuthenticated())
    {
      return $this->redirect('@homepage');
    }

    if($request->isMethod('post') && $request->isXmlHttpRequest())
    {
      $class = sfConfig::get('app_sf_guard_plugin_signin_form', 'sfGuardFormSignin');
      $form = new $class();
      $form->bind($request->getParameter($form->getName()));
      if ($form->isValid())
      {
        $values = $form->getValues();
        $user->signin($values['user'], array_key_exists('remember', $values) ? $values['remember'] : false);
        $signinUrl = sfConfig::get('app_sf_guard_plugin_success_signin_url', $user->getReferer($request->getReferer()));

        return $this->renderText(json_encode(array('status' => 'success', 'url' => $signinUrl)));
      }
      else
      {
        return $this->renderText(json_encode(array('status' => 'failure','message' => 'Identification incorrecte')));
      }
    }

    $this->getResponse()->setStatusCode(401);
  }
}

Il nous reste plus qu’à insérer notre component dans notre layout ou notre template:

<?php if (!$sf_user->isAuthenticated()): ?>
<?php include_component('user', 'signin'); ?>
<?php endif; ?>

L’implémentation est terminée. J’espère que ce petit article vous permettra de mettre en place un formulaire à la web 2.0 😉

Share