Archive for the ‘Symfony 2’ Category

Alors voilà le topo : j’ai une table qui compte mes visiteurs en fonction de leur source :

colonne type index
date DATE PRIMARY
source VARCHAR(50) PRIMARY
hits INT

A chaque visite, j’incrémente le nombre de hits en fonction de la date et de la source. L’approche naïve, c’est d’aller chercher l’objet, de le créer au besoin, de l’incrémenter, et de le sauver. Le principal problème de ce genre de fonctionnement est la gestion de la concurrence : que se passe-t-il si j’ai deux visiteurs qui arrivent au même moment sur mes serveurs ? Je vais sans doute avoir une erreur…

Avant, j’utilisait INSERT … ON DUPLICATE KEY … :

  1. INSERT INTO
  2.   my_table
  3.   (`date`, `source`, `hits`)
  4. VALUES
  5.   (CURDATE(), 'referer', 1)
  6. ON DUPLICATE KEY UPDATE
  7.   hits = hits + 1

Mais ça, c’était avant Doctrine…

Doctrine est un ORM…

… compatible avec beaucoup de moteurs de bases de données, du coup pas possible d’utiliser certaines spécificités du langage à moins de

  • utiliser des requêtes natives ;
  • étendre Doctrine pour faire ce que vous voulez que ça fasse, par exemple rajouter des types.

Bref, c’est du boulot ! Alors si on peut trouver une petite solution de contournement…

Notre problème est double :

  1. Pas de primary sur des champs DATE avec Doctrine : peut être contourné avec un type particulier ;
  2. Pas de INSERT ON DUPLICATE KEY UPDATE : peut être contourné en utilisant une requête native.

Mais bon, si on utilise un ORM, c’est parce qu’on préfère gérer des entités et du DQL 🙂

Utiliser Doctrine, son Entity Manager et le DQL

L’approche que je propose n’est pas des plus optimisées par rapport à la requête d’origine, mais a le mérite d’être compatible DQL.

1ère étape : Modification de l’entité

On préfère travailler avec des entités avec des IDs, on sait jamais ce qu’on va en faire, alors mieux vaut prévoir le coup ! Et même si Doctrine ne gère pas les dates en PRIMARY, ça gère les dates en Unique 🙂

colonne type index
id INT PRIMARY
date DATE UNIQUE(date_source)
source VARCHAR(50) UNIQUE(date_source)
hits INT

Notez que la contrainte unique est sur les deux colonnes date et source.

2ème étape : Traduction de l’INSERT .. UPDATE

Elle se décompose en trois étapes :

  1. on incrémente la ligne qui correspond à la date et la source ;
  2. si aucun enregistrement n’a été mis à jour, on crée une nouvelle ligne dans la base ;
  3. si au moment de l’écriture, ça génère une erreur de duplicate key (sur la UniqueConstraint), alors on relance l’incrément qui cette fois-ci devrait fonctionner 🙂

On se retrouve avec 3 cas :

  1. La ligne éxiste déjà (majorité des cas dans mon contexte) : alors on fait une seule requête, comme avant ;
  2. La ligne n’éxiste pas : on la crée et le INSERT se passe correctement : on a fait 2 requêtes, mais qu’une seule fois par jour et par source ;
  3. La ligne n’éxistait pas, mais a été créée entre temps par un autre visiteurs : on incrémente cette ligne. On a fait 3 requêtes, mais comme avant ça n’arrive qu’une seule fois par jour et par source.

