Ma Relation ManyToMany n’est pas persisté.

La persistance de la relation ManyToMany avec doctrine n’est pas bidirectionnelle.

Il y a un côté maitre et un côté esclave. Seule la sauvegarde du côté maitre entrainera la sauvegarde du côté esclave.

Exemple :

<?php

class Article
{

    /**
    * @ManyToMany(targetEntity="Tag", inversedBy="articles")
    * @JoinTable(name="article_tag")
    **/
    private $tags;

}

class Tag
{

    /**
    * @ManyToMany(targetEntity="Article", mappedBy="tags")
    **/
    private $articles;

}

Dans cet exemple, c’est la classe Article qui est maitresse.

Lorsque l’on persistera la classe Article, les liaisons avec la classe Tag seront persistées elle aussi. Ce n’est pas le cas quand on persiste la classe Tag (il faudra développer du code spécifique)

Pense bête : Gestion des erreurs avec les formulaires imbriqués sous Symfony 2.1

Lorsqu’on met en place des formulaires imbriqués avec Symfony 2.1 il ne faut pas oublier que la gestion des formulaires à légèrement été revue entre la version 2.0 et les versions 2.x, comme expliqué dans la notice prévue UPGRADE FROM 2.0 to 2.1.

Il ne faut également pas oublier de consulter la documentation Symfony officielle expliquant le fonctionnement et la mise en place des formulaires imbriqués.

Au sujet des erreurs sur les formulaire imbriqué, cette dernière nous expose comment « faire remonter » les erreurs propres au premier formulaire imbriqué dans le second. Il ne faut pas oublier de préciser au formulaire qui reçoit le premier, qu’il va devoir récupérer des erreurs autres que les siennes en précisant dans la méthode setDefaultOptions() de votre Form comme ceci :

namespace Acme\Bundle\AcmeBundle\Form;

class MyEntityType extends AbstractType
{
    public buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('myotherentity', new MyOtherEntityType());
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\Bundle\AcmeBundle\Entity\MyEntity',
            'cascade_validation' => true
        ));
    }

    ...
}

Le tout couplé avec vos « Assert » sur les champs de vos entitées et une vue exposant correctement les erreurs de vos formulaires comblera la plupart de vos besoins.

Twig : Comment créer facilement et simplement votre propre filter

Les filters Twig sont très utiles lors des développements d’interfaces. Vous ne pouvez pas le nier, vous les utilisez tous les jours et je suis sûr que vous aimez ça :)

Twig vient avec tout un tas de filters plus utiles les uns que les autres, je vous invite à consulter cette liste complète des filters par défaut.

Vous pouvez également installer des filters en plus de ceux par défauts comme par exemple les quelques filters que Fabien Potencier a regroupé dans un repository Github appelé « Twig-extensions » et que nous vous recommandons. Installez facilement grâce à Composer et vous pourrez par exemple utiliser les filters debug (équivalent d’un var_dump() depuis une vue Twig), des filters relatifs à la manipulation de chaines de caractères comme avec le filter truncate ou encore des filters vous donnant la possibilité d’internationaliser vos vues.

Les filters par défaut et ceux de Fabien Potencier répondent aux besoins de la majorité des développements mais qu’en est-il si vous vous rendez compte qu’aucun des filters que vous connaissez ne réponde à un besoin particulier que vous exprimez ? Rien de plus simple, créez-le ! C’est ce que nous allons voir ici.

Pour commencer, vous devez créer un dossier Twig dans lequel vous créerez un autre dossier Extension à la racine de votre Bundle. Lorsque c’est chose faite, créez dans ce dernier dossier un fichier portant le nom de votre extension Twig comme par exemple MyfilterExtension.php. Voici ensuite ce que vous devrez au moins avoir dans ce fichier :

namespace Acme\Bundle\AcmeBundle\Twig\Extension;

class MyfilterExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return array(
            'myfilter' => new \Twig_Filter_Method($this, 'doSomething')
        );
    }

    public function doSomething($value)
    {
        return 'transformated-' . $value;
    }

    public function getName()
    {
        return 'myfilter_extension';
    }

}

Ce que fait ce filter est très simple, à chaque fois que vous utiliserez dans votre vue Twig le mot clef myfilter, il ajoutera la chaine ‘transformated-’ devant la valeur de la chaine sur laquelle vous être en train d’appliquer votre filter, par exemple :

{{ 'value' }}

Affichera la chaîne de caractère value en tant que telle dans votre vue.

{{ 'value' | myfilter }}

Affichera la chaîne de caractère transformated-value dans votre vue.
Je vous laisse imaginer toutes les possibilités que vous pouvez développer avec ça ! :)

Une fois votre classe terminée, vous n’avez plus qu’à la déclarer dans votre services.xml afin d’injecter votre classe et de pouvoir l’utiliser.

...
<services>
    ...
    <service id="acme.twig.myfilter_extension" class="Acme\Bundle\AcmeBundle\Twig\Extension\MyfilterExtension"> 
        <tag name="twig.extension" />
    </service>
    ...
</services>
...

