Symfony : Boucler sur les champs d’un formulaire avec twig

Sous symfony, il est très simple d’afficher un formulaire entier avec twig. Il suffit d’une ligne {{form(form)}}
Il y’a aussi différentes possibilités pour personnaliser l’affichage (afficher les champs un par un, surcharger les template de formulaires, créer des champs personnalisés…etc

Cependant si on veut adapter seulement un champs particulier de notre formulaire, pour y afficher par exemple une explication au dessus ou quelque chose de plus complexe.
La solution la plus rapide est la plus simple est de boucler sur les champs du formulaire et de placer une condition comme ci-dessous :

1
2
3
4
5
6
7
8
9
10
11
{{ form_start(form, {'attr': {'class': 'form-class'}}) }}
    {% for key, field in form.children %}
        {# condition pour afficher le champ particulier #}
       {% if key=='mon_champ_particulier' %}
            <div class="ma_classe">Mon explication, widget ou tout autre éléments spécifique pour ce champ.</div>
            {{form_row(field)}}
        {% else %}
            {{form_row(field)}}
        {% endif %}
    {% endfor %}    
{{ form_end(form) }}

Comme indiqué au dessus, cette solution est à appliqué pour les cas particuliers, pour des éléments semblables qui se répété mieux vaut mettre en place une solution générique avec un type de champ personnalisé par exemple.

Exemple de requête « complète » avec Doctrine (dans un repository symfony)

Un exemple commenté d’une recherche dans une entité doctrine comprenant pas mal de cas avec le QueryBuilder.
Il s’agit ici d’une méthode situé dans le repository d’une entité doctrine sous Symfony.
De quoi s’en inspirer pour faire son petit moteur de recherche avec multiple critères et pagination.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
    /**
     * recherche les résultats correspondants aux critères passés en paramètres
     * @param type $data - tableau des paramètres au format ('nomDeLaPropriete'=>'valeur')
     * @param type $limit - le nombre de résultats voulus. false si on veut tous les résultats.
     * @param int $page - le numéro de la page de résultat attendus (en fonction du nombre de résultats $limit)
     * @param type $count - à true retourne seulement le nombre total de résultats pour les critères $data passés
     * @return type
     */

    public function findByParameters($data, $limit = 20, $page = 1, $count = false) {
        $query = $this->createQueryBuilder('a');
        //si on veut le total
        if ($count) {
            $query->select('count(a.id)');
        } else {
            //select et jointures facultative
            $query->select(array('a','p', 'c'))->leftJoin('a.profession', 'p')->leftJoin('a.city', 'c');
        }
        //recherche la valeur exacte pour les propriétés listés dans le tableaux ci-dessous
        $search = array('id', 'status');
        foreach ($search as $fieldName) {
            if (isset($data[$fieldName])) {
                $query->andwhere('a.' . $fieldName . ' = :' . $fieldName . '')->setParameter($fieldName, $data[$fieldName]);
            }
        }
        //recherche la valeur contenant (%like%) pour les propriétés listés dans le tableaux ci-dessous
        $searchLike = array('title', 'subtitle', 'description');
        foreach ($searchLike as $fieldName) {
            if (isset($data[$fieldName])) {
                $query->andwhere('a.' . $fieldName . ' LIKE :' . $fieldName . '')->setParameter($fieldName, '%' . $data[$fieldName] . '%');
            }
        }
        //recherche les propriétés listés ayant la valeur exacte ou null (pratique pour les relations non obligatoire)
        $searchOrNull = array('profession', 'city');
        foreach ($searchOrNull as $fieldName) {
            if (isset($data[$fieldName])) {
                $query->andwhere('a.' . $fieldName . ' = :' . $fieldName . ' OR a.' . $fieldName . ' IS NULL')->setParameter($fieldName, $data[$fieldName]);
            }
        }
        //exemple de recherche multiples. Exemple l'entité doit appartenir à une ou plusieurs de ces "category"
        //les valeurs des propriétés listés doivent être un tableaux des valeurs possibles
        $searchMultiple = array('category');
        foreach ($searchMultiple as $fieldName) {
            if (isset($data[$fieldName])) {
                $req = '';
                $i = 0;
                foreach ($data[$fieldName] as $item) {
                    if ($i > 0) {
                        $req.=' OR ';
                    }
                    $req.='a.' . $fieldName . ' = :item' . $i . ' ';
                    $query->setParameter('item' . $i, $item);
                    $i++;
                }
                $query->andWhere($req);
            }
        }
        //on peut imagine d'autre exemple recherche supérieur ou inférieur à la valeur, recherche sur des dates...etc
        //
        //si on veut seulement le total retourne simplement le nombre de résultats
        if ($count) {
            return $query->getQuery()->getSingleScalarResult();
        } else {
            //ajout d'un classement (facultatif)
            $query->addOrderBy('a.dateCreate', 'DESC');
            if (!$limit) {
                //rien
            } else {
                //si on veut un numéro de page particulier sélection des résultats en fonction de ce ce numéro de page et du nombre $limit de résultats voulus.
                if ($page < 1) {
                    $page = 1;
                }
                $query->setFirstResult(($page - 1) * $limit)->setMaxResults($limit);
            }
            //retourne la liste des résultats
            return $query->getQuery()->getResult();
        }
    }

Symfony, Générer des PDF avec EnseparHtml2pdfBundle

Pour générer des PDF en utilisant HTML2PDF (permet de générer des PDF à partir de code HTML) avec symfony on peut utiliser le EnseparHtml2pdfBundle

Ajout du bundle dans le fichier composer.json

1
2
3
4
"require": {
    ...
    "ensepar/html2pdf-bundle" : "dev-master"
},

On active dans app/AppKernel.php

1
2
3
4
$bundles = array(
    ...
    new Ensepar\Html2pdfBundle\EnseparHtml2pdfBundle(),
);

On update le framework avec composer

1
php ../composer.phar update

On peux utiliser html2pdf dans un controler pour générer un pdf

1
2
3
4
5
6
7
8
9
10
//on stocke la vue à convertir en PDF, en n'oubliant pas les paramètres twig si la vue comporte des données dynamiques
$html = $this->renderView('AppAppliBundle:Pdf:page-pdf.html.twig', array('data'=>$data));
//on appelle le service html2pdf
$html2pdf = $this->get('html2pdf_factory')->create();
//real : utilise la taille réelle
$html2pdf->pdf->SetDisplayMode('real');
//writeHTML va tout simplement prendre la vue stocker dans la variable $html pour la convertir en format PDF
$html2pdf->writeHTML($html);
//Output envoit le document PDF au navigateur internet
return new Response($html2pdf->Output('nom-du-pdf.pdf'), 200, array('Content-Type' => 'application/pdf'));

Migrer des utilisateurs et leurs mot de passe (md5) vers Symfony

Dans le cadre d’une migration d’un site sur le framework symfony. On doit parfois migrer des utilisateur existants vers le nouveau système d’authentification de symfony.
Dans un cas « basique » ou le mot de passe des membre est simplement codé en md5 c’est en fait très simple (depuis symfony 2.5)

Il faut configurer 2 « encodeurs », le classique, et celui pour les anciens membres.
Dans le fichier /app/config/security.yml

1
2
3
4
5
6
7
8
9
10
security:
    encoders:
        #votre encoder classique
        App\MemberBundle\Entity\Member: sha512
        #encoder pour les membres pré-existants
        v1_encoder:
            #md5 simple dans notre cas
            algorithm: md5
            encode_as_base64: false
            iterations: 0

Il faut ensuite implémenter « encoderAwareInterface » dans notre classe membre et la fonction getEncoderName() qui retourne le nom de l’encoder à utiliser. Pour le définir un simple champs enregistré en base de donnée sur le type de membre que l’on à convient (membre migré ou pas).
Voici ce que ça donne par exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;

class Member implements AdvancedUserInterface, \Serializable, EncoderAwareInterface {

    //implémenter une classe user normale, ajouter un champs isV1 pour définir la version du membre et la méthode ci dessous.

    /**
     * encoder à utiliser
     * @return string
     */

    public function getEncoderName() {
        if ($this->getIsV1()) {
            return 'v1_encoder';
        }
        return null; // use the default encoder
    }
}

sources : http://symfony.com/doc/current/cookbook/security/named_encoders.html et
http://stackoverflow.com/questions/18739101/using-md5-in-symfony2-security-yml-for-legacy-users

Symfony & FOSUserBundle : « Integrity constraint violation: 1062 Duplicate entry »

Si vous utilisez le FOSUserBundle sous Symfony 2.5 et que vous avez modifé votre formulaire d’inscription en redéfinissant le « RegistrationFormType ». En essayant de vous inscrire 2 fois avec la même adresse email, à la place d’une jolie erreur vous allez avoir droit à un gros plantage avec l’erreur « … Integrity constraint violation: 1062 Duplicate entry … »

Arrêtez de vous arrachez les cheveux vous n’y êtes pour rien, cela vient d’un bug de la version 2.5 de Symfony. Pour le corriger il faut passer l’api validation du framework en version 2.4 comme ceci :

Dans le fichier /app/config/config.yml

1
2
framework:
    validation: { enabled: true, api: 2.4, enable_annotations: true }

Réponse trouvé dans cette discussion : https://github.com/FriendsOfSymfony/FOSUserBundle/issues/1516

Symfony et « csrf token invalid », formulaire ok en local mais pas sur serveur

J’ai été confronté à un problème avec symfony (et le FOSUserBundle), les formulaires d’enregistrement et de connexion indiquaient « csrf token invalide » sur le serveur alors que tout fonctionnait en local sous wamp.

Après pas mal de recherche il s’avère que cela est le cas sous les serveurs OVH ayant à la racine un fichier .ovhconfig, je n’ai pas d’explications mais le fait de changer dans ce fichier la version de php (la passé de 5.4 à 5.5) résout le problème.

Mémo : Ligne de commande Symfony

Quelques ligne de commandes utile pour symfony. A compléter.

Générer un bundle:
php app/console generate:bundle --namespace=Acme/HelloBundle --format=yml

Générer les entités de l’application « Acme » :
php app/console doctrine:generate:entities Acme

Met à jour la structure de la base de données en fonction des entités :
php app/console doctrine:schema:update --force

Vider le cache :
php app/console cache:clear

Liste des routes :
php app/console router:debug

Générer un controlleur CRUD (listing, modification, suppression, ajout) d’une entité Doctrine :
php app/console generate:doctrine:crud --entity=AcmeBlogBundle:Post

Mettre à jour les dépendance avec composer :
\dans\ma\racine \chemin\vers\composer.phar update

Mettre a jour un seul élément avec composer

php C:\wamp\www\composer.phar update lenom/du-bundle

FOSUserBundle : Redirection aprés inscription

Pour avoir recherché un moment la solution pour rediriger un utilisateur après la création de son compte sous symfony (version 2.4) avec le FOSUserBundle sans trouver le résultat convenable, voici la solution :

Création du fichier \Acme\MemberBundle\EventListener\RegistrationConfirmListener.php dans votre UserBundle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace Casting\MemberBundle\EventListener;

use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\UserEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
 * Redirection aprés enregistrement d'un utilisateur
 */

class RegistrationConfirmListener implements EventSubscriberInterface
{
    private $router;

    public function __construct(UrlGeneratorInterface $router)
    {
        $this->router = $router;
    }

    /**
     * {@inheritDoc}
     */

    public static function getSubscribedEvents()
    {
        return array(
                FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationConfirm'
        );
    }

    public function onRegistrationConfirm(\FOS\UserBundle\Event\FormEvent $event)
    {
        $url = $this->router->generate('nom_de_ma_route');

        $event->setResponse(new RedirectResponse($url));
    }
}

et dans Acme\MemberBundle\Resources\config\services.yml

1
2
3
4
5
6
services:
    redirect_register_user.registration.completed:
        class: Casting\MemberBundle\EventListener\RegistrationConfirmListener
        arguments: [@router]
        tags:
            - { name: kernel.event_subscriber }

via

Initialiser l’environnement wordpress dans un script externe

Il arrive de devoir utiliser les constantes, variables, fonctions et méthodes de l’environnement wordpress et de son thème sur un script à part. Pour valider un paiement par exemple via le fichier appelé par la banque, ou encore la création d’une vraie tache cron.

Voici les quelques lignes à placer en début de fichier. Pour charger le bon thème en mode multisite voir la dernière ligne.

1
2
3
4
5
6
7
//Indiquer le dommaine
$_SERVER['HTTP_HOST'] = 'www.monsite.fr';
//chargement de l'environnement, indiquer le chemin du fichier wp-load.php de la racine de wordpress
$wp_load_loc = "/chemin/du/fichier/wp-load.php";
require_once($wp_load_loc);
//en multisite, pour passer sur le bon environnement si besoin
switch_to_blog(3);// Indiquer l'identifiant du blog souhaité évidement.

via

Mémo : barre admin de WordPress

Afficher la barre admin seulement pour les utilisateurs administrateurs :

1
2
3
4
5
6
7
8
9
10
add_filter('show_admin_bar', 'bar_is_admin');

function bar_is_admin() {
    $user = wp_get_current_user();
    if ($user->roles[0] == 'administrator') {
        return true;
    } else {
        return false;
    }
}

La barre est appelé via la fonction « wp_footer(); » juste avant la fermeture de la balise « </body> »