[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 « مال-و-أعمال ».

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

[Symfony2][FOSUserBundle] Utiliser l’adresse mail pour se connecter

FOSUserBundle a son propre provider pour que vous pouvez utiliser non seulement le nom d’utilisateur, mais aussi l’email pour ce connecter. Il suffit de changer une ligne dans security.yml


# app/config/security.yml
security:
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email

Par contre si vous voulez utiliser seulement l’adresse mail pour se connecter, il faut passer par une autre méthode. Selon la documentation d’une ancienne version de FosUserBundle, la méthode le plus « propre » et la moins risquer et de passer par un provider personnalisé.


<?php
namespace FOS\UserBundle\Security;

class EmailUserProvider extends UserProvider
{
    protected function findUser($username)
    {
        return $this->userManager->findUserByUsernameOrEmail($username);
    }
}

Comme vous pouvez remarquer notre provider personnalisé hérite de celui de FosUserBundle, et on a surchargé seulement une seule méthode.

Maintenant, nous allons l’ajouter comme un service à notre application :


<parameters>
    <parameter key="gmu.user_provider.email.class">Gmu\UserBundle\Security\GmuUserProvider</parameter>
  </parameters>
  <services>
    <service id="gmu.user_provider.email">
      <argument type="service" id="fos_user.user_manager" />
    </service>
  </services>

Nous avons injecté FOSUserManager parce que la classe parente en a besoin. Après cela, nous indiquant qu’on va utiliser notre service comme le provider par défaut :


# app/config/security.yml
security:
    providers:
        fos_userbundle:
            id: gmu.user_provider.email

[Symfony2] Désactiver le jeton CSRF d’un formualire

Le rôle de jeton CERF est d’éviter les attaques XSS et CSRF. Dans Symfony il s’ajoute automatiquement au formulaire sous la forme d’un hidden input.

Voici 2 méthode pour désactiver ce jeton, Il faut noter que c’est déconseillé de le faire :

1- Dans la class MonFormType.php :


public function getDefaultOptions(array $options)
{
    return array(
        'data_class' => 'Acme\DemoBundle\Entity\MyEntity',
        'csrf_protection' => false,
);
}

2- Lors de la création du formaulaire dans une action :


$form = $this->createFormBuilder($users, array( 'csrf_protection' => false,))
->add(...);

[phpMyAdmin] Contourner la limite de la taille du fichier à importer

Si vous rencontrez un problème avec l’importation des fichiers SQL de gros volumes dans phpMyAdmin, voici une astuce pour contourner la limite max de la taille de fichier, fixé par défaut à 2 Mo dans php.ini :

  1. Créez le dossier « upload » sous le répertoire de phpMyAdmin et y mettre votre fichier.
  2. À la fin de fichier config.inc.php, ajoutez:
    $cfg['UploadDir'] = './upload';
  3. Allez maintenant à l’onglet importer dans phpMyAdmin, vous allez voir qu’une nouvelle option est apparue :

    Contourner la limite de la taille du fichier à importer

    Contourner la limite de la taille du fichier à importer

Déployer un projet symfony2 sur OVH

J’ai récemment déployé un projet symfony2 sur mon serveur mutualisé OVH. J’ai rencontrer beaucoup de problèmes liés tout aux fichiers htaccess.

Voici les fichiers utilisés pour faire fonctionner mon projet :

La première fichier est sous la racine de projet :

SetEnv SHORT_OPEN_TAGS 0
SetEnv REGISTER_GLOBALS 0
SetEnv MAGIC_QUOTES 0
SetEnv SESSION_AUTOSTART 0
SetEnv ZEND_OPTIMIZER 1
SetEnv PHP_VER 5_3

RewriteEngine on
RewriteBase /

RewriteCond %{REQUEST_URI} !^/web/
RewriteRule ^(.*)$ /web/$1 [L]

La deuxième est sous le dossier web/ :

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ app.php [QSA,L]