Symfony – Behavioral design pattern

Lorsque l’on développe une application avec Symfony, on implémente divers design pattern sans forcément trop sans rendre compte. En effet, le framework intègre directement un nombre important de bonnes pratiques.

Behavioral Design Pattern

Il existe un design pattern très intéressant, BEHAVIORAL, qui permet de définir des comportements sur des objets métiers par exemple. On peut ainsi définir un objet comme étant loggable, blameable ou bien encore timestampable.

Les traits ou l’héritage multiple en PHP

En PHP, ajouter un comportement à un objet n’est pas forcément chose aisée car l’héritage multiple n’existe pas. Heureusement, à partir de PHP 5.4, les « traits » permettent de simuler une forme d’héritage multiple en ajoutant à une classe des propriétés et des méthodes définies dans un « trait ».

<?php

namespace Knp\DoctrineBehaviors\Model\Timestampable;

trait Timestampable
{
    /**
     * @var \DateTime $createdAt
     *
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $createdAt;

    /**
     * @var \DateTime $updatedAt
     *
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $updatedAt;

    /**
     * Returns createdAt value.
     *
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * Returns updatedAt value.
     *
     * @return \DateTime
     */
    public function getUpdatedAt()
    {
        return $this->updatedAt;
    }

    /**
     * @param \DateTime $createdAt
     * @return $this
     */
    public function setCreatedAt(\DateTime $createdAt)
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * @param \DateTime $updatedAt
     * @return $this
     */
    public function setUpdatedAt(\DateTime $updatedAt)
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    /**
     * Updates createdAt and updatedAt timestamps.
     */
    public function updateTimestamps()
    {
        if (null === $this->createdAt) {
            $this->createdAt = new \DateTime('now');
        }

        $this->updatedAt = new \DateTime('now');
    }
}

L’objet de l’article n’étant pas de détailler le fonctionnement des « traits » en PHP, je vous laisse parcourir la documentation à ce sujet. http://www.php.net/manual/fr/language.oop5.traits.php

Doctrine Behaviors

Avec Symfony et l’aide d’un bundle bien pratique, DoctrineBehaviors de KnpLabs, il devient très aisé d’implémenter ce design pattern en un temps record. De plus, si vous avez l’habitude d’utiliser les annotations pour votre mapping ORM/ODM, vous n’aurez rien d’autre à faire que d’utiliser les « traits » PHP offerts par le bundle pour ajouter des comportements à vos objets métiers. Le gestion (CRUD) de ces comportements en base de données se fera alors de manière automatique via les listeners Symfony/Doctrine.

<?php

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;

/**
 * @ORM\Entity(repositoryClass="CategoryRepository")
 */
class Category implements ORMBehaviors\Tree\NodeInterface, \ArrayAccess
{
    use ORMBehaviors\Tree\Node,
        ORMBehaviors\Translatable\Translatable,
        ORMBehaviors\Timestampable\Timestampable,
        ORMBehaviors\SoftDeletable\SoftDeletable,
        ORMBehaviors\Blameable\Blameable,
        ORMBehaviors\Geocodable\Geocodable,
        ORMBehaviors\Loggable\Loggable,
        ORMBehaviors\Sluggable\Sluggable
    ;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="NONE")
     */
    protected $id;
}

Visitez vite le github du bundle : https://github.com/KnpLabs/DoctrineBehaviors

Symfony – Best Bundles

En tant que développeur Symfony, j’essaye lorsque c’est possible, de ne jamais réinventer la roue. Cela tombe bien car Symfony et sa vaste communauté, offrent de nombreux Bundles très bien faits répondant aux besoins les plus courants notamment en terme d’implémentation de bonnes pratiques.

Devenez un expert en utilisant les « Best Bundles » Symfony

The FOSUserBundle adds support for a database-backed user system in Symfony2. It provides a flexible framework for user management that aims to handle common tasks such as user registration and password retrieval.

This bundle provides various tools to rapidly develop RESTful API’s & applications with Symfony2.

Elastica and ElasticSearch integration in Symfony2

