Samuel

Bienvenue sur ma bio. Je suis gérant d'une agence de communication et j'ai pour projet de mettre en ligne un logiciel de poker et une horloge de Poker sur le marché en 2011.

Posts

November 25, 06:27 AM

Voici un nouveau service en ligne c’est my poker bank roll qui permets de créer votre bank roll et suivre vos performances lorsque vous jouez chez des amis, en casino aussi bien pour des sit n go qu’en cash game.

Le site vous propose une analyse poussée qui permettra de déterminer vos points forts et vos points faibles tranquillement chez vous.

Voici un aperçu de l’évolution d’une bank roll

My poker bank roll est disponible est un site internet, mais vous pouvez le consulter aussi sur iPad ou sur votre smartphone.

Si vous ne l’utilisez pas, c’est pas ici : www.mypokerbankroll.com

 

July 07, 11:06 AM

Bwin vous propose de relever un defi sur plusieurs semaines.

Comment ça marche :

Dès que vous cumulez  10 minutes en argent réel à des tables Cash Games du 07 Juillet  au 31 juillet, vous recevrez un ticketde tirage au sort.

Pendant la première semaine d’Aout, un gagnant sera tiré au sort et remportera la coquette somme de 5 000 € en cash!

Bien entendu, ce n’est pas le seul lot. D’autres joueurs remporteront des packages pour des tournois. Ils seront bien entendu tirés au sort aussi.

Si vous voulez participer, c’est pas ici

Je participe au défi BWIN

June 29, 06:48 AM

Suite à sa performance aux WSOP, Everest poker vous propose de gagner une rencontre avec Fabrice Soulier. La rencontre aura lieux lors des World Series Of Poker d’Europe pendant un déjeuner.

Mais pour cela, il faut remporter le tournoi qui aura lieu demain, jeudi 30 juin à 21H00.

Pour participer, il faut jouer sur Everest et être fan de la page Everest sur Facebook.

Inscrivez vous maintenant

June 29, 05:53 AM

Ce week end, Winamax offrira au vainqueur du Sunday Surprise une Rolex Sudmariner en plus des 9 000 € de gain.Tout se passe dimanche à 20h30.

Pour info :

C’est 50 000 € de prix garanti pour une inscription de 10 € de buy in.

Inscrivez vous au tournoi

June 17, 10:55 AM

My poker agenda vient de sortir la nouvelle version de son application Android.

C’est à voir ici sur l’Android Market

La mise à jour :
- Vous pouvez maintenant consulter les photos d’un casino
- Changement de design
- Utilisation de l’application en mode portrait et paysage
- Suppression des permissions inutiles

My Poker Agenda vous permet d’obtenir des informations sur des tournois de poker et sur des casinos. Vous pouvez rechercher des tournois ou casinos soit par leur nom, soit en utilisant la géolocalisation pour trouver directement les évènements proches de vous.

Toutes les informations sur votre tournois de Poker :
Chaque tournoi a une fiche détaillée sur laquelle vous trouverez toutes les informations relatives à celui-ci et comment vous y rendre. En un clic, vous pourrez l’ajouter à votre agenda de téléphone, le publier sur Facebook, Twitter.

Participez à la création des fiches casinos :
Vous accèderez à la fiche détaillée de chaque casino contenant l’adresse, le site web. Si vous vous trouvez dans un casino vous pouvez envoyer des photos prises depuis votre téléphone.

Actuellement les tournois référencés sont essentiellement français, mais en fonction du nombre de téléchargements nous augmenterons notre base de données.
Merci de laisser des commentaires pour nous aider à améliorer l’application et à corriger d’éventuels bugs.

N’hésitez pas à consulter le site web rattaché à l’application www.mypokeragenda.com

May 03, 08:11 AM

 

Comme l’Italie et la France, la Belgique aussi est un pays où le poker a désormais sa place, un jeu dont la version en ligne est aussi reconnue et autorisée. En effet, la loi qui régit le poker en ligne en Belgique a été votée le 10 janvier 2010 après l’approbation du projet de loi en conseil des ministres en mars 2009. Cependant, cette législation n’est entrée en vigueur que depuis le 1er janvier 2011. 

Cependant, quelques différences près caractérisent le poker en ligne en Belgique. Le gouvernement belge est resté catégorique sur le fait que les opérateurs de poker en ligne sur place doivent aussi y être présents en réel, ce qui, à priori, exclut d’office les autres opérateurs de poker en ligne étrangers. De ce fait, ces derniers ne peuvent pas proposer de tournois de poker virtuels aux joueurs belges. Par contre, ces opérateurs de poker online en dehors du territoire ont la possibilité de se joindre aux opérateurs réels présents en Belgique pour avoir le droit d’offrir leurs services aux joueurs belges. Bref, l’autorité du pays a opté pour un système de licences à octroyer seulement aux propriétaires de casino au niveau local. 

Outre cette importante mise au point sur les rooms et les joueurs virtuels, d’autres points distinguent aussi la législation de poker en ligne en Belgique. Ainsi, la taxe brute sur le poker y est de 11%. D’autre part, le gouvernement belge prône les initiatives qui luttent contre le blanchiment d’argent et la dépendance au jeu. Pour Etienne Marique, président de la Commission des jeux de hasard qui roule pour une ouverture maîtrisée, il met plutôt l’accent sur la sincérité et l’égalité des chances des joueurs. Par conséquent, il suggère de prendre en considération, la perte horaire, l’âge du joueur sans oublier les modalités de jeu. 

 

March 29, 06:04 AM

 

Même si le PSG a encaissé des buts ou L’Olympique Lyonnais éliminé de la Ligue des Champions, ses supporters peuvent toujours prendre leurs revanches. Et c’est sur Winamax bien sûr ! Avec son tout nouveau concept de tournoi de poker Match Retour, tout le monde est certain de pouvoir rattraper les défaites de son club du week-end. Il s’agit d’une opportunité de taille pour soutenir son équipe car Match Retour est une série de 10 tournois programmée pour chaque lundi vers 21 h. Tout supporter de chaque équipe peut peut ainsi s’y rattraper pour un buy-in de 2 euros. Lors de l’inscription, chaque participant n’a qu’à choisir un avatar mettant en avant la couleur de son équipe mais supporter plusieurs équipes à la fois est aussi possible.

Match Retour vous propose donc de comptabiliser des points pour votre équipe préférée. Le gagnant de chaque tournoi remportera la grosse part du prizepool et un point pour son équipe tandis que les 9 joueurs remplissant la liste du top 10 gagneront chacun un point. A la fin des journées de compétitions, les membres de Winamax établiront un classement général des 20 équipes avec la même méthode de classement qu’en football. Ainsi, l’équipe gagnante recevra 3 points, en cas de match nul ce sera 1 point et 0 point correspondra à une défaite. Et à la fin du championnat, les 5 premières équipes ayant comptabilisé le plus de point seront dotées d’une récompense.

 

 

March 02, 10:02 AM

Vous cherchez des tournois de poker online et live ?

My Poker Agenda, l’agenda des tournois de poker est un site avec lequel vous pourrez trouver les parties de poker qui vous plaisent.
En effet, vous pouvez rechercher :

Avec My Poker Agenda, vous pouvez aussi vous inscrire aux différents tournois proposés en un clic.

Grâce au moteur de recherche, vous pourrez choisir le buy in, le lieu, la date et toutes les informations qui vous permettront de participer au tournoi de poker qui vous intéresse.

My Poker Agenda propose également des fiches détaillées sur chacun des évènements ainsi que sur les nombreux casinos, les rooms, et les séries etc.

My Poker Agenda vous permet de trouver le tournoi de poker qui vous correspond alors n’hésitez pas et rendez vous sur : My Poker Agenda

February 02, 08:53 AM

Cette année passez une Saint-Valentin unique grâce à Chilipoker! Le 14 février, défiez la joueuse pro Liz Lieu en tête à tête et remportez 2 000€ !

Pour vous qualifier, Chilipoker vous propose des tournois quotidiens du 1 au 11 février au cours desquels vous pourrez vous qualifier pour la Super Finale. Le vainqueur de la Super Finale affrontera Liz Lieu le soir de la Saint-Valentin pour remporter 2 000€.

Agenda des tournois qualificatifs:

Inscrivez-vous vite, vous serez peut-être l’heureux élu qui affrontera Liz Lieu en tête en tête pour la Saint-Valentin…

Je m’inscris

January 28, 11:44 AM

Winamax, sponsor du Paris Saint Germain, vous propose un tournoi très original avec une dotation qui va faire rêver tous les amateurs de foot.

Le samedi 29 janvier à 14h30, participez au tournoi “Spécial Raï” pour le 40ème anniversaire du PSG et donnez le coup d’envoi du match PSG-Lens le 12 février au Parc des Princes aux côtés du Capitaine Raï !

En plus du prize pool, les 4 premiers assisteront, avec une personne de leur choix, au match PSG-Lens du 12 février, dans le “carré VIP” en compagnie du légendaire capitaine du PSG et de l’équipe du Brésil.

Inscription : 5€.

Dotation (en plus du prizepool):

1er : 2 places au carré VIP pour voir le match en compagnie de Raï + coup d’envoi du match + dîner d’après-match  avec Raï dans les salons du Parc.

2ème au 4ème : 2 places au carré VIP pour le match + dîner d’après-match avec Raï dans les salons du Parc.

S’inscrire au tournoi

Posts

November 26, 09:26 AM

Voici un nouvel outil destiné aux joueurs de poker qui veulent souhaitez garder une trace de vos performances live et les analyser.

C'est MyPokerBankroll ! 

L'utilisation est très simple : 

  1. Créez un compte sur mypokerbankroll.com
  2. Ajouter des sessions de Poker
  3. Analyser l'évolution de votre bankroll.

MyPokerBankroll vous propose une analyse poussée avec de nombreux graphique pour connaître vos points forts ou vos points faibles.  L'outil est accessible sur iPhone, Android, iPad et bien sur le Web. L'interet c'est que dès que vous avez fini une partie de Poker live, vous ajouter votre performance.

www.mypokerbankroll.com

May 02, 05:04 AM

SudWeb le petit frère de Paris Web aura lieu le 27 Mai prochain à Nimes. Cet évènement d'une journée de conférences et de networking sur lequel vous pourrez retrouver Thomas et Jérémy de Lexik qui est depuis peu partenaire de l'évènement.

Retrouvez : 

Rendez vous le 27 Mai à l’Ecole des Mines d’Alès.

March 31, 12:11 AM

Avec www.mypokeragenda.com vous pouvez retrouver l'ensemble des tournois de Poker Online et Offline.

Le site réference :

  • les tournois online des rooms de poker ;
  • les tournois live des casinos et cercles de poker en France ;
  • les tournois live des tournois internationaux ;

Si vous êtes gérant d'un casino, d'un cercle ou d'un club de poker vous pouvez rajouter votre calendrier des tournois de Poker en vous inscrivant en quelques clics.

Pour cela, vous pouvez au choix :

  • soumettre vos flux XML/RSS
  • envoyer un fichier excel avec l'ensemble de vos tournois
  • creer votre compte gratuit sur MyPokerAgenda et ajouter vous-meme vos informations.

Version Mobile :

