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;

[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.

<!-- src/Nm/CoreBundle/Resources/config/services.xml -->
<service id="stof_doctrine_extensions.listener.sluggable" class="%stof_doctrine_extensions.listener.sluggable.class%" public="false">
<tag name="doctrine.event_subscriber" connection="default"/>
<call method="setAnnotationReader">
<argument type="service" id="annotation_reader" />
</call>
<call method="setTransliterator">
<argument type="collection">
<argument>Nm\CoreBundle\Service\Slugger</argument>
<argument>transliterate</argument>
</argument>
</call>
<call method="setUrlizer">
<argument type="collection">
<argument>Nm\CoreBundle\Service\Slugger</argument>
<argument>urlize</argument>
</argument>
</call>
</service>
view raw services.xml hosted with ❤ by GitHub
services:
stof_doctrine_extensions.listener.sluggable:
class: %stof_doctrine_extensions.listener.sluggable.class%
public: false
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [setAnnotationReader, ['@annotation_reader']]
- [setTransliterator, [[Nm\CoreBundle\Service\Slugger, transliterate]]]
- [setUrlizer, [[Nm\CoreBundle\Service\Slugger, urlize]]]
view raw services.yml hosted with ❤ by GitHub
<?php
//src/Nm/CoreBundle/Service/Slugger.php
namespace Nm\CoreBundle\Service;
use Gedmo\Sluggable\Util\Urlizer;
class Slugger
{
/**
* Disable the transliterate
*
* @param $text
* @param string $separator
* @return mixed
*/
public static function transliterate($text, $separator = '-')
{
$text = self::slugify($text, $separator);
return $text;
}
/**
* Slugify the given text
*
* @param $text
* @param string $separator
* @return string
*/
public static function urlize($text, $separator = '-')
{
$text = Urlizer::unaccent($text);
$text = self::slugify($text, $separator);
return $text;
}
/**
* Encode the Unicode values to be used in the URI.
*
* ported from wordpress
* @see https://core.trac.wordpress.org/browser/tags/3.9.1/src/wp-includes/formatting.php#L572
*
* @param string $utf8_string
* @param int $length Max length of the string
* @return string String with Unicode encoded for URI.
*/
private static function utf8_uri_encode($utf8_string, $length = 0)
{
$unicode = '';
$values = array();
$num_octets = 1;
$unicode_length = 0;
$string_length = strlen($utf8_string);
for ($i = 0; $i < $string_length; $i++) {
$value = ord($utf8_string[$i]);
if ($value < 128) {
if ($length && ( $unicode_length >= $length ))
break;
$unicode .= chr($value);
$unicode_length++;
} else {
if (count($values) == 0)
$num_octets = ( $value < 224 ) ? 2 : 3;
$values[] = $value;
if ($length && ( $unicode_length + ($num_octets * 3) ) > $length)
break;
if (count($values) == $num_octets) {
if ($num_octets == 3) {
$unicode .= '%' . dechex($values[0]) . '%' . dechex($values[1]) . '%' . dechex($values[2]);
$unicode_length += 9;
} else {
$unicode .= '%' . dechex($values[0]) . '%' . dechex($values[1]);
$unicode_length += 6;
}
$values = array();
$num_octets = 1;
}
}
}
return $unicode;
}
/**
* Make the string slug compatible
* ported from wordpress
*
* @see https://core.trac.wordpress.org/browser/tags/3.9.1/src/wp-includes/formatting.php#L1058
*
* @param $text
* @param $separator
* @return string
*/
private static function slugify($text, $separator)
{
$text = str_replace('%', '', $text);
if (Urlizer::seemsUtf8($text)) {
if (function_exists('mb_strtolower')) {
$text = mb_strtolower($text, 'UTF-8');
}
$text = self::utf8_uri_encode($text, 200);
} else {
$text = strtolower($text);
}
$text = str_replace('.', $separator, $text);
// Convert nbsp, ndash and mdash to hyphens
$text = str_replace(array('%c2%a0', '%e2%80%93', '%e2%80%94'), $separator, $text);
// Strip these characters entirely
$text = str_replace(array(
// iexcl and iquest
'%c2%a1', '%c2%bf',
// angle quotes
'%c2%ab', '%c2%bb', '%e2%80%b9', '%e2%80%ba',
// curly quotes
'%e2%80%98', '%e2%80%99', '%e2%80%9c', '%e2%80%9d',
'%e2%80%9a', '%e2%80%9b', '%e2%80%9e', '%e2%80%9f',
// copy, reg, deg, hellip and trade
'%c2%a9', '%c2%ae', '%c2%b0', '%e2%80%a6', '%e2%84%a2',
), '', $text);
$text = preg_replace('/[^%a-z0-9 _-]/', '', $text);
$text = preg_replace('/\s+/', $separator, $text);
$text = preg_replace('|-+|', $separator, $text);
$text = trim($text, $separator);
$text = urldecode($text);
return $text;
}
}
view raw Slugger.php hosted with ❤ by GitHub

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

[Symfony 2] Implémentation du moteur de recherche Zend Lucene

Dans l’article précédant, nous avons vu comment installer EWZSearchBundle, le bundle d’intégration de Zend Lucene. La documentation officielle de bundle est très pauvre, donc pour un newbie, c’est difficile de se trouver. Dans cet article, on va voir en détails comment utiliser ce bundle.

Indexation

Pour pouvoir utiliser le moteur de recherche, on doit construire en premier un fichier d’index qui contiendra les textes qui seront utilisés dans les recherches.

Le fichier index sera enregistré sous le path définis dans la configuration de bundle. L’indexation sera faite au fur et à mesure de l’enregistrement des données.

Création d’un index

Pour ce tutorial, on va utiliser comme exemple l’Entity Poste :

class Poste
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

    /**
     * @var text $body
     *
     * @ORM\Column(name="body", type="text")
     */
    private $body;
}

L’indexation d’un post est simple : la clé primaire est stockée pour un référencement ultérieur lors de la recherche des postes et les colonnes title et body sont indexées, mais pas stockées dans l’index, car nous allons utiliser les objets réels pour afficher les résultats.

Donc à chaque fois qu’un post est créé, modifié ou supprimé, l’index doit être mis à jour.
Nous allons modifiers les actions createAction, updateAction et deleteAction de notre controlleur PosteController

Tout d’abort, nous ajoutons les indépendances suivante au début de PosteController.php :

use EWZ\Bundle\SearchBundle\Lucene\Document;
use EWZ\Bundle\SearchBundle\Lucene\Field;

Par la suite, nous ajoutons une méthode de création des indexes, elle sera utilisé dans les actions createAction et updateAction.

Zend Lucene a 5 façons pour indexer des champs :

  • Keyword : Le champ est stocké ET indexé. Cela signifie qu’il peut être aussi bien cherché dans l’index qu’affiché dans les résultats de la recherche.
  • UnIndexed : Le champ ne peut pas être utilisé dans la recherche. En revanche, il peut être retourné dans les résultats.
  • Text : Le champ est stocké, indexé et « tokenizé » (devisé en plusieurs mots).
  • Binary : Le champ n’est ni « tokenizé », ni indexé, mais il est stocké dans le but d’être retourné dans les résultats de recherche.
  • UnStored : Le champ est « tokenizé » et indexé, mais pas stocké dans l’index..

Dans notre exemple, nous allons utilisé, seulement « Keyword » pour stoker l’id du Post, et « UnStored » pour enregistrer « title » et « body » du post.

Voici la méthode de création d’index :

private function createIndex($post)
{
    $search = $this->get('ewz_search.lucene');

    $document = new Document();
    $document->addField(Field::Keyword('key', $post->getId()));
    $document->addField(Field::UnStored('title', $post->getTitle()));
    $document->addField(Field::UnStored('body', $post->getBody()));

    $search->addDocument($document);
    $search->updateIndex();
}

Comme on a déjà dit, on ajoute cette fonction dans createAction et updateAction après la validation de formulaire comme suit :

        if ($form->isValid()) {
            $this->createIndex($entity);
            $em->persist($entity);
            $em->flush();

             return $this->redirect($this->generateUrl('posts_show', array(
                        'id' => $entity->getId()
                     )));
            $query = $form->getData();

        }

Lancer une recherchet

Maintenant que nous avons tout mis en place, nous passons à l’implémentation de l’action recherche, la méthode $this->get(‘ewz_search.lucene’)->find(), interroge l’index pour trouver des mots correspondantes à notre mot cherchée :

    /**
     * 
     * @param \Symfony\Component\HttpFoundation\Request $request
     * @Route("/search", name="search_poste")
     * @Template()
     */
    public function searchAction(Request $request)
    {
        $form = $this->createFormBuilder()->add('search', 'text')->getForm();

        $posts = new \Doctrine\Common\Collections\ArrayCollection;

        if($request->getMethod() === "POST") {

            $form->handleRequest($request);

            $query = $form->getData();

            $results = $this->get('ewz_search.lucene')->find($query['search']);

            $em = $this->getDoctrine()->getManager();

            foreach($results as $hit)
            {
                $document = $hit->getDocument();
                $post = $em->getRepository('BlobBundle:Poste')->find($document->key);
                $posts->add($post);
            }
        }

        return array(
            "posts" => $posts,
            "form" => $form->createView()
         );
    }

Voici une simple Template twig pour l’action :

<form action="{{ path("search_poste") }}" method="post" {{ form_enctype(form) }}>
    {{ form_errors(form) }}
    {{ form_rest(form) }}
    <input type="submit" />
</form>

{% if posts.count > 0 %}
    <h1>Search result</h1>
    <table class="records_list">
        <thead>
            <tr>
                <th>Id</th>
                <th>Title</th>
                <th>Body</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
        {% for entity in posts %}
            <tr>
            <td><a href="{{ path('poste_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
            <td>{{ entity.title }}</td>
            <td>{{ entity.body }}</td>
            <td>
                <ul>
                    <li>
                       <a href="{{ path('poste_show', { 'id': entity.id }) }}">show</a>
                    </li>
                    <li>
                        <a href="{{ path('poste_edit', { 'id': entity.id }) }}">edit</a>
                    </li>
                </ul>
            </td>
        </tr>
        {% endfor %}
        </tbody>
</table>
{% endif %}

Supprimer un index

On arrive à la fin, il nous reste que la mise à jours de l’index après la surpression d’un post, nous allons créer une methode deleteIndex:


    
    private function deleteIndex($post) 
    {
        $search = $this->get('ewz_search.lucene');

        $document = new Document();
        $document->addField(Field::Keyword('key', $post->getId()));

        $search->deleteDocument($document);
    }

Cet méthode sera appelé dans l’action deleteAction() juste après la suppression du poste :


        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $entity = $em->getRepository('BlobBundle:Poste')->find($id);

            if (!$entity) {
                throw $this->createNotFoundException('Unable to find Poste entity.');
            }

            $em->remove($entity);
            $em->flush();
            
            $this->deleteIndex($entity);
        }

[Symfony2] Collection des Bundles pour accélérer votre développement

Depuis la naissance de Symfony2, un grand nombre de bundles ont été créés pour faciliter la tâche au développeurs.
Voici ma collection des bundles indispensable pour chaqu’un de mes projets Symfony :

1. FOSUserBundle : un bundle pour la gestion des utilisateurs, il implémente beaucoup de fonctionnalités tel que inscription, authentification, récupération mot de passe …

2. DoctrineExtensions : un bundle très pratique intègre une série d’extensions Doctrine comme Sluggable, Translatable, Timetampable, Sortable …

3. KnpPaginatorBundle : un système de pagination simple d’utilisation et performant.

4. GenemuFormBundle : Bundle Symfony plein de FormType à insérer dans les formulaires. il facilite l’utilisation des champs de formulaire de type Select2, tinyMCE, ReCaptcha, Rating …

5. KnpMenuBundle : c’est un Bundle qui permet de gérer assez facilement les menus d’un site web. Faire un menu statique est très simple, mais parfois, le développeur a besoin de construire un menu (ou un sous-menu) en fonction de contenus en base de données. Ce Bundle va donc vous être très utile.