This Bundle provides a Symfony2 authentication provider so that users can login to a Symfony2 application via Facebook. Furthermore via custom user provider support the Facebook login can also be integrated with other data sources like the database based solution provided by FOSUserBundle.

JMSDiExtraBundle adds more powerful dependency injection features to Symfony2

This bundle allows you to create i18n routes.

This bundle adds AOP capabilities to Symfony2.

If you haven’t heard of AOP yet, it basically allows you to separate a cross-cutting concern (for example, security checks) into a dedicated class, and not having to repeat that code in all places where it is needed.

In other words, this allows you to execute custom code before, and after the invocation of certain methods in your service layer, or your controllers. You can also choose to skip the invocation of the original method, or throw exceptions.

XHProf is a hierarchical profiler for PHP. It reports function-level call counts and inclusive and exclusive metrics such as wall (elapsed) time, CPU time and memory usage. A function’s profile can be broken down by callers or callees. The raw data collection component is implemented in C as a PHP Zend extension called xhprof. XHProf has a simple HTML based user interface (written in PHP). The browser based UI for viewing profiler results makes it easy to view results or to share results with peers. A callgraph image view is also supported.

This bundle is a fork of AvalancheImagineBundle which provides easy image manipulation support for Symfony2. The goal of the fork is to make the code more extensible and as a result applicable for more use cases.

This php 5.4+ library is a collection of traits that add behaviors to Doctrine2 entites and repositories.

This bundle integrates the Doctrine2 Migrations library. into Symfony so that you can safely and quickly manage database migrations.

Provide markdown conversion (based on Michel Fortin work) to your Symfony2 projects.

Alice allows you to create a ton of fixtures/fake data for use while developing or testing your project. It gives you a few essential tools to make it very easy to generate complex data with constraints in a readable and easy to edit way, so that everyone on your team can tweak the fixtures if needed.

It adds in your WebProfiler extra sections (routing, container, twig,…)

This bundle allows you to expose your routing in your JavaScript code. That means you’ll be able to generate URL with given parameters like you can do with the Router component provided in the Symfony2 core.

Pour finir…

…la liste n’est bien sûr pas exhaustive, retenez que vous trouverez souvent des bundles existant répondant à vos besoins. Attention cependant, car certains sont de meilleure qualité que d’autres, certains sont bien maintenus d’autres pas, enfin plus grave, certains ne fonctionnent pas ou contiennent de nombreux bugs. Alors attention !

Quelques liens utiles

Symfony2 + FOSUserBundle – Comment étendre la commande par défaut de création d’un utilisateur

La gestion des utilisateurs dans un projet symfony2 est une tâche répétitive qu’on confie la plupart du temps à un bundle qui gère parfaitement bien la chose : FOSUserBundle.
Le bundle fournit une couche d’abstraction via les UserManager et GroupManager qui permet au développeur de stocker ses utilisateurs via Doctrine ORM, MongoDB / CouchDB ODM ou encore via Propel.
Il fournit également tout un lot de commandes bien utiles, et notamment une qui permet d’ajouter en CLI un utilisateur en base en lui définissant ses attributs obligatoires. On peut éventuellement lui ajouter ou révoquer un ou plusieurs rôles, également via des commandes dédiées.

Une entité User de base est définie dans le bundle, entité que le développeur s’empresse en général de modifier pour lui rajouter les attributs nécessaires à son application.
Les champs email et username sont déjà obligatoires, on souhaite souvent rajouter par exemple :

  • Firstname
  • Lastname

On modifie alors notre entité User et on se retrouve à quelque chose du genre :

<?php

namespace Acme\AcmeUserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Entity\User as BaseUser;

/**
 * @ORM\Table(name="users")
 */
class User extends BaseUser
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected  $id;

    /**
     * @var string $last_name
     *
     * @ORM\Column(name="last_name", type="string", length=255)
     */
    private $last_name;

    /**
     * @var string $first_name
     *
     * @ORM\Column(name="first_name", type="string", length=255)
     */
    private $first_name;
    
    // ...
}
&#91;/sourcecode&#93;

Dans mon exemple, les attributs <strong>firstname</strong> et <strong>lastname</strong> sont mappés sur des champs not nullable.
Et c'est justement le fait que ces champs soient requis qui va nous poser problème lors de l'exécution de la commande de création d'un nouvel utilisateur.