Un point intéressant consiste à donner la possibilité à votre filter d’accepter des paramètres depuis la vue Twig, en plus de la valeur (ou l’objet) sur lequel vous voulez effectuer un traitement. On pourrait imaginer par exemple un appel à notre filter suivant :

{{ 'value' | myfilter('rockin') }}

Et avoir une méthode doSomething() acceptant cette fois-ci un deuxième paramètre.

public function doSomething($value, $parameter)
{
    return 'transformated-' . $value . '-' . $parameter;
}

ce qui nous retournerais, en gardant notre exemple précédent, la chaîne transformated-value-rockin.

Bien sûr, ce n’est ici qu’une application simple, d’autres utilisations plus poussées des filters peuvent être faites comme par exemple passer un manager en paramètre à votre classe lors de l’injection pour pouvoir effectuer du traitement lié à des entités ou autre.

Les différents moyens d’exécution d’une requête Doctrine2

Pour exécuter une requête Doctrine, il y’a plusieurs façons de faire à partir du Query Builder Doctrine2.

Doctrine2: Récupérer une collection de résultats

Exemple de requête à exécuter:

  $q = $this->createQueryBuilder('p')
->where('p.state = :state')
->setParameter('state', $state);

Récupérer tous les posts valides.

$q->getQuery()->execute();

Va permettre de retourner une collection d’objets (\Doctrine\Common\Collections\ArrayCollection()) de notre entité Post.

execute() a un équivalent:

$q-&gt;getQuery()->getResult()

Doctrine2: Récupérer un seul résultat

Si nous souhaitons récupérer une seule entrée avec Doctrine dans Symfony2:

$q = $this->createQueryBuilder('p')
->where('p.id = :post')
->setParameter('post', $idPost);

Il suffit d’utiliser getSingleResult() ou getOneOrNullResult().

$q->getQuery()->getOneOrNullResult()

L’avantage de getOneOrNullResult() et qu’il retournera null si aucune donnée n’est trouvée, contrairement à getSingleResult() qui déclenchera une erreur.

Doctrine2: Récupérer uniquement une valeur

Dans le cas d’un count() par exemple, nous ne souhaitons récupérer qu’une seule valeur avec notre requête:

$q = $this->createQueryBuilder('p')
          ->select('COUNT(p)');
On exécute la requête du Query Builder avec getSingleScalarResult()
$q->getQuery()->getSingleScalarResult();

Doctrine2: Récupérer les données en tableau

Et si vous ne souhaitez pas récupérer vos données sous forme d’objet, vous pouvez les récupérer formatées en tableau:
   $q = $this->createQueryBuilder('p')
->where('p.state = :state')
->setParameter('state', $state);

Récupérer tous les posts valides.

$q->getQuery()->getScalarResult();
OU
$q->getQuery()->getArrayResult();

PagerFanta et l’erreur « array_merge() »

PagerFanta est un Bundle pour Symfony 2 qui permet de paginer l’affichage de collections. Il est assez simple d’utilisation, mais il est courant de rencontrer l’erreur suivante :

"Warning: array_merge() [<a href='function.array-merge'>function.array-merge</a>]: Argument #2 is not an array in /path/to/my/project/vendor/bundles/WhiteOctober/PagerfantaBundle/Twig/PagerfantaExtension.php line 73") in MaBoite\/MonBundle\/Resources\/views\/MonControlleur\/index.html.twig at line XXX.

Ce message apparait lorsque l’on a installé une mauvaise version de PagerFanta. Les sources disponibles par défaut sur GitHub suivent la branche master de Symfony, qui avance actuellement sur la version 2.1 du Framework.

Il est très probable que la version Symfony sur laquelle vous travaillez soit une 2.0.XX (dernière version stable au moment où j’écris ces lignes). Il faut donc utiliser la branche symfony2.0 de PagerFanta. Si vous installez les sources directement par copier/coller (déconseillé), remplacez simplement les sources master par celles de la branche symfony2.0. Si vous installez le bundle via le fichier deps (recommandé), il vous faut utiliser les lignes suivantes.

[PagerFanta]
 git=http://github.com/whiteoctober/Pagerfanta.git
 target=pagerfanta
[PagerFantaBundle]
 git=http://github.com/whiteoctober/WhiteOctoberPagerfantaBundle.git
 target=bundles/WhiteOctober/PagerfantaBundle
 version=origin/symfony2.0

Lancez ensuite simplement la commande pour installer les vendors.

bin/vendors install

Attention ! Si vous aviez déjà installé la version master de PagerFanta via le fichier deps, celle ci est probablement verrouillée. Il vous faudra alors supprimer la ligne PagerFantaBundle dans le fichier deps.lock avant de lancer l’installation.

Service pour récupérer le nom du controller ou de l’action dans la vue

Il faut d’abord commencer par créer la classe dans Acme\Bundle\AcmeBundle\Twig\Extension