Retrouvez sur votre téléphone Android MyPokerAgenda.

Cliquez ici pour accéder à l'application.

December 10, 05:19 AM

Voici un sondage que nous venons de mettre en place su l'utilisation de MyPokerClock.com l'outil d'organisation de Poker Maison.

Pour répondre au sondage, c'est ici

November 25, 05:11 AM

Hier, je vous ai parlé de la mise en place d'une horloge de Poker sur votre site.

Et bien Vincent, qui s'occupe du site mypokerclock.com à réalisé un tutoriel pour vous permettre de facilement récupérer les informations.

C'est à voir ici sur Dailymotion

Le tutoriel en vidéo

November 23, 09:49 AM

MyPokerClock vous propose d'intégrer sa clock sur votre site !

Rendez-vous sur www.mypokerclock.com/services

  • 100% gratuit pour vous et pour vos visiteurs
  • une source de trafic supplémentaire
  • un outil de fidélisation
  • un service original

Les avantages :

  • Facile à insérer sur votre site
  • possibilité de personnaliser le visuel pour l'intégrer au mieux à votre site

Mise en place :

Remplissez le formulaire pour :

  • Choisir la couleur d'arrière-plan
  • Choisir la couleur du texte
  • Choisir la langue
  • Tapez le nom de votre site

Vous n'avez plus qu'a récupérer le code HTML.

C'est très facile, votre clock de poker ICI

November 09, 08:35 AM

Vous connaissez probablement le projet MyPokerTour qui est l'outil gratuit ultime pour organiser des parties de Poker entre amis. Et bien la version Light avec l'application FaceBook est maintenant disponible!

Le projet est MyPokerClock. La création d'une partie est très simple en 2 étapes, avec la possibilité de se connecter avec votre compte Facebook pour ajouter des amis.

Tout ceci est à découvrir ici :

 

October 29, 04:02 AM

Lexik vient de mettre en ligne un site Web : www.andycot.com pour les collectionneurs de Timbres, Cartes postales. C'est un site de vente aux enchères qui vous permet d'acheter ou de vendre des biens. Jusque la, rien de transcendant.

Mais comment faire quand vous récupérez la vielle collection de votre grand père dont vous ne connaissez pas du tout la valeur.

Et bien, Andyc0t est allé plus loin avec la reconnaissance d'image couplée à la vente aux enchères

En effect, Andyc0t révolutionne la vente d'objet de collection grâce aux technologies de reconnaissance d'image. Le site possède une base de données très complète qui fera la recherche d'origine et identifiera les objets pour vous.

Ainsi vous vendrez votre collection au juste prix et les fiches d'information sur les objets seront toujours au top.

Merci à tous ce qui ont travaillé sur le projet :

Bien entendu, tout ceci est a découvrir sur : http://www.andycot.com/

October 20, 12:30 AM

Nos offres pour booster vos activités ont été réactualisées ces dernières semaines avec le lancement de la version 2 du Pack Business qui possède maintenant sa propre boutique en ligne avec de nombreux produits.

Ce nouveau site traite des problématiques que vous pouvez rencontrer avec vos auto-répondeurs, votre boutique Prestashop et bien d'autres outils.

C'est aussi des tutoriaux et du conseil pour la gestion de votre entreprise sur Internet.
Retrouvez par exemple les explicatifs pour installer le code de convertion google sur votre boutique prestashop, ou les informations sur la mise à jour d'OpenX.

Découvrez dès maintenant le site et la boutique

September 13, 05:54 AM

Voici les nouvelles offres d'infogérance professionnelle pour vos serveurs dédiés. L'ensemble des serveurs sont déployés avec notre logiciel d'administration serveur : FTpannel.

N'hésitez pas à contacter le support pour toute question en avant vente. Si les offres packagées ne semblent pas vous correspondre ou que vous avez un besoin spécifique pour votre site Web ou parc de serveur, vous pouvez faire une demande de devis.

Voici un exemple de l'outil de gestion de serveur :

Pour plus d'information, consultez directement la boutique en line

Posts

June 24, 07:44 AM

Si vous souhaitez vendre le Pack Business, vous pouvez déposer votre candidature dans notre partie affilié.

Le systeme est géré par Post Affiliate Pro 4, qui est inclus dans le pack business. Si vous avez des listes qui peuvent être interessé par le produit et que vous souhaitez gagner de l’argent en vendant des Pack Business, remplissez le formulaire de candidature pour être affilié au Pack Business.

Cliquez ici pour plus d’information

June 22, 07:40 AM

Bénéficiez de l’offre de lancement jusqu’au 15 Juillet 2009 pour votre pack Business complet!

Posts

February 02, 09:00 AM

Description de la société :

Lexik c’est qui ?
Une société basée à Montpellier depuis 5 ans, avec 10 personnes et une ambiance sympa.

On fait quoi chez Lexik ?
Nous sommes spécialisés dans le développement d’applications web en Symfony2. Nos projets reposent sur les méthodes agiles et un développement de qualité via la mise en place de tests unitaires et fonctionnels.

Quels types de clients ?
Des start-ups qui recherchent un appui technique sur la mise en place de leur projet, des PME et collectivités locales ayant un besoin d’accompagnement en conseil et réalisation.

Lexik, c’est quoi d’autre ?
Des scrum meetings quotidiens, des points techniques presque tous les mardi, des idées communes pour des projets internes, du développement en équipe et comme dirait Laurent, le vendredi c’est sushis !

Et si on résumait tout ça techniquement :

  • spécialistes du framework Symfony2 ;
  • contributions à des bundles Open Source sur Github ;
  • un blog technique ;
  • développement frontend avec jQuery, Twitter Bootstrap, websockets, etc. ;
  • méthodes agiles avec Scrum ;
  • intégration continue avec Jenkins et tests fonctionnels avec Behat.

Description du poste :

Vous êtes motivé et passionné par votre métier, vous suivez avec attention l’évolution des frameworks et des derniers langages à la mode. Vous avez déjà travaillé en équipe et vous aimez ça.

Vous bénéficiez d’une bonne expérience à titre privé ou professionnel dans les technologies suivantes :

  • PHP 5.3, OOP ;
  • HTML5 / CSS3 ;
  • SQL (MySQL, SQLite, PostgreSQL, …).

Une expérience ou un intérêt dans les domaines suivants seront un atout :

  • Framework symfony 1.4 / Symfony2 avec l’ORM Doctrine ou Propel2 ;
  • Gestionnaire de version (Git / SVN) ;
  • Javascript, jQuery, node.js ;
  • Tests unitaires et fonctionnels, intégration continue, Behat ;
  • Méthodologie de développement agile (Scrum).

Détails :

Envoyez-nous votre CV et lettre de motivation par email à l’adresse : recrutement@lexik.fr

  • Type de contrat : CDI
  • Lieu : Pérols
  • Début du contrat : ASAP
  • Télétravail : non
  • Rémunération : selon profil
December 09, 04:40 AM

Ayant à réaliser une boutique plutôt complexe, mais n’étant pas spécialistes du développement Magento, nous avons voulu tester l’association de Symfony2 en front end et Magento en back end.

L’intégration de ces deux systèmes est intéressante à plus d’un titre. Vous souhaitez afficher une liste de produits stockés dans une boutique Magento ? Récupérer des commandes pour les traiter dans un ERP réalisé avec Symfony2 ? Ou pourquoi pas développer une boutique entière en vous concentrant uniquement sur la logique ?

Nous allons voir ici comment utiliser l’API de Magento depuis Symfony2 : à distance en utilisant le web service, ou en local en faisant directement appel au noyau Magento.

Le web service SOAP / XML-RPC

A distance, le seul moyen d’échanger des données entre la boutique et un autre site est de passer par le web service. Son activation et ses possibilités sont très bien décrites dans la documentation officielle et sur Wikigento.

L’utilisation de base dans un controller ressemblerait au morceau de code suivant. Évidemment, dans l’idéal il faudrait créer un service pour ne pas avoir à se répéter d’une action à l’autre, et mettre en place un système de cache.


Le noyau Magento « Mage »

Si Magento et Symfony2 sont sur le même serveur, le plus efficace reste encore l’utilisation directe du noyau. L’équipe Liip a réalisé un bundle permettant d’instancier le coeur de Magento et donc d’utiliser ses méthodes internes. Le LiipMagentoBundle permet (à l’heure actuelle) :

  • la connexion des utilisateurs Magento dans Symfony2
  • le partage des sessions utilisateurs
  • l’accès au contenu et même aux blocks Magento

Une fois le bundle en place, l’application Symfony2 aura accès à toutes les données et méthodes du noyau Magento : création/édition/suppression de clients, produits et commandes, gestion du panier et des factures par Magento, l’accès au différents modes de livraison et de paiement…

Exemple 1 : Affichage d’un block Magento

Voir la documentation du LiipMagentoBundle.

Exemple 2 : Récupération de données Magento


Conclusion

Au final, l’intégration des deux systèmes est relativement aisée. Si le web service est indispensable pour faire communiquer des sites distants, l’intérêt de son utilisation en local est plus limité : relativement lent, il ne donne pas accès à toutes les fonctions que l’on peut attendre (pour le moment).

L’utilisation du noyau Magento à travers le LiipMagentoBundle pallie à ces limitations : vitesse d’exécution, partage de sessions, accès à toutes les méthodes existantes… Il ne reste plus qu’à s’occuper de la logique et de la présentation.

L’association Magento/Symfony2 semble très intéressante pour la mise en place d’une boutique rapide, si vous avez des retours d’expérience n’hésitez pas à nous en faire part. Nous ne manquerons pas d’en discuter si nous réalisons un site de cette façon.

November 09, 10:48 AM

Présentation

Voici le LexikMaintenanceBundle qui a pour but d’activer et désactiver la mise en maintenance de votre site. Quand un site à besoin pour X raisons d’être mis en maintenance, par exemple lors d’une mise à jour, vous allez pouvoir faire apparaître une page d’erreur où seront redirigés les visiteurs avec la possibilité d’en autoriser certains.

Pourquoi utiliser un bundle pour gérer la maintenance ?

Il existe de nombreuses manières de mettre un site Internet en maintenance, comme par exemple en éditant le .htaccess ou le virtual host. Symfony 1 proposait une mise en maintenance en ligne de commande et cette fonctionnalité n’a pas été reprise dans sa version 2, l’idée de ce bundle est de proposer une fonctionnalité similaire tout en laissant plus de souplesse, via l’utilisation de drivers et d’un service.

Nous avons choisi de créer ce bundle car nous voulions laisser aux utilisateurs plusieurs alternatives de mise en maintenance, ainsi que son activation depuis la console ou depuis le backend par exemple.

Fonctionnement

Un listener

Le bundle utilise un listener qui écoute l’événement onKernelRequest avec une priorité evelée pour être ainsi le premier à être appelé. Le listener va tout d’abord vérifier s’il y a des adresses IP autorisées à consulter le site en maintenance. Si oui, l’utilisateur peut continuer. Dans le cas contraire on va demander au driver utilisé dans la configuration de vérifier si la maintenance a été activé et de décider si on lève une exception qui retournera un status 503. Sinon on laisse passer l’utilisateur.

Des drivers