<code>> php app/console fos:user:create
Please choose a username:test
Please choose an email:test@test.fr
Please choose a password:testtest
</code>

Et on se choppe, c'est normal, une grosse exception :

<code>[Doctrine\DBAL\DBALException]
[PDOException] 
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'last_name' cannot be null
</code>

La solution est donc de surcharger la commande du bundle pour y ajouter les contraintes propres à notre application, à savoir, la saisie du prénom et du nom de famille de l'utilisateur.
La commande <strong>CreateUser</strong> se trouve dans <code>FOS\UserBundle\Command\CreateUserCommand</code>.
On commence donc par créer la notre, dans notre propre UserBundle en la faisant étendre de celle du FOSUserBundle :


<?php

namespace Acme\AcmeUserBundle\Command;

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use FOS\UserBundle\Command\CreateUserCommand as BaseCommand;

class CreateUserCommand extends BaseCommand
{
    // ...
}
&#91;/sourcecode&#93;

On souhaite rajouter deux nouveaux arguments obligatoires dans le configure et on le fait de la manière suivante :

&#91;sourcecode language="php"&#93;
<?php

// ...
class CreateUserCommand extends BaseCommand
{
    /**
     * @see Command
     */
    protected function configure()
    {
        parent::configure();
        $this
            ->setName('acme:user:create')
            ->getDefinition()->addArguments(array(
                new InputArgument('firstname', InputArgument::REQUIRED, 'The firstname'),
                new InputArgument('lastname', InputArgument::REQUIRED, 'The lastname')
            ))
        ;
        $this->setHelp(<<<EOT
// L'aide qui va bien
EOT
            );
    }

    // ...

    protected function interact(InputInterface $input, OutputInterface $output)
    {
        parent::interact($input, $output);
        if (!$input->getArgument('firstname')) {
            $firstname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a firstname:',
                function($firstname) {
                    if (empty($firstname)) {
                        throw new \Exception('Firstname can not be empty');
                    }

                    return $firstname;
                }
            );
            $input->setArgument('firstname', $firstname);
        }
        if (!$input->getArgument('lastname')) {
            $lastname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a lastname:',
                function($lastname) {
                    if (empty($lastname)) {
                        throw new \Exception('Lastname can not be empty');
                    }

                    return $lastname;
                }
            );
            $input->setArgument('lastname', $lastname);
        }
    }
    // ...
}

Et enfin, on surcharge la méthode execute. FOSUserBundle utiliser la classe UserManipulator via le service user_manipulator.
Libre à vous de créer votre propre UserManipulator ou de placer votre code de création directement au sein de la méthode, en passant bien par le UserManager, comme ceci :

<?php
    // ...

    /**
     * @see Command
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        /** @var \FOS\UserBundle\Model\UserManager $user_manager */
        $user_manager = $this->getContainer()->get('fos_user.user_manager');

        /** @var \Acme\AcmeUserBundle\Entity\User $user */
        $user = $user_manager->createUser();
        $user->setUsername($input->getArgument('username'));
        $user->setEmail($input->getArgument('email'));
        $user->setPlainPassword($input->getArgument('password'));
        $user->setEnabled(!$input->getOption('inactive'));
        $user->setSuperAdmin((bool)$input->getOption('super-admin'));
        $user->setFirstName($input->getArgument('firstname'));
        $user->setLastName($input->getArgument('lastname'));

        $user_manager->updateUser($user);

        $output->writeln(sprintf('Created user <comment>%s</comment>', $username));
    }
    // ...
}

Ci-dessous, la commande complète, à adapter selon vos besoins :

<?php

namespace Acme\AcmeUserBundle\Command;

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use FOS\UserBundle\Model\User;
use FOS\UserBundle\Command\CreateUserCommand as BaseCommand;

