phpmd logo

Minimiser les risques de bugs dans votre projet Symfony2 avec PHPMD

PHPMD ou PHP Mess Detector, est un analyseur de code PHP pour assurer la qualité de code. L’analyse de coude source permet de détecter plusieurs problèmes potentiels, tel-que :

  • Les bugs potentiels
  • Code non optimisé
  • Expressions trop compliquées (if-else, …)
  • Code non utilisé (variables, méthodes, propriétés)

Installation de PHPMD

PHP Mess Detector est disponible via PEAR :

pear channel-discover pear.phpmd.org
pear channel-discover pear.pdepend.org
pear install --alldeps phpmd/PHP_PMD

Nous pouvons aussi l’installer en récupérant son fichier .phar :

wget -c http://static.phpmd.org/php/latest/phpmd.phar
chmod +x phpmd.phar
mv phpmd.phar /usr/local/bin/phpmd

Utilisation de PHPMD

Afin de détecter ces problèmes, PHP Mess Detector applique certaines règles pour vérifier la qualité de votre code. La liste des règles peut se trouver ici.

L’utilisation de PHP est simple, il suffit de passer la liste des règles que nous désirons les vérifier en tant que paramètre séparé par une virgule ou passer un fichier xml contenant ces règles, le rapport généré peut être sous 3 formats : xml, text ou html.

#Vérifier le dossier src/
phpmd src html /path/to/phpmd.xml > myreport.html
#ou un fichier bien spécifique
phpmd src/AppBundle/Controller/DefaultController.php text codesize,unusedcode,naming

Voici un exemple d’un fichier XML contenant Les règles que je les utilise avec mes projets Symfony2

<?xml version="1.0"?>
<ruleset name="PHPMD ACSEO RULESET" xmlns="http://pmd.sf.net/ruleset/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd" xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"> 
    <description>Règles PHPMD pour Symfony2</description>
    <rule ref="rulesets/codesize.xml/CyclomaticComplexity" />
    <rule ref="rulesets/codesize.xml/NPathComplexity" />
    <rule ref="rulesets/codesize.xml/ExcessiveClassComplexity" /> 
    <rule ref="rulesets/codesize.xml/ExcessiveClassLength" /> 
    <rule ref="rulesets/codesize.xml/ExcessiveMethodLength" /> 
    <rule ref="rulesets/codesize.xml/ExcessiveParameterList" /> 
    <rule ref="rulesets/design.xml/EvalExpression" /> 
    <rule ref="rulesets/design.xml/ExitExpression" /> 
    <rule ref="rulesets/design.xml/GotoStatement" /> 
    <rule ref="rulesets/naming.xml/ConstructorWithNameAsEnclosingClass" /> 
    <rule ref="rulesets/unusedcode.xml/UnusedFormalParameter" /> 
    <rule ref="rulesets/unusedcode.xml/UnusedLocalVariable" /> 
    <rule ref="rulesets/unusedcode.xml/UnusedPrivateField" /> 
    <rule ref="rulesets/unusedcode.xml/UnusedPrivateMethod" /> 
</ruleset>

Le résultat de PHPMD ressemblera à ceci :

src/UserBundle/Controller/RegistrationController.php:227  Avoid unused parameters such as '$form'.
src/UserBundle/Controller/RegistrationController.php:227  Avoid unused parameters such as '$request'.
src/UserBundle/Controller/RegistrationController.php:227  Avoid unused parameters such as '$dispatcher'.
code

Vérifier le respect des normes de codage de votre projet Symfony2 avec PHP Code Sniffe

Le respect des normes de codage est très important, particulièrement en travaillant sur les grands projets avec de nombreux collaborateurs tels que Symfony2. L’impact du non respect sur le coût d’un projet est important, car un code salissant sans cohérence est difficile à lire qui rend sa maintenance et son évolution difficile.

Il y a plusieurs façons d’écriture d’un code uniforme, mais le plus important est que tout le monde impliqué dans le projet applique la même façon. Pour cela les différents projets open source avec un grand nombre de contributeurs tel que Symfony2 propose des conventions de codage à suivre.
La liste détaillée des conventions peut être lu dans la section contribuer de la documentation.

Se rappeler de toute la liste de ces normes est difficile, c’est pour cela que nous faisons appel à un outil tel que PHP Code Sniffe qui permet de détecter les violations de ces conventions.

Installation et Configuration de « PHPCS »

L’installation de PHP_CodeSniffer est simple car il est disponible via PEAR :

pear install PHP_CodeSniffer 

PHPCS est installé avec des standards par défauts, mais ceux de Symfony2 ne font pas partie, nous devons les téléchargés et configurer PHPCS pour les utiliser manuellement.

cd /path/to/pear/PHP/CodeSniffer/Standards
 