Un driver est une méthode choisie pour activer/désactiver la maintenance. Le bundle propose trois drivers différents.

  • Le premier est le FileDriver dont l’utilisation consiste à créer un fichier quelque part et à vérifier si celui-ci existe.
  • Le second est le MemCacheDriver qui utilise MemCache et qui va regarder si une donnée a été stockée dans le serveur de cache.
  • Le troisième est le DatabaseDriver qui va créer une table dans une base de données et vérifier les informations présentes à l’intérieur.

Il y a d’autres implémentation de drivers possibles, libre à chacun de créer le sien.

Options

TTL

Chaque driver a la possibilité d’avoir un temps limite pour la durée de la maintenance -par défaut il n’y en a pas-. Cette option est le ttl -time to life- qui peut être modifié soit dans votre configuration soit dynamiquement via la console par exemple. (A l’exception du FileDriver).

IP autorisées

Vous avez la possibilité, comme écrit plus haut, d’ajouter dans un tableau des adresses IP qui seront autorisées à consulter le site.

Installation et configuration

L’installation se fait comme pour tous les autres bundle Symfony2 à l’aide des fichiers deps et deps.lock.

Fichier deps:

Enregistrer le namespace:

Activer le bundle:

Pour la configuration, les options de bases requises sont la classe du driver et les options qui iront avec.

Remarques pour la configuration:

  • Pour utiliser la connexion Doctrine, vous n’êtes pas obligé de passer des options, vous pouvez écrire simplement le nom de la classe du driver.
  • Pour utiliser une table existante sur une autre base de données (option dsn), vous devez avoir un champ ttl dans votre table.
  • Pour le FileDriver, vous ne pouvez pas changer la durée en mode console, seulement pour DatabaseDriver et MemCacheDriver

Utilisation

Ce service met à disposition les méthodes `lock()` et `unlock()` permettant d’activer et désactiver le mode maintenance de votre site. L’intérêt majeur de ce service est qu’il vous permet de l’utiliser dans le contexte de votre choix, libre à vous d’implémenter une interface de backend permettant d’activer la maintenance. LexikMaintenanceBundle propose en standard les commandes console suivantes:

lexik:maintenance:lock qui aura pour effet d’appeler la méthode lock().

lexik:maintenance:unlock aura l’effet inverse.

Pour la vue

Element important de la maintenance, une page d’erreur à afficher. Lorsque le listener lève une exception avec le status 503, Symfony2 va afficher par défaut la vue error.html.twig située dans le bundle TwigBundle. Vous pouvez alors définir une vue appelée error503.html.twig dans app/Resources/TwigBundle/views/Exception/ qui sera appelée à la place.

Astuce: Cela fonctionne aussi pour les erreurs 404, 403 etc…

Information: Nous ne proposons pas de vue par défaut, c’est à vous d’en créer une.

Pour plus le reste, voici le lien vers les sources Github.

October 28, 04:23 AM

Bonjour à tous, voici le premier bundle Symfony2 made in Lexik, j’ai nommé LexikTranslationBundle. Le but de ce bundle est simple, pouvoir gérer le contenu des fichiers de traduction (xliff, yml, php) via la base de données afin de faciliter l’édition les traductions sans avoir à mettre le nez dans le code source du projet.

Pourquoi utiliser la base de données ?

En effet, pourquoi utiliser la base de données au lieu d’éditer directement les fichiers de traduction ? Lorsque votre site sera en production des modifications pourront être faites grâce à l’interface d’édition, cependant les développeurs seront potentiellement amenés à ajouter de nouvelles traductions dans les fichiers. Donc si on édite directement les fichiers de traduction un problème risque de se présenter: lors des mises en ligne, comment ajouter les nouvelles traductions sans perdre les modifications faites depuis l’interface d’édition ? (sans compter le fait que Apache devra avoir les droits en écriture sur les fichiers).
C’est pour cela que nous avons choisi de charger les traductions en base de données. De cette façon l’administrateur du site peut modifier les traductions à son gré et les développeurs peuvent à tout moment ajouter de nouvelles entées dans les fichiers.

Fonctionnement

Le bundle redéfinit le service de traduction de Symfony (translator) et fournit un loader pour charger les traductions depuis la base de données.
Les traductions présentes en base de données sont chargées en dernier, elles prennent donc le dessus sur les traductions des fichiers xliff, yml ou php. Le bundle fournit aussi une interface d’édition et d’ajout de traductions avec une intégration de jqGrid 4.2.0.
L’ajout de traduction dans la base de données se fait soit par l’interface, soit en ligne de commande avec une tâche qui va importer vos fichiers de traduction dans la base de données.

Utilisation

Lors de votre développement créez vos fichiers de traductions au moins pour une langue (la langue par défaut du site par exemple). Ensuite lancez la tâche d’import pour ajouter les nouvelles traductions en base de données. Une fois importées les traductions sont éditables dans l’interface, vous pouvez donc modifier les traductions pour toutes les langues gérées par le bundle.
Une traduction n’est éditable que si elle est présente dans la base de données, donc si vous ajoutez de nouvelles entées dans vos fichiers de traduction il faudra relancer la tache d’import.

Note: lors de la modification d’une traduction via l’interface il sera nécessaire de cliquer sur le bouton « invalidate cache » afin de prendre en compte le changement de la traduction (surtout en environnement de production).

Installation et configuration

L’installation se fait comme pour tous les autres bundle Symfony2 à l’aide des fichiers deps et deps.lock.
Au niveau de la configuration il vous faudra au minimum définir la langue par défaut (fallback_locale) et les langues que le bundle doit gérer (managed_locales), comme ci-dessous :

# app/config/config.yml
lexik_translation:
    fallback_locale:  fr
    managed_locales:  [fr, en]

Il est aussi possible de configurer certaines classes utilisées par le bundle dans le cas où vous souhaitez redéfinir des éléments (entité et/ou service).

Pour plus de détail sur cette partie je vous invite à lire la doc sur le dépot Github.

September 06, 08:20 AM

Bonjour,

Nous allons voir dans cet article la mise en place d’un callback sur une entité dans symfony2.

Contexte

Nous allons ici prendre l’exemple d’un site de commande. Chaque commande a un statut et nous souhaitons enregistrer dans la base de données chaque changement de statut afin de conserver un historique.

Pour cela, Symfony2 et Doctrine fournissent un ensemble d’actions pouvant être appelées à chaque étapes du cycle de vie d’une entité (« lifecycle »).

Vous trouverez à cette adresse l’ensemble des événements disponibles: Lifecycle Events

Dans notre cas, nous avons choisi d’effectuer la sauvegarde du statut après chaque mise à jour de notre entité Order (qui gère les commandes du site). Pour cela nous allons utiliser l’événement postUpdate qui sera appelé après chaque update de l’entité.

Voici, les deux objets concernés par ce service:

L’objet commande

/**
 * Projet\OrderBundle\Entity\Order
 *
 * @ORM\Entity(repositoryClass="Projet\OrderBundle\Repository\OrderRepository")
 * @ORM\Table(name="Projet_order_order")
 * @ORM\HasLifecycleCallbacks
 */
class Order
{
     /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
 
    /**
     * @var Yatoo\UserBundle\Entity\User $user
     *
     * @ORM\ManyToOne(targetEntity="Yatoo\UserBundle\Entity\User", inversedBy="orders")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     */
    protected $user;
 
    /**
     * @var string $comment
     *
     * @ORM\Column(name="title", type="string", length=255)
     */
    protected $title;
 
    /**
     * @var decimal $price
     *
     * @ORM\Column(name="price", type="decimal", length="7", scale="2")
     */
    protected $price;
 
    /**
     * @var integer $quantity
     *
     * @ORM\Column(name="quantity", type="integer")
     */
    protected $quantity;
 
    /**
     * @var Yatoo\CardBundle\Entity\Pricing $pricing
     */
    public $pricing;
 
     /**
     * @var smallint $status
     *
     * @ORM\Column(name="status", type="smallint")
     */
    protected $status;
 
    /**
     * @var text $comment
     *
     * @ORM\Column(name="comment", type="text", nullable="true")
     */
    protected $comment;
 
    /**
     * @ORM\OneToMany(targetEntity="HistoricalStatus", mappedBy="order", cascade={"all"})
     * @ORM\JoinColumn(name="order_id", referencedColumnName="id")
     */
    protected $historicalStatus;
 
    /**
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(name="created_at", type="datetime")
     */
    protected $createdAt;
 
    protected $statusOld;
 
    ...
 
}

Et HistoricalStatus qui servira à stocker l’historique des changements de statut des commandes.

/**
 * Projet\OrderBundle\Entity\HistoricalStatus
 *
 * @ORM\Entity(repositoryClass="Projet\OrderBundle\Repository\HistoricalStatusRepository")
 * @ORM\Table(name="Projet_order_historical_status")
 * @ORM\HasLifecycleCallbacks
 */
class HistoricalStatus
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
 
    /**
     * @var object $order
     *
     * @ORM\ManyToOne(targetEntity="Order", inversedBy="files")
     * @ORM\JoinColumn(name="order_id", referencedColumnName="id")
     */
    protected $order;
 
 
    /**
     * @var smallint $status
     *
     * @ORM\Column(name="status", type="smallint")
     */
    protected $status;
 
    /**
     * @var text $comment
     *
     * @ORM\Column(name="comment", type="text", nullable="true")
     */
    protected $comment;
 
    /**
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(name="created_at", type="datetime")
     */
    protected $createdAt;
 
    ...
}

Mise en place

Dans un premier temps, il faut créer le dossier Listener dans votre bundle.Dans notre exemple ce dossier se trouvera à l’adresse : repSite/src/Projet/OrderBundle/Listener puis créer le fichier OrderListener.php.

Voici pour exemple le code de notre service:

namespace Projet\OrderBundle\Listener;
 
use Projet\OrderBundle\Entity\HistoricalStatus;
use Projet\OrderBundle\Entity\Order;
 
use Doctrine\ORM\Event\LifecycleEventArgs;
 
class OrderListener
{
    public function postUpdate(LifecycleEventArgs $args) {
 
        $entity = $args->getEntity();
        $entityManager = $args->getEntityManager();
 
        if ($entity instanceof Order) {
 
            if($entity->getOldStatus() != $entity->getStatus()) {
                $historicalStatus = new HistoricalStatus();
                $historicalStatus->setOrder($entity);
                $historicalStatus->setComment($entity->getComment());
                $historicalStatus->setStatus($entity->getStatus());
                $entityManager->persist($historicalStatus);
                $entityManager->flush();
            }
        }
      }
}

Pour expliquer rapidement ce que fait ce bout de code :

Si le statut de la commande a changé, on crée un nouveau statut historique et on l’enregistre.

La fonction $entity->getOldStatus() permet de récupérer l’ancien statut de la commande avant sa mise à jour. La valeur est initialisée à la création de l’objet dans le constructeur.

Configuration

Une fois votre listener écrit, il faut le configurer pour qu’il soit appelé. Pour cela, éditer simplement le fichier : Projet\OrderBundle\Ressources\config\services.xml et ajoutez y ces lignes:

<service id="order.postUpdate" class="Projet\OrderBundle\Listener\OrderListener">
            <tag name="doctrine.event_listener" event="postUpdate"/>