class CreateUserCommand extends BaseCommand
{
    /**
     * @see Command
     */
    protected function configure()
    {
        parent::configure();
        $this
            ->setName('acme:user:create')
            ->getDefinition()->addArguments(array(
                new InputArgument('firstname', InputArgument::REQUIRED, 'The firstname'),
                new InputArgument('lastname', InputArgument::REQUIRED, 'The lastname')
            ))
        ;
        $this->setHelp(<<<EOT
// L'aide qui va bien
EOT
            );
    }

    /**
     * @see Command
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $username   = $input->getArgument('username');
        $email      = $input->getArgument('email');
        $password   = $input->getArgument('password');
        $firstname  = $input->getArgument('firstname');
        $lastname   = $input->getArgument('lastname');
        $inactive   = $input->getOption('inactive');
        $superadmin = $input->getOption('super-admin');

        /** @var \FOS\UserBundle\Model\UserManager $user_manager */
        $user_manager = $this->getContainer()->get('fos_user.user_manager');

        /** @var \Acme\AcmeUserBundle\Entity\User $user */
        $user = $user_manager->createUser();
        $user->setUsername($username);
        $user->setEmail($email);
        $user->setPlainPassword($password);
        $user->setEnabled((Boolean) !$inactive);
        $user->setSuperAdmin((Boolean) $superadmin);
        $user->setFirstName($firstname);
        $user->setLastName($lastname);

        $user_manager->updateUser($user);

        $output->writeln(sprintf('Created user <comment>%s</comment>', $username));
    }

    /**
     * @see Command
     */
    protected function interact(InputInterface $input, OutputInterface $output)
    {
        parent::interact($input, $output);
        if (!$input->getArgument('firstname')) {
            $firstname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a firstname:',
                function($firstname) {
                    if (empty($firstname)) {
                        throw new \Exception('Firstname can not be empty');
                    }

                    return $firstname;
                }
            );
            $input->setArgument('firstname', $firstname);
        }
        if (!$input->getArgument('lastname')) {
            $lastname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a lastname:',
                function($lastname) {
                    if (empty($lastname)) {
                        throw new \Exception('Lastname can not be empty');
                    }

                    return $lastname;
                }
            );
            $input->setArgument('lastname', $lastname);
        }
    }
}

Les ressources utiles du mois de Juillet

C’est une pratique assez récurrente chez les développeurs Web ou chez les web designers qui bloguent. Alors moi aussi je vais régulièrement (chaque mois) poster un article regroupant les ressources utiles sur lesquelles je suis tombé. Deux intérêts à cette démarche :

  • D’une part pouvoir les retrouver le jour où j’en aurai besoin / re-besoin
  • D’autre part, les partager avec vous amis lecteurs

Je crée donc pour l’occasion une nouvelle catégorie d’articles sur OvernetCity intitulée sobrement : « Ressources Utiles ».

Pour ce mois de Juillet, un peu de jQuery, un peu de symfony et un peu de développement iPhone sont au programme. D’ailleurs, cela risque d’être souvent le cas…

Interactive Photo Desk with jQuery and CSS3

Les démonstrations concernant CSS 3 et HTML 5 ont le vent en poupe depuis quelques mois. Il ne se passe pas une journée sans qu’un tuto complet soit posté sur un des nombreux blogs que je suis, montrant à quel point l’arrivée de ces deux langages et leur support par l’ensemble des navigateurs modernes va changer notre façon d’interagir avec une application Web.

Ici, c’est l’alliance du CSS 3 et de jQuery qui permet de réaliser un chouette bureau sur lequel s’entassent quelques photos. L’utilisateur peut les déplacer avec un résultat d’une fluidité étonnante.

A consulter avec un navigateur moderne évidemment.

http://tympanus.net/codrops/2010/07/01/interactive-photo-desk/

Minimalistic Slideshow Gallery with jQuery

Encore un tuto expliquant comment réaliser une mini gallerie pour afficher quelques photos. Celui-ci a le mérite d’être très clair et d’avoir un rendu vraiment pro.

Très facile à intégrer au sein d’une page existante !

http://tympanus.net/codrops/2010/07/05/minimalistic-slideshow-gallery/

Symfony 1.4 – Tri sur les colonnes étrangères dans l’admin generator

L’admin generator de symfony est un outil merveilleusement pratique qui permet de créer en quelques minutes un backend parfaitement fonctionnel et adapté aux besoins du client.