Exemple de code :

  1. <?php
  2.  
  3. namespace Tests\Model;
  4.  
  5. class HitsManager
  6. {
  7.     /** @var EntityManager */
  8.     protected $em;
  9.  
  10.     /**
  11.      * Executes the update statement
  12.      * @param  \DateTime $date   The date
  13.      * @param  string    $source The source value
  14.      * @return integer           1 if successful (thanks to unique key), 0 otherwise
  15.      */
  16.     protected function doInc($date, $source)
  17.     {
  18.         return $em
  19.             ->createQuery('
  20.                UPDATE \Tests\Entity\Hits h
  21.                SET h.hits = h.hits + 1
  22.                WHERE h.date = :date AND h.source = :source
  23.            ')
  24.             ->setParameter('date', $date)
  25.             ->setparameter('source', $source)
  26.             ->execute();
  27.     }
  28.  
  29.     /**
  30.      * Executes the insert ... on duplicate key update
  31.      * @param  \DateTime $date   The date
  32.      * @param  string    $source The source value
  33.      * @return boolean           True if successful, false otherwise
  34.      */
  35.     public function inc(\DateTime $date, $source)
  36.     {
  37.         // Try to increment
  38.         if (0 < $this->doInc($date, $source)) {
  39.             // We were successful
  40.             return true;
  41.         }
  42.  
  43.         // Failed to increment, try to create new one
  44.         $hit = new \Tests\Entity\Hits();
  45.         $hit->setDate($date);
  46.         $hit->setSource($source);
  47.         $hit->setHits(1);
  48.         try {
  49.             $this->em->persist($hit);
  50.             // Flush to generate \Exception if failure !
  51.             $this->em->flush();
  52.  
  53.             return true;
  54.         } catch (\Doctrine\DBAL\DBALException $e) {
  55.             // _One reason_ might be we have duplicate key
  56.             return (boolean) $this->doInc($date, $source);
  57.         }
  58.     }
  59. }
  60.  

Avec les nouveaux outils développeurs des navigateurs, il est désormais possible de faire du debug sur des fichiers JS et CSS compilés à partir de Coffee Script, LESS ou SCSS, en particulier sur Firefox et Chrome.

Les sources maps

Les sources maps sont des informations sur les fichiers générés, qui permettent de lier une instruction compilée avec l’endroit ou elle a été définie. Le format est bien complexe, mais les outils que nous utilisons nous permettent aisément de générés ces sources maps automatiquement, en rajoutant des options à la compilation.

Les sources maps et assetic

Le filtre assetic LESS peut prendre des options de compilation, qui sont distinguées en deux groupes : les options qui permettent de transformer le code source LESS en arbre (parser : représentation informatique du code source), puis de transformer cet arbre en fichier CSS (dumper). Ce qui nous intéresse ici, c’est de passer les bonnes options au moment du dump pour dire à LESS de dumper également les sources-maps.

Un coup d’oeil au fichier lessc nous permet de trouver les options à passer au dumper en fonction des options que prend lessc.

  1. $filter = new Filter\Less();
  2. $filter->addTreeOption('outputSourceFiles', true);
  3. $filter->addTreeOption('sourceMap'', true);

Avec ces options, le compilateur less va inclure les fichiers sources dans les fichiers compilés.

Intégration avec Symfony 2

Il suffit juste d’ajouter ces options au service « Filtre LESS » de symfony 2 de l’assetic-bundle.

Modification du Bundle :

  1. // \Acme\Demo\AcmeDemoBundle
  2. public function build(ContainerInterface $container)
  3. {
  4.     parent::build($container);
  5.     $container->addCompilerPass(new Compiler\LessSourceMapPass());
  6. }
  7.  

Ajout d’une nouvelle passe de compilation :

  1. // \Acme\Demo\DependencyInjection\Compiler\LessSourceMapPass
  2.  
  3. class LessSourceMapPass implements CompilerPassInterface
  4. {
  5.     public function process(ContainerBuilder $container)
  6.     {
  7.         // Activation uniquement en mode debug
  8.         if ($container->getParameter('kernel.debug')) {
  9.             $lessAsseticFilter = $container->getDefinition('assetic.filter.less');
  10.             // Ajout des options au dumper
  11.             $lessAsseticFilter->addMethodCall('addTreeOption', array('outputSourceFiles', true));
  12.             $lessAsseticFilter->addMethodCall('addTreeOption', array('sourceMap', true));
  13.         }
  14.     }
  15. }
  16.  

iGraal a développé un bundle pour répondre à la question suivante : comment pouvoir utiliser les extensions et filtres twig dans un fichier javascript, qui sera lui-même passé dans la moulinette d’assetic ?

Expression du besoin

iGraal a une application mobile compatible Android et iOS. iGraal souhaite utiliser le cache varnish pour son site, mais certains traitements traînent dans les request listeners, dont le test de compatibilité mobile. Hors, il faudrait éviter de faire un appel à toute la couche Symfony juste pour détecter qu’un utilisateur est mobile ou non. Une solution qui fonctionne très bien est de passer ce test en javascript.

Le rôle de ce javascript est donc 1) la détection du device utilisé par la personne (iOS / Android), 2) la décision de l’affichage (via cookies), 3) la redirection vers une URL donnée et 4) la possibilité d’inclure ce JS dans notre JS global déjà compilé par assetic.

Le filtre assetic Twig

Nous voulions donc utiliser :

  1. Assetic pour inclure ce JS dans notre JS global
  2. Utiliser la fonction twig path pour trouver vers quelle URL rediriger notre internaute

Un bundle a donc été créé : https://github.com/igraal/twig-assetic-filter-bundle

Au début, c’était pas vraiment un bundle, juste un bout de code, mais autant partager !

Ce bundle permet d’utiliser un filtre « twig » pour inclure des JS ou des CSS.

Exemple d’utilisation

Fichier HTML

  1. <DOCTYPE html>
  2. <html>
  3.   <head>
  4.     <% javascripts
  5.       '@AcmeWhateverBundle/Resources/js/main.js'
  6.       '@AcmeWhateverBundle/Resources/js/feature.twig.js'
  7.       filter="twig" %>
  8.     <script type="text/javascript" src="{{ asset_url }}" async defer />
  9.     <% endjavascripts %>
  10.   </head>
  11. ...
  12. </html>

Fichier JS

  1. if (isMobile()) window.location.replace('{{ path('www_mobile') }}');

 

Principales utilisations

Utilisation du filtre :

  1. Utiliser les routing dans du javascript (comme le montre l’exemple)
  2. Utiliser des traductions dans du javascript, surtout utile lors de validation de form en JS, dans tous les cas il y a l’ajax 😉
  3. Utiliser vos propres extensions pour du JS, par exemple pour faciliter le codage ou autre. On pourrait très bien imaginer une fonction js_debug qui fait un console.debug en javascript en dev, rien en prod.