git clone git://github.com/escapestudios/Symfony2-coding-standard.git Symfony2

phpcs --config-set default_standard Symfony2

Le ‘/path/to/pear’ peut être récupérer avec la commande :

pear config-show | grep php_dir

Utilisation de « PHPCS »

Pour utiliser PHPCS, il suffit de lancer la commande suivante dans un projet Symfony2 :

phpcs src/

Si vous avez des violations des conventions Symfony2, le résultat ressemblera à ceci :

FILE: .../src/Rocket/FullContactBundle/Service/FullContactManager.php
----------------------------------------------------------------------
FOUND 3 ERRORS AFFECTING 3 LINES
----------------------------------------------------------------------
 31 | ERROR | Doc comment for parameter "$apiKey" missing
 34 | ERROR | Doc comment for parameter $container does not match
    |       | actual variable name $apiKey
 42 | ERROR | Missing function doc comment
----------------------------------------------------------------------
Time: 12.73 secs; Memory: 54.75M

Certaines erreurs et avertissements, peuvent être fixées automatiquement avec l’outil PHPCBF déjà installé avec PHPCS. l’exécution est simple :

phpcbf src/

Utilisation avec Git

Vous aurez remarqué que la vérification du code n’est faite qu’à la demande du développeur et n’est donc pas systématique.
L’idéale sera d’empêcher le développeur de « commiter » son code s’il y a une erreur dans l’un de ses fichiers.
Cette vérification peut être mise en place à travers un hook pre-commit.

Ajoutez ce fichier pre-commit sous le dossier .git/hooks de votre projet Symfony2 et créez aussi un fichier config comme ceci :

# path to phpcs "binary"
PHPCS_BIN=/usr/bin/phpcs

# the coding standard, you can also specify a path to your own standard here 
# e. g. /path/to/my/standard/dir/
PHPCS_CODING_STANDARD=Symfony2

# comma-separated list of file patterns being ignored
PHPCS_IGNORE=

# egrep compatible pattern of  files to be checked
PHPCS_FILE_PATTERN="\.(php|phtml)$"

# ignore warnings
PHPCS_IGNORE_WARNINGS=1

# encoding
PHPCS_ENCODING=utf-8

Et enfin autoriser l’exécution de fichier :

chmod a+x .git/hooks/pre-commit

Maintenant, avant chaque nouvelle commit PHPCS sera lancé, en cas de violations la commit sera bloquée.

bower-logo

Utiliser Bower Avec Symfony 2

Bower est un outil de gestion des dépendances front-end, c’est un composer mais pour les js et css.

Comme composer, Bower fonctionne en téléchargeant les libs à partir de github, ces libs sont indexées dans un catalogue récupérable à partir d’ici.

Installation de Bower

Bower est un package Node.js donc pour l’installer on aura besoin de node.js et aussi de git.

$ npm install -g bower

Après l’installation, nous devons créer le fichier bower.json, c’est comme composer.json mais il contiendra nos dépendances Front-end.

Nous pouvons créer le fichier bower.json interactivement avec la commande :

$ bower init

À la fin nous allons obtenir un fichier bower.json semblable à celui-ci :

{
  "name": "Bower Symfony Inegration",
  "version": "0.0.1",
  "authors": [
    "Mohammed Rhamnia"
  ],
  "description": "Utiliser Bower Avec Symfony 2",
  "keywords": [
    "Symfony",
    "Bower"
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ]
}

Le fichier bower.json doit être à la racine de votre projet dans le même emplacement que composer.json.

Avec Symfony, les assets sont installés dans le dossier « Resources/public » de votre bundle. Donc, nous allons indiqué à Bower l’emplacement exact où il doit télécharger nos dépendances. Ceci est effectué à travers le fichier .bowerrc placé aussi à la racine de votre projet, son contenu sera similaire à celui ci :

{
    "directory": "src/AppBundle/Resources/public/bower_components"
}

Supposons que notre projet aura besoin de :

  • La version 2.0 de jQuery
  • La dernière version de Twitter Bootstrap
  • La dernière version de Less

L’installation se fait via l’option install, nous pouvons indiquer les libs à installer après l’option ou nous pouvons mettre à jour le fichier bower.json pour indiquer les dépendances de notre projet :

    "dependencies": {
        "jquery": "2.0.0",
        "bootstrap": "latest",
        "less.js": "latest""
    }

et puis

$ bower install

ou directement :

$ bower install bootstrap less jquery#2.0.0

le « #2.0.0 » indique le tag 2.0.0 du repository jquery sur github.com.

Voila, nous arrivons à la fin de l’intégration de bower dans Symfony, il nous reste qu’ajouter les assets téléchargés dans nos twigs, voici un exemple :

{% javascripts
            '@AppBundle/Resources/public/bower_components/jquery/jquery.js'
            '@AppBundle/Resources/public/bower_components/less.js/dist/less.js'
            %}
            <script src="{{ asset_url }}"></script>
 {% endjavascripts %}

Symfony2: Comment créer une Contrainte de Validation Doctrine Personnalisée

Une des tâches les plus courantes que vous devez effectuer lors de la validation d’un formulaire est de vérifier les données d’un champ dans votre base de données comme le cas de vérification de l’unicité personnalisée.

Dans ce Tutorial, le but de notre contrainte est de vérifier qu’un code postal a été utilisé avec une autre entité.

La première étape est la création de la classe de la contrainte :

// src/Nm/CoreBundle/Validator/ZipCodeExist.php

namespace Nm\CoreBundle\Validator;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */

class ZipCodeExist extends Constraint
{

    public $message = "Le code '%value%' n'est pas utilisé";
    public $entity;
    public $property;

    public function validatedBy()
    {
        return 'validator.zip.exist';
    }

    public function requiredOptions()
    {
        return array('entity', ' property');
    }

    public function targets()
    {
        return self::PROPERTY_CONSTRAINT;
    }
}

Comme vous pouvez voir, la déclaration de la classe est simple. Nous avons défini le message d’erreur et deux autres options nécessaires l’Entité et sa propriété. La méthode validatedBy() retourne le nom du service que nous allons déclarer juste après qui se chargera de valider la contrainte. Enfin, la fonction des targets() indique que la contrainte sera appliqué seulement sur les propriétés.

// src/Nm/CoreBundle/Validator/ZipCodeExistValidator.php

namespace Nm\CoreBundle\Validator;

use Doctrine\ORM\EntityManager;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class ZipCodeExistValidator extends ConstraintValidator
{

    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function validate($value, Constraint $constraint)
    {
        $entities = $this->entityManager->getRepository("CoreBundle:Restaurant")
                ->findByZipCode($value);
        
        if (empty($entities)) {
            
            $this->context->addViolation($constraint->message, array('%value%' => $value));

            return false;
        }

        return true;
    }

}

La méthode validate() vérifie la validité de la contrainte et renvoie par conséquent TRUE ou FALSE et ajoute le message d’erreur.

Ici, notre contrainte fonctionne contrairement à celle de symfony @UniqueEntity, dans notre cas la valeur est valide seulement lorsqu’elle est déjà utilisée dans l’entité Restaurant.

ZipCodeExistValidator a une dépendance c’est Doctrine EntityManager. Nous aurons besoin alors de le déclarer en tant que service :

<!-- src/Nm/CoreBundle/Resources/config/services.xml -->
    <parameters>
        <parameter key="nm.validator.zip.exist.class">Nm\CoreBundle\Validator\ZipCodeExistValidator</parameter>
    </parameters>
    <services>
        <service id="nm.validator.zip.exist" class="%nm.validator.zip.exist.class%">
            <argument type="service" id="doctrine.orm.entity_manager" />
            <tag name="validator.constraint_validator" alias="validator.zip.exist" />
        </service>
    </services>

La dernière étape est l’utilisation de notre validateur personnalisé, c’est très facile, tout comme ceux fournis par Symfony2 lui-même :

// src/Nm/CoreBundle/Entity/Address.php

use Symfony\Component\Validator\Constraints as Assert;
use Nm\CoreBundle\Validator as NmAssert;

class Address
{
    // ...


    /**
     * @var string
     * @Assert\Length(max="5", min="5", minMessage="Code postal invalide", maxMessage="Code postal invalide")
     * @Assert\Regex("/[0-9]{2}[0-9]{3}/", message="Code postal invalide")
     * 
     * @NmAssert\ZipCodeExist(message="Aucun de nos restaurants ne peux livré à cette adresse")
     * 
     * @ORM\Column(name="zip_code", length=5)
     */
    private $zipCode

    // ...
}

Symfony2: Connexion automatique avec FosUserBundle

En général, la connexion avec FosUserBundle se fait à travers un formulaire, mais récemment j’avais besoin d’une tâche pas assez récurrente, dans ce cas l’utilisateur peut se connecter automatiquement en cliquant sur un lien envoyé par email.

Tout d’abord, j’ai ajouté à ma entité User un attribut qui va jouer le rôle d’un jeton de connexion, ce jeton est généré automatiquement lors de la persiste de l’objet User via les events PrePersist et PreUpdate :


use FOS\UserBundle\Model\User as BaseUser;

/**
 * User
 *
 * @ORM\Table(name="users")
 * @ORM\Entity(repositoryClass="Nm\UserBundle\Repository\UserRepository")
 * @ORM\HasLifecycleCallbacks
 */
class User extends BaseUser
{
     // ...

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

    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null === $this->loginToken) {
            $this->loginToken = sha1(uniqid(mt_rand(), true));
        }
    }
}

Après, j’ai créé l’action de la connexion automatique, sa route prendra en paramètre le loginToken :


use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

//...

    /**
     * @Route("/auto-login/{loginToken}", name="auto_login")
     */
    public function autoLogin(Request $request, User $user)
    {
         $firewallName = $this->container->getParameter('fos_user.firewall_name');
        
        $token = new UsernamePasswordToken($user, $user->getPassword(), $firewallName, $user->getRoles());
        $this->get('security.context')->setToken($token);
        $request->getSession()->set('_security_main', serialize($token));
        $url = $this->generateUrl('fos_user_registration_confirmed');
        $response = new RedirectResponse($url);
                    
        return $response;
    }

[Symfony2] Réderiction aprés téléchargment d’un fichier

Il s’agit d’une petite astuce pour rediriger l’utilisateur après le téléchargement d’un fichier, on ajoute l’option « refresh » aux headers, voici un exemple avec Symfony2 :


//Fonction perso pour récupérer le contenue d'un fichier.
$content = $this->getFileContent($file);

$response = new Response();

$response->setContent($content);

$response->headers->set('Content-Type', "application/force-download");
$response->headers->set('Content-disposition', "filename=$file");
$response->headers->set('refresh', "1;".$this->generateUrl('homepage'));

return $response;

[Symfony2][DoctrineExtension] Créer des slugs pour les caractéres non latin (Arabe particulièrement).

Le comportement Sluggable de la bibliothèque Doctrine extensions permet de créer facilement des URLs SEO-Friendly.
La mise en place du bundle est facile et ne nécessite pas beaucoup de configuration.

Dans le cas des url non-latin tel que l’arabe, le chinois, ou le japonais, le behavior Sluggable translittère les mots en latin, par exemple le mot « إقتصاد » sera translittéré en « qtsd » ce qui n’a aucun sens.

Dans cet article, nous allons voir comment installer et configurer le Bundle StofDoctrineExtensions pour bien créer des slugs pour les langues non-latin.

Installation du StofDoctrineExtensionsBundle

Le bundle StofDoctrineExtensionsBundle intègre la bibliothèque DoctrineExtensions .
L’installation se fait à travers le gestionnaire des dépendances composer :

composer require "doctrine/doctrine-fixtures-bundle:dev-master"
composer require "stof/doctrine-extensions-bundle:dev-master"

Par la suite, nous activons le bundle dans le fichier AppKernel.php

// app/AppKernel.php
// ...

public function registerBundles()
{
    $bundles = array(
        // ...
        new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
    );

    // ...
}

À la fin, nous ajoutons la configuration de bundle à la fin de notre fichier config.yml :

stof_doctrine_extensions:
    default_locale: en_US
    orm:
        default:
            sluggable:   true

Utilisation du comportement Sluggable

Dans notre exemple, nous allons créer un slug pour la propriété « name » de notre entity Post.

// src/Nm/CoreBundle/Entity/Post.php
// ...

use Gedmo\Mapping\Annotation as Gedmo;
// ...

class Post
{
    // ...

    /**
     * @Gedmo\Slug(fields={"name"}, updatable=false)
     * @ORM\Column(length=255, unique=true)
     */
    protected $slug;
}

Ce code permet de créer automatiquement des slugs sur la propriété « name ».
En ajoutant « updatable=false », cela indiquera que le slug ne sera jamais mis à jour une fois créée, même si nous changons la valeur de « name ».
C’est une bonne idée parce que le slug sera utilisé dans l’URL de l’événement, que nous ne voulons pas changer.

Par la suite nous mettons à jour notre base de données :

php app/console  doctrine:schema:update --force

Si vous avez des anciennes données vous allez vous trouver avec une erreur de contrainte d’intégrité, il faut donc vider la table de ces données et relancer la mise à jour, pour cela on peut supprimer toutes les tables et les recréer à nouveau à travers la console de symfony pour contourner ce problème :

php app/console doctrine:schema:drop
php app/console doctrine:schema:create

À ce niveau, l’installation et la configuration du comportement sluggable sont terminées. Vous pouvez faire les tests et voir que tout est fonctionnel.

Adaptation du comportement Sluggable pour les url non-latin

Comme je vous ai dit au début, le but de cet article est d’adapter ce behavior pour créer des slugs pour les langues non-latin.

Beaucoup de CMS, tel-que Drupal et WordPress possède un système de slugify puissant.
Pour notre cas, nous allons utiliser les fonctions utf8_uri_encode et slugify de wordpress.

Nous commencons par créer un service personnalisé et par la suite nous l’activons pour surcharger celui par défaut de DoctrineExtensions.

Maintenant vous pouvez slugify votre url facilement en gardant votre langue par défaut. Dans le cas de l’Arabe le mot « مال و أعمال » sera « مال-و-أعمال ».