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.

[PHP] Comprendre le design pattern Factory

Le patron de conception la Fabrique (factory) est un patron de conception de création souvent utilisé en programmation orientée objet.

Le but de ce patron est de retourné une instance d’une classe parmi plusieurs possibles, en fonction des paramètres qui ont été fournis.

Par exemple, nous allons développer une bibliothèque qui parse un fichier est retourne les données y contenue dans un tableau (Array), ce fichier peut être de type : json, xml ou yaml.

La solution sera de créer une classe pour chaque type de fichier, chaque classe est composée de deux methodes : read et parse.

La première étape consiste à créer une interface qui définis ces deux méthodes :

interface ParserInterface {

    /**
     * Read file content
     */
    public function readFile();

    /**
     * Parse file content
     */
    public function parseFile();
}

Par la suite nous définissons nos classes qui vont implémenter cet interface :

class JsonParser implements ParserInterface {

    public $content;

    public function __construct($file) {
        $this->readFile($file);
    }

    public function readFile($file) {
        if (file_exists($file)) {
            $this->content = file_get_contents($file);
        }
    }

    public function parseFile() {
        return json_decode($this->content);
    }
}
class YamlParser implements ParserInterface {

    public $content;

    public function __construct($file) {
        $this->readFile($file);
    }

    public function readFile($file) {
        if (file_exists($file)) {
            $this->content = file_get_contents($file);
        }
    }

    public function parseFile() {
        return yaml_parse($this->content);
    }

}
class XmlParser implements ParserInterface {

    public $content;

    public function __construct($file) {
        $this->readFile($file);
    }

    public function readFile($file) {
        if (file_exists($file)) {
            $this->content = file_get_contents($file);
        }
    }

    public function parseFile() {
        return simplexml_load_string($this->content);
    }

}

Pour utiliser cette bibliothèque, il faut à chaque fois tester sur le type de fichier est initialisé la classe correspondante, le code rassemblera à celui la :

$file = __DIR__ . "/file.json";

switch (pathinfo($file, PATHINFO_EXTENSION)) {
    case "json":
        $obj = new JsonParser($file);
        break;
    case "xml":
        $obj = new XmlParser($file);
        break;
    case "yml":
        $obj = new YamlParser($file);
        break;
    default:
        throw new Exception("File type unsupported");
}

var_dump($obj->parseFile());

À chaque fois que nous allons utiliser notre bibliothèque, nous aurons besoin de ré-écrire ce code, si un jour nous ajoutons un nouveau parseur, nous aurons besoin de faire un tour dans les fichiers et modifier cette partie.

Pour avoir une seule endroit ou initialiser notre parseur, nous faisons appel au patron fabrique qui sera le seule responsable de la création / distribution de l’objet, ainsi nous pouvons modifier/ajouter des nouveaux parseur sans se soucier.

Notre classe Factory sera une simple class PHP, contenant une méthode abstraite qui se charge de l’initialisation des objets, elle sera définis comme suit :

class ParserFactory {

    public static function getParser($file) {

        switch (pathinfo($file, PATHINFO_EXTENSION)) {
            case "json":
                return new JsonParser($file);
            case "xml":
                return new XmlParser($file);
            case "yml":
                return new YamlParser($file);
            default:
                throw new Exception("File type unsupported");
        }
    }
}

Par la suite, à chaque fois que nous voulons parser un fichier, nous appelons notre méthode statique :

$file = __DIR__ . "/file.json";

$obj = ParserFactory::getParser($file);

var_dump($obj->parseFile());

Vous pouvez consulter le code source de cet exemple en suivant ce lien

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;