Néanmoins, je me suis souvent demandé pourquoi il est impossible sans mettre les mains dans le cambouis de faire un tri sur les colonnes persos dans la liste générée par le Framework. Chaque colonne est par défaut cliquable ce qui entraine un tri ascendant ou descendant de la liste en fonction de cette colonne mais ce comportement est indisponible dès que la colonne en question n’est pas une colonne native en base et fait, par exemple, référence à l’attribut name d’un objet récupéré via une relation.

Les développeurs experts symfony de l’agence Elao nous montrent comment modifier le comportement de l’admin generator pour que, justement, ce tri soit possible sur les colonnes non natives. Très utile !

http://www.elao.org/symfony/symfony-1-4-admin-generator-tris-sur-les-colonnes-etrangeres.html

Introduction aux Métadonnées RDFa, Microdata et Microformats

Je connais la technique depuis un moment mais n’ai pas encore eu l’occasion de la mettre en pratique au sein d’un projet. Ces métadonnées à destination des moteurs de recherche permettent de stocker de modifier la présentation d’une page au sein d’un moteur de recherche pour apporter de la valeur ajoutée à l’utilisateur. Les sites référençant des restaurants, par exemple, peuvent profiter de ces métadonnées pour afficher, directement au sein des résultats d’un moteur de recherche, la note associée à un restaurant sous forme d’étoiles.

Ces données sont de plus en plus utilisées et de mieux en mieux exploitées. Elles méritent donc qu’on s’y attarde un tout petit peu de manière à optimiser le référencement naturel d’une application web.

Cet article présente quelques exemples concrets d’utilisation de ces métadonnées et référence quelques liens utiles pour le développeur.

http://spyrestudios.com/real-world-microformats-rdfa-microformats-and-microdata-practical-examples/

Tutoriel complet sur les Microdata de HTML5

Quand on pense à l’HTML 5, on ne pense pas forcément immédiatement à Microdata. Et pourtant c’est également une des grandes nouveauté que ce nouveau langage nous apporte et à laquelle tout bon développeur se doit de s’intéresser. Ce tutoriel très complet permet d’aller plus loin que l’article précédent en se basant sur un exemple concret.

http://net.tutsplus.com/tutorials/html-css-techniques/html5-microdata-welcome-to-the-machine/

Comment intégrer Google Analytics dans son application iPhone en 7 minutes ?

Intégrer un script de tracking des visiteurs dans une application Web est aujourd’hui presque un réflexe pour le développeur. On peut faire la même chose pour une application lourde sur iPhone puisque l’API de Google est compatible avec plusieurs terminaux mobiles dont ce dernier.

Quel est l’intérêt ? Comment est-ce que ça marche ? Quels sont les pré-requis ? Autant de questions qui trouveront leurs réponses dans ce tutoriel très complet de iCodeBlog.

http://icodeblog.com/2010/04/22/how-to-integrate-google-analytics-tracking-into-your-apps-in-7-minutes/

Premiers pas avec iPhone Core Data

Ce tutoriel est une belle introduction au Framework Core Data de l’iPhone OS qui n’est pas très bien couvert par les livres que j’ai pu lire lors de ma formation au développement iPhone.

A parcourir pour comprendre quelles sont les possibilités offertes par ce Framework et surtout comment les mettre en oeuvre.

http://mobile.tutsplus.com/tutorials/iphone/iphone-core-data/

Afficher un flux RSS en temps réel avec XMPP

Comment fait Google avec son GTalk pour afficher les messages envoyés en temps réel sur sa page web, sans sacrifier les ressources serveurs avec des requêtes Ajax envoyée chaque seconde ?

C’est une question que je me suis souvent posé et à laquelle j’ai trouvé une réponse aujourd’hui grâce à un tutoriel très complet posté sur le site d’IBM. GMail, Gtalk, Google Wave font tous usage du langage XMPP dédié à la notification en quasi temps réel pour les applications web. A lire d’urgence si le sujet vous intéresse…

http://www.ibm.com/developerworks/xml/tutorials/x-realtimeXMPPtut/index.html