</service>

Vous pouvez aussi le déclarer en yml :

services:
    order.postUpdate :
        class: Projet\OrderBundle\Listener\OrderListener
        tags:
            - { name: doctrine.event_listener, event: postUpdate }
September 01, 08:03 AM

Toi l’ami qui travaille dans le Web, rejoins nous à l’apéro Web de Montpellier :

Date : Jeudi 8 Septembre
Lieu : au Fitzpatrick’s Irish pub (Map)
Heure : à partir de 18H30.

Vous pensez venir, passez sur le Doodle!

Les apéros web ont pour objectif de donner un cadre d’échange aux acteurs du web. Que vous soyez développeurs, webdesigner, chef de projet agile ou non, au marketing, ou resp. d’entreprise les apéros web vous permettront de discuter hors contexte entreprise / travail de votre métier et pouvoir échanger librement sur vos problématiques.

August 26, 04:00 AM

Un problème récurrent pour beaucoup de développeurs utilisant Symfony est l’éternel conflit avec l’utilisateur « www-data » lors des accès, par exemple, aux fichiers du cache et aux logs. Sur Symfony 2 ce problème devient encore plus visible avec l’utilisation massive du répertoire cache même en environnement « dev ».

Le cas de conflit le plus flagrant est certainement le app/console cache:clear qui renvoie une erreur car le cache a été généré par www-data lors du chargement du site par apache. Le bon gros sudo rm -rf app/cache/* n’est pas une solution acceptable, tout juste une rustine.

Il existe différents moyens d’éviter ces conflits, que ce soit via scripts personnalisés, configurations avancées, ou packages additionnels. SuPHP est un module pour Apache très facile à mettre en place pour un environnement de « dev », et il va répondre à notre problème d’une manière très simple: Apache ne va plus éxécuter les sites via ‘www-data’, mais via l’utilisateur propriétaire des fichiers éxécutés, c’est-à-dire le plus souvent le compte du développeur.

L’installation comprend deux packages dispos sur synaptic ou directement depuis la console:

sudo apt-get install suphp-common libapache2-mod-suphp

Pour une station de dev, une configuration fonctionnelle et simple est très bien décrite sur http://doc.ubuntu-fr.org/suphp#installation_de_suphp. Cette configuration empêche l’intervention de suphp dans les répertoires de /usr/share pour permettre entre autres à phpmyadmin (dans son emplacement par défaut) de continuer à fonctionner sans problèmes.

Il faudra éventuellement supprimer les fichiers de sessions en cours appartenent encore à www-data (dans /var/lib/php5 par défaut).

Si vos projets ne sont pas stockés dans les répertoires /var/www ou ~/public_html, il faut éditer la valeur de « docroot » dans /etc/suphp/suphp.conf pour y ajouter les répertoires supplémentaires. Par exemple, mes projets sont dans ~/www/ :

docroot=/var/www:${HOME}/public_html

devient alors:

docroot=/var/www:${HOME}/public_html:${HOME}/www

Pensez à nettoyer vos fichiers appartenant encore à www-data (cette fois avec des sudo rm par contre), comme les répertoires cache et logs, et redémarrez simplement Apache (sudo service apache2 restart). Après ça vous ne devriez plus voir de www-data dans vos projets, et vous pourrez oublier ce mauvais réflexe du sudo rm -rf à tout bout de champ !

July 27, 12:58 PM

Fêtons de lancement de Symfony2 au Shakespeare

A partir de 19H00
The Shakespeare Pub
12 Rue de la Petite Loge, 34000 Montpellier
04 67 60 22 25 ‎


Agrandir le plan

July 25, 08:34 AM

Pour calculer les distances entre deux points géographiques il faut se baser sur leurs latitudes et longitudes. Lorsque l’on souhaite appliquer ce calcul en SQL on se retrouve avec une requête extrêmement verbeuse. Heureusement si vous utilisez Doctrine2 vous allez pouvoir créer votre propre fonction DQL qui vous permettra de faire ce calcul très simplement.

Dans un premier temps il va falloir ajouter les propriétés latitude et longitude à notre modèle, imaginons que nous souhaitons calculer la distance entre deux villes.

<?php

namespace Acme\GeoBundle\Entity

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @ORM\Entity
* @ORM\Table(name="city")
*/
class City
{
    /**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
    protected $id;
    
    /**
* @ORM\Column(name="label")
* @Assert\NotBlank()
*/
    protected $label;

    /**
* @ORM\Column(type="decimal", scale="8")
* @Assert\NotBlank()
*/
    protected $latitude;

    /**
* @ORM\Column(type="decimal", scale="8")
* @Assert\NotBlank()
*/
    protected $longitude;

// Getter and Setter
}
view raw gistfile1.php This Gist brought to you by GitHub.

Une fois que nous avons nos colonnes nous pourrions écrire la requête suivante pour connaître la distance en kilométres qui sépare les villes de notre base de données de Montpellier:

SELECT
  ((ACOS(SIN(43.61 * PI() / 180) * SIN(latitude * PI() / 180) + COS(43.61 * PI() / 180) * COS(latitude * PI() / 180) * COS((3.87 - longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344)
FROM city
view raw gistfile1.sql This Gist brought to you by GitHub.

Heureusement le parser de Doctrine2 va me permettre de créer une fonction DQL qui sera ensuite retranscris en SQL.

<?php

namespace Acme\GeoBundle\AST\Functions;

use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\Lexer;

class Geo extends FunctionNode
{
    /**
     * @var \Doctrine\ORM\Query\AST\ComparisonExpression
     */
    private $latitude;
    /**
     * @var \Doctrine\ORM\Query\AST\ComparisonExpression
     */
    private $longitude;
    
    /**
     * Parse DQL Function
     *
     * @param Parser $parser
     */
    public function parse(Parser $parser)
    {
        $parser->match(Lexer::T_IDENTIFIER);
        $parser->match(Lexer::T_OPEN_PARENTHESIS);
        $this->latitude = $parser->ComparisonExpression();
        $parser->match(Lexer::T_COMMA);
        $this->longitude = $parser->ComparisonExpression();
        $parser->match(Lexer::T_CLOSE_PARENTHESIS);
    }

    /**
     * Get SQL
     *
     * @param SqlWalker $sqlWalker
     * @return string
     */
    public function getSql(SqlWalker $sqlWalker)
    {
        return sprintf('((ACOS(SIN(%s * PI() / 180) * SIN(%s * PI() / 180) + COS(%s * PI() / 180) * COS(%s * PI() / 180) * COS((%s - %s) * PI() / 180)) * 180 / PI()) * 60 * %s)',
                $this->latitude->rightExpression->dispatch($sqlWalker),
                $this->latitude->leftExpression->dispatch($sqlWalker),
                $this->latitude->rightExpression->dispatch($sqlWalker),
                $this->latitude->leftExpression->dispatch($sqlWalker),
                $this->longitude->rightExpression->dispatch($sqlWalker),
                $this->longitude->leftExpression->dispatch($sqlWalker),
                '1.1515 * 1.609344');
    }
}

Que se passe t’il dans cette fonction ? Dans un premier temps la méthode Geo::parse() va parcourir le DQL et en extraire les parties souhaitées.

  • Le parser reconnaît ma fonction (ligne 28)
  • J’ouvre ma parenthèse (ligne 29)
  • Le parser récupère ma première expression : latitude = :latitude (ligne 30)
  • Je mets une virgule (ligne 31)
  • Le parser récupère ma seconde expression : longitude = :longitude (ligne 32)
  • Je ferme ma parenthèse

Maintenant que ma fonction est capable de parser mon DQL il va falloir qu’elle me genère du SQL, tout se passe dans la méthode getSql. Lorsque j’ai parsé mon DQL j’ai stocké les expressions correspondants à la longitude et latitude en tant qu’expression de comparaison (Parser::ComparisonExpression). Je vais ensuite pouvoir récupérer la partie qui m’intéresse pour construire mon SQL.

<?php

$this->latitude->rightExpression->dispatch($sqlWalker) // Correspond à ma colonne latitude
$this->latitude->leftExpression->dispatch($sqlWalker) // Correspond à la valeur de la latitude
view raw gistfile1.aw This Gist brought to you by GitHub.

Désormais il me suffira décrire pour connaître la distance entre toutes les villes de ma base et Montpellier :

<?php

$entityManager->createQueryBuilder()
                ->from('AcmeGeoBundle:City', 'c')
                ->select('GEO(c.latitude = :latitude, c.longitude = :longitude)')
                ->setParameter('latitude', '43.61')
                ->setParameter('longitude', '3.87')
                ->getQuery()
                ->execute();
view raw gistfile1.php This Gist brought to you by GitHub.

Pour terminer il va falloir enregistrer cette fonction sur votre EntityManager :

# Doctrine Configuration
doctrine:
    dbal:
        driver: %database_driver%
        host: %database_host%
        dbname: %database_name%
        user: %database_user%
        password: %database_password%
        charset: UTF8

    orm:
        auto_generate_proxy_classes: %kernel.debug%
        entity_managers:
            default:
                auto_mapping: true
                dql:
                    numeric_functions:
                        geo: Acme\GeoBundle\AST\Functions\Geo
view raw gistfile1.yml This Gist brought to you by GitHub.

Doctrine2 permet donc d’étendre très facilement son système de fonction. Dans l’exemple donné ici il est évident que l’écriture d’une nouvelle fonction nous a simplifié l’écriture de nos futures requêtes DQL.

July 18, 12:48 PM

Après 2 ans et demi de bons et loyaux services, le thême trouvé rapidement sur internet s’en est allé et laisse place à une version plus personnelle de l’équipe de développement.

Retrouvez nos développeurs Symfony cachés entre des div et des hn.

Quoi de neuf chez Lexik ?

  • un nouveau logo ;
  • des projets Symfony2 ;
  • de nouveaux développeurs ;
  • un championnat de pétanque tout l’été ;
  • des applications mobiles pour le blog.
April 20, 07:51 AM

Lexik est une agence Web spécialisée dans le développement d’application Web et de sites Internet sur-mesure en PHP avec le framework Symfony. Nous souhaitons renforcer nos équipes de développement par 2 développeurs pour nos projets clients et internes. Vous avez envie de vous intégrer dans une jeune société (âge moyen : entre 25 et 30 ans) avec une ambiance agréable et dynamique, de travailler en équipe sous la responsabilité d’un chef de projet.

Votre profil :

De formation BTS / DUT ou école d’ingénieur, vous justifiez d’une expérience significative dans le milieu du développement Web.

Vous bénéficiez d’une bonne expérience à titre privé ou professionnel dans les technologies suivantes :

  • XHTML / CSS ;
  • PHP 5, orienté objet ;
  • SQL (MySQL, SQLite, PostgreSQL, …).

Une expérience ou un intérêt dans les domaines suivants seront un atout :

  • Framework symfony 1.4 / Symfony2 avec l’ORM Doctrine ;
  • Gestionnaire de version (Git / SVN) ;
  • Javascript ;
  • Tests unitaires et fonctionnels / Intégration continue ;
  • Méthodologie de développement agile (SCRUM).

Détails :

Envoyez-nous votre CV et lettre de motivation par email à l’adresse : contact@lexik.fr

  • Type de contrat : CDI
  • Lieu : Pérols
  • Début du contrat : ASAP
  • Télétravail : non
  • Rémunération selon profil

http://www.lexik.fr/

April 14, 04:17 AM

Petit constat :

La gestion des expéditions de mails dans les projets Web est souvent laborieuse. En effet, les serveurs ne sont pas toujours bien configurés pour ça. Il arrive que les emails partent en spam ou encore qu’ils soient bloqués avant même d’entrer dans la boite mails des utilisateurs… Bref, c’est jamais pratique et le suivi se fait très difficilement.

Quelques services d’envoi :

La solution réside bien souvent dans le fait de déporter l’expédition sur une plateforme spécialisée. Il en existe des dizaines :
Amazon a récemment sorti une plateforme Amazon SES qui permet d’expédier ses emails via un simple appel à une API. Cette solution est très pratique mais a pour inconvénient de changer un peu le processus d’envoi d’email, il faudra donc faire quelques adaptations dans le code de votre application.
Il existe également d’autres solutions :

pour en citer quelques uns…

Ces solutions sont idéales pour gérer des newsletters mais sont contraignantes pour les emails transactionnels. Eh oui, il faut souvent se rendre sur l’interface du site pour faire ses expéditions ou tout traiter via leur API.

Une solution intéressante : Mailjet

Les emails de vos projets Web sont souvent délivrés via un serveur SMTP, Mailjet est un service en ligne qui a eu la bonne idée de proposer d’exploiter un serveur SMTP authentifié et donc optimisé pour assurer la délivrabilité des emails. L’autre avantage indéniable de cette solution est qu’il suffit de changer les paramètres du SMTP de votre application et le tour est joué, vos emails sont délivrés par Mailjet. En prime Mailjet vous propose un système de statistiques sur les expéditions que vous pouvez consulter sur leur plateforme ou encore interroger à distance, via leur API.

Nous avons testé cette solution sur un projet pour envoyer des mails transactionnels tels que des mails d’inscriptions, de notifications ainsi qu’une newsletter hebdomadaire. Mailjet propose plusieurs offres dont une gratuite permettant d’envoyer jusqu’à 6000 mails par mois ce qui dans notre cas était suffisant.

Exemple de configuration du SMTP :

# apps/<app_name>/config/factories.yml
prod:
  mailer:
    param:
      transport:
        class: Swift_SmtpTransport
        param:
          host:       in.mailjet.com
          port:       465
          encryption: ssl
          username:   "your-username"
          password:   "your-pass"

Une fois cette petite configuration faite vos mails seront délivrés par le SMTP de Mailjet qui s’occupe d’envoyer vos mails et qui fera aussi en sorte de limiter les risques de non livraison des mails.

PS : Non, non, non ce billet n’est pas sponsorisé! C’est juste que cette solution est vraiment pratique et souhaitions la partager.

March 11, 05:30 AM

Beaucoup d’entre vous ont certainement déjà eu l’occasion de jouer avec l’api javascript de GoogleMaps. Elle est performante, bien documentée, et plutôt instinctive à implémenter. De plus il éxiste énormément de ressources sur le web proposant des exemples d’utilisation.
On trouve cependant encore peu de ressources mentionnant la dernière mouture de l’api, la v3, qui simplifie et éclaircie encore plus son utilisation. Ce petit article va survoler quelques exemples d’utilisation de celle-ci.

La première grosse nouveauté de la v3, c’est la mort de la clé api ! En effet, il n’est plus nécessaire de déclarer un domaine pour récupérer une clé, ou de se battre avec tout un trousseau pour jongler entre des environnements de dev, preprod, prod, test, etc… Maintenant on charge simplement l’api comme n’importe quelle librairie Javascript.
Second changement notoire, plus aucun appel Ajax explicite vers l’api REST n’est nécessaire pour récupérer des données de géolocalisation. Un simple passage de paramètres à des objets Javascript nous rendra les informations voulues en un temps record.

Commençons donc par charger l’api et afficher une map:

?View Code JAVASCRIPT
<div id="gmap-div" style="width: 500px; height: 400px;"></div>
 
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false" />
<script type="text/javascript">
$(function() {
  //J'utilise dans l'exemple jQuery pour améliorer la lisibilité du code, le focus étant sur GoogleMaps
 
  //tout d'abord on définit le centre de notre map par sa latitude et longitude, par exemple Montpellier: 
  var latLng = new google.maps.LatLng(43.60, 3.88);
  //puis on créé la map
  var gmap = new google.maps.Map(document.getElementById('gmap-div'), {
    zoom: 10, //le zoom de départ
    center: latLng, //le centre de la map au chargement
    mapTypeId: google.maps.MapTypeId.ROADMAP, //le type de map, ROADMAP correspond à la version par défaut des versions précédentes
    streetViewControl: false, //si on souhaite désactiver les contrôle StreetView
    panControl: false //si on souhaite masquer les contrôles de déplacement
  });
});	
</script>

Créons maintenant quelques interactions. Pour commencer un formulaire va nous permettre de rechercher une ville pour ensuite placer un marqueur à son emplacement sur la map:

<form id="search-form" action="#">
  <input type="text" id="search-query" size="20" />
  <input type="submit" value="chercher" />
</form>

Et le code javascript correspondant:

?View Code JAVASCRIPT
//on créé un geocoder qui s'occupera des requètes de géolocalisation
var geocoder = new google.maps.Geocoder();
 
$('#search-form').submit(function(e) {
  e.preventDefault();
 
  //l'objet request que l'on passe au geocoder
  var request = {
    address:    $('#search-query').val(),
    region:     'fr', //pour améliorer la qualité des résultats en précisant que l'on cherche principalement en France
    language:   'fr'
  };
  //aucune requète ajax nécessaire !
  geocoder.geocode(request, function(results, status) {
    if (status == 'OK' && results.length) {
      //on récupère les coordonnées du premier résultat
      var lat = results[0].geometry.location.lat();
      var lng = results[0].geometry.location.lng();
 
      //puis on recentre la map...
      var newCenter = new google.maps.LatLng(lat, lng);
      gmap.setCenter(newCenter);
 
      //...et on pose un marqueur dessus
      new google.maps.Marker({
	position:  newCenter,
        map:       gmap
      });
    } else {
      console.log('aucun résultat');
    }
  });
});

Plutôt simple pour l’instant. Allons un peu plus loin et traçons une ligne qui va relier au fur et à mesure toutes les villes marquées. Il va falloir pour cela utiliser la librairie supplémentaire de GoogleMaps « Geometry ». N’étant pas une librairie chargée par défaut, il faut préciser son utilisation au chargement de GoogleMaps:

<script type="text/javascript" src="http://maps.google.com/maps/api/js?libraries=geometry&sensor=false"></script>

Profitons-en pour afficher la longueur en kilomètre de notre itinéraire dans une div que l’on va tout de suite rajouter au document:

<div id="total-length"></div>

Pour tracer une ligne reliant des points sur une GoogleMap, il faut utiliser la classe « Polyline ». Déclarons-la juste après la map en début de script:

?View Code JAVASCRIPT
  /* ... */
  var polyline = new google.maps.Polyline({
    strokeColor:   '#2222FF', //on définit la couleur
    strokeOpacity: 0.5,       //l'opacité
    strokeWeight:  3,         //l'épaisseur du trait,
    map:           gmap       //la map à laquelle rattacher la polyline
  });
 
  //on peut extraire un objet renfermant tous les points d'une Polyline
  //cet objet nous sera utile pour calculer la distance de l'itinéraire
  var path = polyline.getPath();

Il faut maintenant étoffer le code de création du marqueur:

?View Code JAVASCRIPT
  /* ... */
  geocoder.geocode(request, function(results, status) {
    if (status == 'OK' && results.length) {
      var lat = results[0].geometry.location.lat();
      var lng = results[0].geometry.location.lng();
 
      var newCenter = new google.maps.LatLng(lat, lng);
      gmap.setCenter(newCenter);
 
      new google.maps.Marker({
        position:  newCenter,
        map:       gmap
      });
 
      //on ajoute le nouveau point à notre path, le Polyline associé se met automatiquement à jour
      path.push(newCenter);
 
      //on affiche la longueur du path dans notre div
      //c'est ici qu'intervient la librairie Geometry, qui va gérer automatiquement tous les calculs nécessaires
 
      var pathLength = parseInt(google.maps.geometry.spherical.computeLength(path)) / 1000;
      //computeLength nous renvoie des mètres que l'on transforme en kilomètres
 
      //on met à jour l'affichage
      $('#total-length').html(pathLength + ' km');
 
    } else {
      console.log('aucun résultat');
    }
  });
  /* ... */

Il a suffi d’une poignée de lignes pour rajouter ces informations, et le résultat est plutôt sympathique.

On remarque tout de même un détail ennuyeux: il faut déplacer et zoomer à la main si on veut avoir une vue globale et centrée de notre itinéraire à chaque nouvel ajout de marqueur. A la manière du « path » qui stocke les informations d’une Polyline, une map possède des « bounds » qui définissent les limites de son affichage. La classe « LatLngBounds » arrive ici en jeu et va nous permettre d’optimiser automatiquement l’affichage de notre itinéraire.

?View Code JAVASCRIPT
  //en début de script avec les déclarations, on créé notre objet bounds
  var bounds = new google.maps.LatLngBounds();
 
  /* ... */
 
  //à la suite de l'ajout d'un nouveau marqueur, on étend la zone "bounds" en lui demandant d'inclure le nouveau point
  bounds.extend(newCenter);
  //on demande en suite à la map de s'adapter à cette zone
  gmap.fitBounds(bounds);
 
  /* ... */

Et c’est tout ! Encore trois malheureuses lignes pour une fonctionnalité relativement puissante. Vous pouvez voir l’exemple tourner sur cette page.

La richesse de cette dernière version de l’api, couplée à la fluidité de son utilisation, rend l’intégration d’une GoogleMap dans une application riche vraiment agréable. Jetez un oeil à la documentation officielle très complète si vous souhaitez en apprendre plus !

Les sources de l’exemple de cet article sont disponibles sur Github.

March 02, 09:00 AM

Jérémy Barthe et Cédric Girard seront présent au symfony live à Paris les 3 et 4 mars pour la présentation de symfony 2. Retrouvez le programme ici: http://www.symfony-live.com/paris/schedule et pour les inscriptions ca doit surement être trop tard! Les prochains …

...

February 16, 05:31 AM

Voici le petit dernier des plugins Symfony de chez Lexik, lxErrorLoggerPlugin, son but est simple : vous alerter en cas d’erreurs PHP ou Exceptions sur vos projets Symfony.
Le besoin est simple, être alerté et éventuellement logger chaque erreurs, qu’elles soient PHP, Exception ou erreurs remontées par Symfony. En effet le logger de base de Symfony s’arrête aux erreurs remontées par ses soins mais ne remontent pas forcément aux erreurs PHP. Le système de notification du plugin est très flexible grâce à une série de « notifier » que l’on peut activer ou non de façon indépendante les uns des autres.

Voici les différents notifiers que nous proposons :

  • stockage en base de données (celle par défaut, ou via un dsn personnalisé) ;
  • stockage en fichier XML ;
  • stockage en fichier de log classique (fichier texte) ;
  • notification via un envoi de mail ;
  • notification via Hoptoad, un webservice de log d’erreur.

Le plugin va par défaut logger toutes les exceptions levées, par contre pour les erreurs PHP il est évidement possible de configurer l’error_reporting, ce qui permet de remonter seulement les erreurs et non les notices par exemple. La configuration du plugin se fait tout simplement dans le fichier app.yml de votre application (ou du projet). Chaque « notifier » possède ses propres options, pour plus de détail je vous invite à regarder le README du plugin.

Voici par exemple une capture d’écran d’un email envoyé par lxErrorLoggerPlugin :

Comme il a été précisé en introduction, lxErrorLoggerPlugin permet de notifier les erreurs mais également de les stocker. Le stockage se fait via une base de données ou un fichier XML. Cela apporte 2 avantages indéniables :

  • la possibilité de repérer des erreurs similaires à d’autres, il est alors possible de prévenir les notifiers tels que le notifier email pour ne pas envoyer un email doublon, tout ceci est configurable ;
  • la possibilité de générer un flux RSS des erreurs, l’URL du flux sera protégé par un token paramètrable par vos soins.

Exemple d’utilisation :

Supposons que nous avons besoin de logger les erreurs qui se produisent sur notre application frontend.

1. Activer le plugin :

// config/ProjectConfiguration.class.php
  public function setup()
  {
    $this->enablePlugins(array(
      ...,
      'lxErrorLoggerPlugin',
    ));
  }

2. Configurer le plugin dans l’application souhaité, frontend dans cet exemple :
Ici nous allons utiliser la notification des erreurs en base de données et par email.

# apps/frontend/config/app.yml
all:
  lx_error_logger_plugin:
    enabled:              true # Enable error notification
    php_error_reporting:  <?php echo (E_ALL | E_STRICT)."\n" ?>
    rss:
      token:              264864156479631564841687 # token to access to the rss
      items:              10 # items in the rss
    notifier:
      db:
        enabled:          true
        options:
          similar_error:  true
      mail:
        enabled:          true
        options:
          to:             [bob@example.com, chuck@example.com]
          subject:        A new error occured on your website
          always_send:    true # always send an email even if db or xml notifiers detect the error as similar

3. Activer le flux RSS des dernières erreurs :
Dans cet exemple la notification des erreurs en base de données est activée donc le flux RSS peut récupérer des données.

# apps/frontend/config/settings.yml
all:
  .settings:
    enabled_modules: [..., lxErrorNotifierRss]

Le flux RSS est maintenant accessible avec l’url :

http://super-website.com/lx-error/rss.xml?token=264864156479631564841687

Et c’est tout

Note : le plugin ne fournit aucune interface permettant de lister les erreurs loggées, libre à chacun de créer son CRUD, module d’admin generator ou autre interface comme il le souhaite en fonction du besoin.

Dépôt du plugin sur Github : https://github.com/lexik/lxErrorLoggerPlugin.

January 18, 04:53 AM

L’article d’hier sur les bonnes pratiques Javascript dans un projet Symfony faisait mention d’un plugin que nous utilisons en interne et qui est maintenant disponible sur Github: lxJavascriptPlugin. Ce court article va brièvement présenter son fonctionnement et son utilisation.

L’installation du plugin est très rapide:

  • téléchargez les sources sur Github dans le répertoire plugins de votre projet,
  • éditez le fichier config/settings.yml pour y inclure le helper lxJavascript:
    all:
      .settings:
        standard_helpers: [ ... , lxJavascript ]

Et c’est tout! Voyons maintenant le fonctionnement.

Le premier but du plugin est de solutionner les problèmes de granularité du code Javascript dans un projet Symfony. Lorsque l’on utilise par exemple des widgets JQuery, des partials pour les réseaux sociaux, ou des composants pour des GoogleMap, on se retrouve vite avec du code Javascript éparpillé dans tout notre projet, et par conséquent tout autant éparpillé dans le code des pages générées.

Pour régler ce problème lxJavascriptPlugin propose un premier outil: le helper lxJavascriptHelper. Son utilisation est très similaire à celle du javascriptHelper de Symfony, mais avec la méthode lx_javascript:

  • pour inclure un fichier source externe à la page:
    <?php lx_javascript('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js') ?>
  • pour inclure directement du code javascript, par exemple le bout de code pour Google Analytics, le fonctionnement est similaire à celui d’un slot:
    <?php lx_javascript() ?>
      var _gaq = _gaq || [];
      _gaq.push(['_setAccount', '*********']);
      _gaq.push(['_trackPageview']);
      (function() {
        var ga = document.createElement('script');
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
        ga.setAttribute('async', 'true');
        document.documentElement.firstChild.appendChild(ga);
      })();
    <?php lx_end_javascript() ?>

    Ici la balise <script> n’est pas nécessaire, mais vous pouvez l’utiliser si vous le souhaitez (par exemple pour garder la coloration syntaxique de votre IDE)

  • pour insérer tout le code javascript déclaré via lx_javascript() à la fin de votre layout.php:
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
      <head>
        ...
      </head>
      <body>
        ...
        <?php lx_include_javascripts() ?>
      </body>
    </html>

Le soucis de javascript dans les templates est adressé. Reste maintenant le problème du code javascript inséré via d’autres moyens, comme par exemple un widget qui utilise JQuery. Pour cela lxJavascriptPlugin propose une classe statique lxJavascriptStorage qui offre les fonctionnalités du helper depuis n’importe où dans votre code:

  • pour ajouter une source externe:
    lxJavascriptStorage::addRemoteJavascript('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js');
  • pour ajouter du code javascript. Voyons par exemple la méthode render() du widget sfWidgetFormJQueryDate:
    public function render($name, $value = null, $attributes = array(), $errors = array())
    {
      //code
      return $this->getOption('date_widget')->render($name, $value, $attributes, $errors).
               $this->renderTag('input', array('type' => 'hidden', 'size' => 10, 'id' => $id = $this->generateId($name).'_jquery_control', 'disabled' => 'disabled')).
               sprintf(<<<EOF
    <script type="text/javascript">// <![CDATA[
      [...]
    // ]]></script>
    EOF
        ,
        //arguments du sprintf
      );
    }

    Nous voyons bien le javascript retourné dans le code du widget, et donc en milieu de page. Cette fonction render() pourrait, avec lxJavascriptPlugin, ressembler à ça:

    public function render($name, $value = null, $attributes = array(), $errors = array())
    {
      //code </code>
     
      $js = sprintf(<<<EOF
    <script type="text/javascript">// <![CDATA[
      [...]
    // ]]></script>
    EOF
        ,
        //arguments du sprintf
      );
      lxJavascriptStorage::addJavascriptCode($js);
     
      return $this->getOption('date_widget')->render($name, $value, $attributes, $errors).
               $this->renderTag('input', array('type' => 'hidden', 'size' => 10, 'id' => $id = $this->generateId($name).'_jquery_control', 'disabled' => 'disabled'));
    }

    Peu de changements, mais ici le javascript sera rendu avec tout le reste du javascript en pied de page.

Notez enfin que la méthode lx_include_javascripts() rendra en premier les sources externes, puis le code javascript concaténé dans une unique balise <script>. Si tous les exemples précédents sont utilisés dans une application, le javascript en pied de page donnera ceci:

?View Code JAVASCRIPT
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">// <![CDATA[
  //javascript du widget
 
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', '*********']);
  _gaq.push(['_trackPageview']);
  (function() {
    var ga = document.createElement('script');
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    ga.setAttribute('async', 'true');
    document.documentElement.firstChild.appendChild(ga);
})();
/* ]]> */</script>

Vous pouvez obtenir lxJavascriptPlugin sur Github.

January 17, 10:06 AM

Dans cet article nous allons voir quelques astuces et bonnes pratiques, non pas directement de développement symfony mais de développement javascript au sein d’un projet symfony.

Si l’on reprend quelques bases de bonnes pratiques de développement javascript, on constate :

  • que vos javascripts doivent être non-intrusifs, autrement dit là pour améliorer l’expérience utilisateur et en aucun cas être indispensable au fonctionnement d’une page ;
  • que les javascript doivent être combinés en 1 seul fichier et minifié (ceci afin de limiter le nombre de requête HTTP et car le chargement de la page est arrêté à chaque balise script, notamment à cause d’un éventuel document.write(); ;
  • que l’appel au javascript doit être en bas de page ;

Cette liste est bien entendu non exhaustive, vous trouverez une liste plus détaillée par ici.

Toutes ces bonnes pratiques de développement Javascript n’ont pas toujours été facilitées dans symfony, notamment à la grande époque des helpers link_to_remote() & co. qui en ont ravi certains et fait cauchemarder d’autres. Et aujourd’hui encore bon nombre de widgets de formulaire retournent directement du code javascript (dépendant de jQuery ou autre) et qui ne fonctionneront évidemment plus dès lors que vos javascripts se trouveront en bas de page.

1. Où placer les javascripts dans le document

Idéalement et selon les recommandations de Yslow, en bas de page, donc juste avant la balise fermante de votre layout.php. Ceci afin de ne pas ralentir inutilement le début du chargement de votre page, puisque chaque balise script bloque le chargement de la page (notamment pour gérer le document.write();).
Source : http://developer.yahoo.com/performance/rules.html#js_bottom

Si cette méthode ne vous gênera pas pour les librairies tierces telles que jQuery, qu’en est-il par contre de vos codes javascript ? Ils doivent également se trouver dans des fichiers séparés, dissociés de votre document HTML. Néanmoins comme je vois souvent le besoin pour les développeurs d’avoir le code javascript au sein du template symfony, voici une technique pour qu’il soit tout de même placé en fin de page : via un slot.

Un petit exemple de code :

// indexSuccess.php :
<?php slot('javascript') ?>
<script type="text/javascript">
  console.log('javascript @ bottom');
</script>
<?php end_slot() ?>
 
// layout.php :
<?php include_javascripts() ?>
<?php include_slot('javascript') ?>

Vous pourrez ainsi éditer votre code javascript depuis votre template PHP, bénéficier par exemple de valeurs dynamiques de PHP et pour autant que le code soit bien inclus en bas de page. Toutefois je vous recommande d’écrire du javascript là où il doit se trouver, à savoir dans un fichier JS…

De plus l’inconvénient majeur du slot est qu’il ne peut contenir qu’une valeur, il est écrasé à chaque utilisation. La technique fonctionne mais est très limitée. Pour ces besoins nous avons écrit chez Lexik un petit plugin « lxJavascript », très similaire à l’utilisation d’un slot mais optimisé pour l’inclusion de javascripts. Il gère donc le multiple ajout, supprime les éventuelles multiples balises script pour en conserver qu’une seule, etc. Vous en saurez plus demain, le plugin sort en OpenSource sur le Github de Lexik.

2. Compression

La compression des javascripts (c’est valable aussi pour les CSS) est une des tâches d’optimisations les plus importantes et également une des plus simples, du moins via les plugins symfony. Cette technique consiste à compresser tous vos fichiers javascript en 1 seul et surtout d’y appliquer un numéro de version (un timestamp le plus souvent) afin de prévenir des problèmes de cache dans vos mises à jour.

Les principaux plugins :

Chez Lexik, nous utilisons npAssetsOptimizerPlugin, un plugin de Nicolas Perriault, extrêmement simple à installer et paramétrer. Sur votre environnement de production, vous obtiendrez ainsi un fichier javascript compressé /js/optimized.js?1294759429 qui contient un timestamp permettant à chaque modifications des fichiers d’être prises en compte sans problème de cache.

Nous n’appliquons pas la compression pour les librairies externes telles que jQuery que nous déléguons aux serveurs CDN de Google, cela apporte ces avantages notables :

  • l’avantage du CDN, à savoir de proposer des données statiques via un réseau de multiples serveurs dans de multiples points géographiques, afin de permettre un accès très court ;
  • les navigateurs sont limités sur le nombre de connexions HTTP simultanées sur un même domaine, le téléchargement d’un javascript sur les serveurs de Google se fera en parallèle ;
  • les librairies du CDN ont les bonnes en-têtes de cache, par exemple la version 1.4.4 de jQuery restera en cache pendant 1 an sur le client, cela signifie également qu’un internaute peut bénéficier du cache téléchargé depuis un autre site, pas forcément sur le votre.

3. Accéder à certaines données dynamiques depuis javascript

Il peut parfois être très utile depuis javascript d’accéder à certaines données dynamiques telles que la culture de l’utilisateur, du routing pour des appels Ajax ou encore de l’i18n. Il suffit de s’appuyer sur un simple fichier de configuration via un tableau de données JSON généré depuis une action symfony. J’ai écrit sur mon blog personnel un article détaillé sur le sujet Configurations Javascript dynamiques en Symfony.

4. Javascript depuis les widget

Maintenant que vos inclusions de javascripts se trouvent en bas de page, jQuery par exemple, que faire de nos chers widgets jQuery Autocompleter sfWidgetFormJQueryAutocompleter et autre Datepicker sfWidgetFormJQueryDate qui retournent des champs de formulaire et le javascript d’initialisation du composant ? Eh bien, ne plus les utiliser… Simplement car ils seront inclus avant jQuery et vous aurez donc droit à l’erreur « jQuery is not defined ». Sans être aussi extrême, vous pouvez charger jQuery en haut de page (via les CDN de Google) et votre script compressé en bas de page, les widgets de formulaire seront alors à nouveau fonctionnels.

Cela fait déjà quelques temps que chez Lexik nous avons fait le choix d’enlever tous ces widgets pour les ré-écrire et les adapter à nos besoins. Les javascripts des widgets passent par notre plugin lxJavascript ce qui permet de les inclure là où on le souhaite. Egalement, le javascript retourné par ces widgets correspond à une utilisation basique du composant, dans un cas concrêt il est souvent nécessaire d’en ré-écrire une bonne part. Il peut être pratique par contre que le widget retourne seulement la sémantique HTML adaptée, par exemple dans le cas d’un Autocomplete un champ hidden pour stocker la valeur et un champ texte pour l’autocomplete, aussi nos widgets ont une option pour retourner ou non le code javascript.

5. Optimisation frontend

Presque aussi important que la compression, la configuration d’apache pour mettre en cache certains de vos fichiers statiques. Typiquement, maintenant que vos fichiers javascripts et CSS sont compressés en 1 seul et possèdent un timestamp unique par version, il serait dommage de les recharger à chaque chargement de page… Vous devez donc configurer le module Expires d’apache via le fichier de config du vhost ou via le .htaccess, en voici un exemple :


  ExpiresActive on
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType text/javascript "access plus 1 year"

Vos fichiers javascript et CSS seront ainsi en cache pendant 1 an, attention du coup à ne pas avoir de fichier sans numéro de version, car ils resteraient en cache et vos internautes ne bénéficieraient pas de vos mises à jour…

 

Dernière chose, si ce n’est pas encore fait, installez et testez Yslow pour trouver des pistes d’optimisations de performance de vos sites.

December 20, 04:17 AM

Le but de cet article n’est pas d’expliquer le fonctionnement des événements symfony, la documentation officielle est très bien faite à ce sujet, et pas mal d’articles expliquant leur implementation existent déjà.

Le but est juste de savoir comment faire pour avoir accès à l’event dispatcher depuis les classes du modèle, de manière à pouvoir lever des événements « métiers » qui permettent des traitements qui se rapprochent des trigger sql. L’avantage est de rester au sein de l’application symfony et ne pas disperser la logique. C’est une problèmatique que nous avons rencontré à plusieurs reprises dans les projets et nous avons pu trouver cette implementation grace à n1k0 de chez Akei.

(Pour la suite, je vais me servir d’une partie du modèle de Jobeet comme je l’avais fait dans mon post précédent)

A la base, l’event dispatcher se trouve dans le contexte, et donc accessible principalement depuis les actions des modules. L’idée est de le rendre accessible aux classes du modèle doctrine de manière à pouvoir notifier des événements « métier » directement depuis les méthodes. On va donc passer l’event dispatcher à la classe de base dont hérite toutes les classes du modèle. Ou plus exactement, on va rajouter une classe custom dans la pile d’héritage des classes du modèle.

On créé donc une classe CustomDoctrineRecord, que je place dans /lib/model/doctrine/record/CustomDoctrineRecord.class.php pour éviter qu’elle se retrouve noyée avec les classes du modèle. On va faire passer à cette classe l’event dispatcher par des méthodes statiques.
Les classes de base du modèle héritent de sfDoctrineRecord, il faut donc impérativement que la classe custom hérite elle aussi de sfDoctrineRecord.

Fichier : /lib/model/doctrine/record/CustomDoctrineRecord.class.php

<?php
 
abstract class CustomDoctrineRecord extends sfDoctrineRecord
{
  static protected $dispatcher = null;
 
  /**
   * Sets the EventDispatcher
   *
   * @param sfEventDispatcher $dispatcher 
   */
  static public function setEventDispatcher(sfEventDispatcher $dispatcher)
  {
    self::$dispatcher = $dispatcher;
  }
 
  /**
   * Returns the EventDispatcher
   *
   * @return sfEventDispatcher
   */
  static public function getEventDispatcher()
  {
    return self::$dispatcher;
  }
 
  /**
   * Returns if EventDispatcher is available
   *
   * @return boolean
   */
  static public function hasDispatcher()
  {
    return (self::getEventDispatcher() instanceOf sfEventDispatcher);
  }
}

Maintenant il faut spécifier à doctrine d’utiliser notre classe custom comme classe de référence des classes modèles, on utilise la méthode configureDoctrine() de sfProjectConfiguration. Aprés quoi on va avoir besoin de passer l’event dispatcher à notre classe. On va donc catcher l’événement « context.load_factories » qui est déclenché une fois que le contexte est initialisé pour aller initialiser la classe CustomDoctrineRecord.

Fichier : /config/ProjectConfiguration.class.php

<?php
 
require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();
 
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    parent::setup();
 
    $this->enablePlugins(array(
      'sfDoctrinePlugin',
    ));
 
    $this->dispatcher->connect('context.load_factories', array($this, 'listenToContextFactoriesLoaded'));
  }
 
  /**
   * Defines the base objects' classe
   */
  public function configureDoctrine(Doctrine_Manager $manager)
  {
    sfConfig::set('doctrine_model_builder_options', array(
      'baseClassName' => 'CustomDoctrineRecord'
    ));
  }
 
  /**
   * Objects can acces the EventDispatcher
   */
  public function listenToContextFactoriesLoaded(sfEvent $event)
  {
    CustomDoctrineRecord::setEventDispatcher($event->getSubject()->getEventDispatcher());
  }
}

Aprés ca, il nous reste à faire un petit

./symfony doctrine:build --all-classes

Et on peut vérifier l’inpact en ouvrant une classe de base, et on trouve bien que la classe hérite de notre classe CustomDoctrineRecord

Fichier : /lib/model/doctrine/base/BaseJobeetJob.class.php

<?php
...
abstract class BaseJobeetJob extends CustomDoctrineRecord
{
...

Il est donc maintenant possible de lever des événements depuis les classes du modèle. Pour exemple je vais lever un événement à l’insertion d’un nouveau Job. Cet événement pourra être utilisé par la suite pour faire ce que l’on veut, comme par exemple envoyer une email au modèrateur l’informant qu’un nouveau job a été saisi sur le site.

Fichier : /lib/model/doctrine/JobeetJob.class.php

<?php
 
class JobeetJob extends BaseJobeetJob
{
  /**
   * @param Doctrine_Event $event 
   */
  public function postInsert($event)
  {
    if (self::hasDispatcher())
    {
      if (self::getEventDispatcher()->hasListeners('JobeetJob.Inserted'))
      {
        self::getEventDispatcher()->notify(new sfEvent($this, 'JobeetJob.Inserted'));
      }
    }
  }
}

C’est un exemple tout simple, mais les possibilités sont immenses. Attention aux événements en cascades, ca peut très vite devenir un casse tête à debugger !

Dernier petit tips, pour pouvoir faire des tests de vos méthodes qui lèvent des événements : c’est à mis chemin entre les tests unitaires et les tests fonctionnels. J’ai voulu tester des enchainements de méthodes qui levaient des événements, et je voulais vérifier que les règles de gestion étaient respectées. Je suis donc parti du bootstap unitaire où j’ai initialisé un contexte que je passe à ma classe de base, de manière à ce que les événements soient diffusés.

Fichier : /test/bootstrap/unit_event.php

<?php
 
include(dirname(__FILE__).'/unit.php');
 
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);
$context = sfContext::createInstance($configuration);
CustomDoctrineRecord::setEventDispatcher($configuration->getEventDispatcher());

Quand on commence à utiliser les événements, il y a deux réactions possibles. Il y a les réfractaires qui disent « c’est null, ca sert a rien », soit on devient accro et on commence à en mettre de partout. Si vous faites parti du deuxième groupe, voici quelques conseils élémentaires mais important à respecter pour éviter que le debuggage se transforme en calvaire :

  • mettre des noms d’événements explicites ;
  • centraliser les écouteurs.

Bon codage à tous, et bonnes fêtes

October 27, 03:54 AM

L’idée est de ne plus accéder au backend de notre site web en utilisant backend.php dans l’url, mais de passer par un sous domaine.

  • http://www.super-website.com → frontend
  • http://admin.super-website.com → backend

Supposons que mon projet Symfony se trouve dans /home/public_html/.
Commençons par ajouter un vhost pour définir le sous domaine dans Apache. On précise au sous domaine que le fichier index pour ce sous domaine sera backend.php à l’aide de la directive DirectoryIndex.

?View Code CONSOLE
<VirtualHost *:80>
  ServerName admin.super-website.fr
  DocumentRoot "/home/public_html/web"
  DirectoryIndex backend.php
 
  <Directory "/home/public_html/web">
    AllowOverride All
    Allow from All
  </Directory>
 
  Alias /sf /home/public_html/lib/vendor/symfony/data/web/sf
  <Directory "/home/public_html/lib/vendor/symfony/data/web/sf">
    AllowOverride All
    Allow from All
  </Directory>
</VirtualHost>

Ensuite il faut modifier le .htaccess de Symfony afin qu’il redirige les requêtes http du sous domaine vers le fichier backend.php.

?View Code CONSOLE
Options +FollowSymLinks +ExecCGI
 
<IfModule mod_rewrite.c>
  RewriteEngine On
 
  # uncomment the following line, if you are having trouble
  # getting no_script_name to work
  #RewriteBase /
 
  # we skip all files with .something
  #RewriteCond %{REQUEST_URI} \..+$
  #RewriteCond %{REQUEST_URI} !\.html$
  #RewriteRule .* - [L]
 
  # we check if the .html version is here (caching)
  RewriteRule ^$ index.html [QSA]
  RewriteRule ^([^.]+)$ $1.html [QSA]
 
  # redirect to the backend web controller
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{HTTP_HOST}  ^admin.*
  RewriteRule ^(.*)$ backend.php [QSA,L]
 
  # no, so we redirect to our front web controller
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

Au niveau de la config Symfony, nous pouvons maintenant masquer le nom du script dans les urls du backend:

?View Code CONSOLE
# apps/backend/config/settings.yml
prod:
  .settings:
    no_script_name:    true
    ...

Et pour finir ne pas oublier de vider le cache =), le backend est maintenant accessible sur http://admin.super-website.com.

August 09, 04:28 AM

Symfony propose des outils très puissants pour faciliter le développement d’applications et surtout la génération des modules grâce à l’Admin Generator et le CRUD. Chacunes de ces solutions à ses avantages et ses inconvénients.

L’admin generator permet en une commande d’avoir un module complet et fonctionnel avec de nombreuses fonctionnalités, le tout relativement configurable. En contre partie, l’ajout de fonctionnalités spécifiques et la mise en forme peuvent rapidement s’avérer fastidieuses. Être obligé d’aller fouiller dans le cache pour aller faire des copier/coller afin de pouvoir surcharger une action, c’est comme qui dirait, bien mais pas top…

D’autre part le CRUD permet lui aussi de générer en une seule commade un module, mais ce module est plus que limité et on se retrouve systématiquement à ré-implémenter les mêmes fonctionnalités de base comme le pager, les filters, etc. Par contre travailler avec un CRUD permet de garder la main sur le code, ce qui n’est pas négligeable.

C’est pourquoi je vous propose de voir comment fabriquer un thème pour le CRUD de manière à l’enrichir de quelques fonctionnalités indispensables pour ne pas avoir à les ré-écrire systematiquement et ainsi ganger du temps. Je vais en profiter pour développer ce thème au sein d’un plugin de manière à pouvoir s’en resservir facilement.

Pour se faire je vais partir d’un projet vierge type sandbox auquel je rajoute le plugin sfTaskExtraPlugin qui facilite grandement la génération de plugins.
Pour me simplifier la vie, je vais reprendre le schema.yml et les fixtures (affiliates.yml, category.yml et jobs.yml) de Jobeet pour avoir un modèle sur lequel m’appuyer pour les exemples.

(Attention le fichier de fixture category.yml comporte les traductions, alors que le schema n’a pas le behavior I18n. Il faut au choix modifier le schema.yml pour rajouter le behavior ou modifier les categories pour supprimer les traductions)

Un petit ./symfony doctrine:build –all –and-load et nous voilà parti.

Let’s rock!

Pour fabriquer un module de type CRUD symfony se sert de template de code pour générer les actions et les vues. Ces fichiers sont bien cachés au fin fond du symfony ! On les trouve dans

/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/data/generator/sfDoctrineModule/default

On va donc utiliser le thème par défaut que l’on va enrichir.

Tout d’abord, on va générer l’arborescence du plugin grâce à la commande generate:plugin de sfTaskExtraPlugin.

?View Code CONSOLE
./symfony generate:plugin myCrudThemePlugin

Puis on prépare l’arborescence qui va accueillir les fichiers du template à l’intérieur du plugin.

?View Code CONSOLE
mkdir -p plugins/myCrudThemePlugin/data/generator/sfDoctrineModule/myCrudTheme

Et on y copie tous les fichiers du thème par défaut.

?View Code CONSOLE
cp -r lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/data/generator/sfDoctrineModule/default/* plugins/myCrudThemePlugin/data/generator/sfDoctrineModule/myCrudTheme

Si vous avez fait un checkout de symfony, les fichiers que vous avez copiés contiennent les informations de svn. Il faut nettoyer tout ça. Je vous propose une petite ligne de commande qui permet de faire ca.

?View Code CONSOLE
find plugins/myCrudThemePlugin/data/generator/sfDoctrineModule/myCrudTheme -name .svn -exec rm -rf {} \;

Les fichiers sont prêts. A ce stade, on pourrait déjà générer un thème en utilisant notre thème. Il sufirait d’activer le plugin dans

/config/ProjectConfiguration.class.php
<?php
 
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins(array(
      ...
      'myCrudThemePlugin',
    ));
  }
}

Et de lancer la commande :

?View Code CONSOLE
./symfony doctrine:generate-module --theme="myCrudTheme" frontend job JobeetJob

Mais ne le faites pas ! :p Ca n’aurait pas un grand intérêt étant donné que l’on a rien changé par rapport au thème par défaut.

Nous allons commencer par rajouter un pager sur la page de listing. Il faut modifier l’action dans

/plugins/myCrudThemePlugin/data/generator/sfDoctrineModule/myCrudTheme/parts/indexAction.php
 
  public function executeIndex(sfWebRequest $request)
  {
    $query = Doctrine_Core::getTable('<?php echo $this->getModelClass() ?>')->createQuery('<?php echo strtolower(substr($this->getModelClass(), 0, 1)) ?>');
 
    $this->pager = new sfDoctrinePager('<?php echo $this->getModelClass() ?>', sfConfig::get('app_myCrudThemePlugin_pagination', 20));
    $this->pager->setQuery($query);
    $this->pager->setPage($this->getRequestParameter('page', 1));
    $this->pager->init();
  }

Pour l’affichage du pager, je vous propose de créer un partial que l’on va inclure dans un module du plugin. Etant donné que le fonctionnement est générique, on a pas besoin qu’il soit recopié systèmatiquement dans les modules générés.

On va générer un module appelé shared qui va accueillir le partial du pager et éventuellement d’autres éléments que l’on voudra mutualiser.
La encore sfTaskExtraPlugin nous aide bien car il intégre une commande qui fais ca.

?View Code CONSOLE
./symfony generate:plugin-module myCrudThemePlugin shared

Il nous reste à aller créer notre partial dans

/plugins/myCrudThemePlugin/modules/shared/templates/_pager.php
<?php if ($pager->haveToPaginate()): ?>
  <div class="pagination">
    <a title="First Page" href="<?php echo (isset($object)) ? url_for($route, $object) : url_for($route) ?>?page=<?php echo $pager->getFirstPage() ?>">« First</a>
    <a title="Previous Page" href="<?php echo (isset($object)) ? url_for($route, $object) : url_for($route) ?>?page=<?php echo $pager->getPreviousPage() ?>">« Previous</a>
<?php foreach ($pager->getLinks() as $page): ?>
    <a title="<?php echo $page ?>" class="number<?php echo ($page == $pager->getPage()) ? ' current' : '' ?>" href="<?php echo (isset($object)) ? url_for($route, $object) : url_for($route) ?>?page=<?php echo $page ?>"><?php echo $page ?></a>
<?php endforeach ?>
    <a title="Next Page" href="<?php echo (isset($object)) ? url_for($route, $object) : url_for($route) ?>?page=<?php echo $pager->getNextPage() ?>">Next »</a>
    <a title="Last Page" href="<?php echo (isset($object)) ? url_for($route, $object) : url_for($route) ?>?page=<?php echo $pager->getLastPage() ?>">Last »</a>
  </div>
  <div class="clear"></div>
<?php endif; ?>

Un pager générique qui fonctionne pour les sfDoctrineRoute et les sfRequestRoute. Il prends deux paramètres, route (obligatoire) et object (facultatif).

On modifie la vue indexSuccess.php pour la faire fonctionner avec le pager et ajouter le partial du pager.

Dans

/plugins/myCrudThemePlugin/data/generator/sfDoctrineModule/myCrudTheme/template/templates/indexSuccess.php
<h1><?php echo sfInflector::humanize($this->getPluralName()) ?> List</h1>
 
<table>
  <thead>
    <tr>
<?php foreach ($this->getColumns() as $column): ?>
      <th><?php echo sfInflector::humanize(sfInflector::underscore($column->getPhpName())) ?></th>
<?php endforeach; ?>
    </tr>
  </thead>
  <tbody>
    [?php foreach ($pager->getResults() as $<?php echo $this->getSingularName() ?>): ?]
    <tr>
<?php foreach ($this->getColumns() as $column): ?>
<?php if ($column->isPrimaryKey()): ?>
<?php if (isset($this->params['route_prefix']) && $this->params['route_prefix']): ?>
 
      <td><a href="[?php echo url_for('<?php echo $this->getUrlForAction(isset($this->params['with_show']) && $this->params['with_show'] ? 'show' : 'edit') ?>', $<?php echo $this->getSingularName() ?>) ?]">[?php echo $<?php echo $this->getSingularName() ?>->get<?php echo sfInflector::camelize($column->getPhpName()) ?>() ?]</a></td>
<?php else: ?>
      <td><a href="[?php echo url_for('<?php echo $this->getModuleName() ?>/<?php echo isset($this->params['with_show']) && $this->params['with_show'] ? 'show' : 'edit' ?>?<?php echo $this->getPrimaryKeyUrlParams() ?>) ?]">[?php echo $<?php echo $this->getSingularName() ?>->get<?php echo sfInflector::camelize($column->getPhpName()) ?>() ?]</a></td>
<?php endif; ?>
<?php else: ?>
      <td>[?php echo $<?php echo $this->getSingularName() ?>->get<?php echo sfInflector::camelize($column->getPhpName()) ?>() ?]</td>
<?php endif; ?>
<?php endforeach; ?>
    </tr>
    [?php endforeach; ?]
  </tbody>
</table>
 
[?php include_partial('shared/pager', array('pager' => $pager, 'route' => '<?php echo (isset($this->params['route_prefix']) && $this->params['route_prefix']) ? $this->getUrlForAction('index') : $this->getModuleName().'/index' ?>')) ?]
 
<?php if (isset($this->params['route_prefix']) && $this->params['route_prefix']): ?>
  <a href="[?php echo url_for('<?php echo $this->getUrlForAction('new') ?>') ?]">New</a>
<?php else: ?>
  <a href="[?php echo url_for('<?php echo $this->getModuleName() ?>/new') ?]">New</a>
<?php endif; ?>

On peut d’ores et déjà générer le crud pour tester,

?View Code CONSOLE
./symfony doctrine:generate-module --theme="myCrudTheme" frontend job JobeetJob

Et vous voilà libéré de la tâche rébarbative des pagers ! :p

Ca faisait un petit moment que je voulais traiter ce sujet sur le blog, et je comptais continuer en intégrant les formFilter. Malheureusement, je manque de temps pour aller plus loin.
Mais vous avez désormais en votre possession les éléments pour pouvoir faire votre propre thème et l’enrichir à votre guise.

J’espére que ca vous aura été utile et si j’en ai le temps je ferais un prochain post pour voir cette intégration des formFilter

abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz