Archives de catégorie : Symfony

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 MopaBoostrapBundle – Les problèmes fréquents

Ce bundle est parfait pour se lancer dans la réalisation d’une application basée sur le Twitter Boostrap. En revanche, c’est à chaque fois une vrai galère pour l’installer sur un projet sf2 existant.
Si vous partez sur une application toute neuve, je vous suggère de jeter un oeil à l’édition Symfony Boostrap hébergée sur ce repo github : https://github.com/phiamo/symfony-bootstrap. Elle intègre nativement les bundles suivants pré-configurés :

  • Symfony2
  • bootstrap
  • MopaBootstrapBundle
  • MopaBootstrapSandboxBundle

Si vous souhaitez intégrer ce même bundle avec vos petites mains, vous risquez de rencontrer quelques problèmes facilement solvables. Je ne vais pas refaire la documentation du projet qui est tout de même assez complète. Vous la trouverez à cette adresse : https://github.com/phiamo/MopaBootstrapBundle/blob/master/Resources/doc/index.md

Le fail le plus courant, c’est la compilation des fichiers less. Première chose à faire, vérifier qu’une version récente de nodejs est installée.
Sinon, sur Ubuntu :

sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs npm
sudo npm install -g less

Plus d’infos à ce sujet ici : http://blog.overnetcity.com/19-05-2011/premiers-pas-avec-node-js-installation-de-node-et-express/

Une fois node, npm et less installés, il faut indiquer au MopaBoostrapBundle où se situent les binaires de node et du compilateur less.

Rajoutez dans votre fichier de config les lignes suivantes :

node: /usr/bin/node
node_paths: [/opt/lessc/lib, /usr/lib/node_modules]

Si le chargement de votre fichier css échoue et que vous vous choppez une stacktrace de ce type, c’est vraisemblablement que les chemins indiqués vers node ou lessc ne sont pas les bons :

Error Output:

module.js:340
throw err;
^
Error: Cannot find module less
at Function.Module._resolveFilename (module.js:338:15)
at Function.Module._load (module.js:280:25)
at Module.require (module.js:362:17)
at require (module.js:378:17)
at Object.<anonymous> (/tmp/assetic_lessnn6vMt:1:74)
at Module._compile (module.js:449:26)
at Object.Module._extensions..js (module.js:467:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.runMain (module.js:492:10)

Faites un petit whereis lessc et whereis node pour trouver les chemins qui vont bien !

Autre problème fréquent au moment de la compilation :

Error Output:
[ERROR] /path/to/project/src/YourProject/MainBundle/Resources/public/less/../img/glyphicons-halflings.png (No such file or directory)

Pour le coup, une petite recherche Google remonte pas mal de résultats à ce sujet. C’est un problème avec cssembed qui fonctionne mal avec les chemins relatifs. Il suffit en fait de recréer l’arborescence attendue par le boostrap twitter, soit en copiant le sprite vers le dossier img/, juste au dessus de votre fichier less principal, soit en utilisant un lien symbolique.
Plus d’infos ici : https://github.com/phiamo/MopaBootstrapBundle/blob/master/Resources/doc/assetic-configuration.md

Voilà c’est tout pour le moment. Je tâcherai de mettre à jour cet article en fonction des nouveaux points bloquants que je rencontrerai éventuellement.

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);
        }
    }
}