<?php
namespace Acme\Bundle\AcmeBundle\Twig\Extension;
use Symfony\Component\HttpFoundation\Request;
class AcmePageExtension extends \Twig_Extension
{
 protected $request;
 /**
 *
 * @var \Twig_Environment
 */
 protected $environment;
public function __construct(Request $request)
 {
 $this->request = $request;
 }
public function initRuntime(\Twig_Environment $environment)
 {
 $this->environment = $environment;
 }
public function getFunctions()
 {
 return array(
 'getControllerName' => new \Twig_Function_Method($this, 'getControllerName'),
 'getActionName' => new \Twig_Function_Method($this, 'getActionName'),
 );
 }
/**
 * Get controller name
 */
 public function getControllerName()
 {
 $regexp = "#Controller\\\([a-zA-Z]*)Controller#";
 $results = array();
 preg_match($regexp, $this->request->get('_controller'), $results);
return strtolower($results[1]);
 }
/**
 * Get action name
 */
 public function getActionName()
 {
 $regexp = "#::([a-zA-Z]*)Action#";
 $results = array();
 preg_match($regexp, $this->request->get('_controller'), $results);
return $results[1];
 }
public function getName()
 {
 return 'acme_page';
 }
}

Ensuite, il faut créer le service dans services.xml

<parameters>
  <parameter key="acme.enhance.twig.class">Acme\Bundle\AcmeBundle\Twig\Extension\AcmePageExtension</parameter>
</parameters>

<services>
<service id="request" class="Symfony\Component\HttpFoundation\Request" />
<service id="acme.enhance.twig" class="%acme.enhance.twig.class%">
 <tag name="twig.extension" />
 <argument type="service" id="request" />
 </service>
</services>

Pour connaitre le nom du controller ou de l’action, il suffit de faire :

{{ getControllerName() }}
ou
{{ getActionName() }}

Annotation et paramètres par défaut pour les routes sf2

Si l’on souhaite passer la page courante dans une route sf2, en utilisant les annotations, pour obtenir à une structure comme ceci  :

/news/ (page = 1)
/news/2 (page = 2)

On peut utiliser l’option defaults de @Route en affectant 2 routes à son contrôleur :

    /**
     *
     * @Route("/news",  name="news_list", defaults={"page" = 1})
     * @Route("/news/{page}", name="news_list_page", requirements={"page" = "\d+"})
     * @Template()
     */
    public function indexAction($page)
    {
       return array()
    }

Symfony2 et la gestion des rôles personnalisés

Par défaut, Symfony2 donne comme exemple les rôles ROLE_USER, ROLE_ADMIN et ROLE_SUPER_ADMIN dans le fichier app/config/security.yml.

Il est bien sûr possible d’en créer de nouveaux en pensant à bien les préfixer obligatoirement par ROLE_ sinon la gestion des droits d’accès échouera (cf le constructeur de RoleHierarchyVoter).

Utiliser un Validator dans un Form\Type Symfony2

Vous avez sûrement déjà dû utiliser des fonctions de validation appelées en callback sur vos formulaires (CallbackValidator) pour tester un retour de formulaire et lever une erreur sur un certain champs si la valeur de ce dernier n’est pas valide.

public function buildForm(FormBuilder $builder, array $options)
{
    $builder->add('myfield', 'text');
    $builder->addValidator(new CallbackValidator(function(FormInterface $form) {
    $myfield = $form->get('myfield');
    if ($myfield->getData() != null && !is_string($myfield->getData())) {
        $myfield->addError(new FormError('Invalid format'));
    }
}

Et bien sachez que vous pouvez utiliser dans vos Form/Type des Validator (à l’instar de ceux qu’on peut invoquer en utilisant les Assert dans nos entitées) qui vont pouvoir vous apporter un nombre important d’outils qui vont vous aider à valider vos formulaires.

Tout d’abord, il vous faudra utiliser les namespace de classes suivants :

<?php
namespace Acme\Bundle\AcmeBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\CallbackValidator;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\RegexValidator;

class FooType extends AbstractType
{
    [...]
}

Les deux derniers sont ceux que nous devons rajouter pour pouvoir utiliser cette contrainte de validation. Ici nous avons choisit la validation de Regex mais vous pouvez utiliser n’importe quel type de contrainte tant qu’elle possède une classe Validator associée. (cf. liste des toutes les contraintes)

Il vous suffira désormais de juste instancier de nouveaux objets dans votre CallbackValidator afin de pouvoir analyser et valider une valeur.

[...]
public function buildForm(FormBuilder $builder, array $options)
{
    $builder->add('myfield', 'text');
    $builder->addValidator(new CallbackValidator(function(FormInterface $form) {
    $myfield = $form->get('myfield');
    if ( ! is_null($myfield->getData()) ) {
        $validator      = new RegexValidator();
        $constraint     = new Regex(array(
            'pattern' => "/^[a-z0-9-]+$/"
        ));
        $isValid = $validator->validate( $myfield->getData(), $constraint );
        if ( ! $isValid ) {
            $myfield->addError( new FormError( "This field is not valid (only alphanumeric characters separated by hyphens)" ) );
        }
    }
}
[...]

Voilà une astuce bien sympathique qui peut vous être utile à valider un champs qui, par exemple, ne figure pas dans les attributs de votre entitée. Exactement comme vous pourriez le faire en utilisant les validations de type Assert.