Résultats de recherche pour Symfony

Symfony 2 : avantages et inconvénients

Après bon nombre de commentaires, voire d’insultes (si si), je résume l’article qui suit :


Vous allez toujours entendre le même discours pour ceux qui commencent avec Symfony et qui n’ont jamais vraiment développé d’autres choses, qui est quelque part logique : Symfony c’est génial, c’est super, c’est la réponse à la vie, l’univers, et au reste, il aurait dû s’appeler « Php – 42 » ou quelque chose comme ça (et si vous ne comprenez pas l’allusion c’est qu’on n’a ni le même background, ni le même humour). C’est, de plus, très souvent le genre de personnes qui n’admettent pas qu’ils n’ont pas d’expérience et cela finit à l’insulte ou au pugilat.

S’ils avaient vraiment pratiqué d’autres choses, il verraient qu’il y a plein d’autres trucs ailleurs, et ils verraient qu’à d’autres endroits il y a des choses mieux.

Ceux qui n’ont pas d’expérience mais l’esprit ouvert me demandent ce qu’il y a comme autres choses à tester, et je leur réponds avec plaisir ! Ceux qui n’ont pas d’expérience et ont besoin de justifier leur existence diront « c’est un gros con il est dépassé il n’y comprend rien ». Je ne donne des cours que pour les premiers. Les autres je les laisse faire alt-tab en permanence entre Facebook et Symfony (et si vous saviez tout ce que je pense quand je parle ainsi…).


Après avoir développé deux applications relativement basiques, voici les inconvénients que j’ai relevé de Symfony :

  • Twig : c’est la seule chose qui semble vraiment pratique, utile et optimisée dans Symfony. A tel point que Zend l’utilisera comme moteur de templates, et Drupal 8 aussi. Un bon point pour Symfony, et le seul. Voici la suite.
  • Symfony prône les bonnes pratiques – ce qui est louable – pourtant j’ai dû faire trois fois du copier coller pour que ça fonctionne… Exemple le plus frappant : la bonne pratique (louable) est de séparer totalement la vue du modèle, et du contrôleur. Mieux : ils ont fait un endroit où on écrit toutes les requêtes complexes, afin de les séparer du modèle : ce sont les Repositories. Problème concret : quand on a un type fichier dans un formulaire qui est envoyé, le type « file » est posté. Si on veut écrire le fichier envoyé, ça passe par un contrôleur, et c’est lui qui est censé l’écrire en base de données ? Non ! Donc c’est côté de la base de données, côté modèle donc. Mais… les entités ne peuvent pas accéder à leur propre repository ! Si vous vous retrouvez dans ce cadre (hypra courant) alors vous allez devoir bidouiller, et créer une fonction statique qui renvoie le manager d’entités et demander à ce dernier de récupérer le repository de la classe en cours. Véridique. Bidouillage, bidouillage, bidouillage. Pourtant, j’insiste : Symfony prône les bonnes pratiques et essaie de faire au mieux, mais il échoue assez… attendez je cherche le qualificatif exact… j’ai trouvé : il échoue lamentablement.
  • Je pensais gagner du temps avec le bundle FOSUser. Faux ! En théorie il est censé simplifier la vie à mort et faire gagner énormément de temps. En pratique, c’est comme Symfony, il va vous falloir entrer dans son coeur, comprendre comment il a été écrit afin de pouvoir vous en servir. Exemple concret là aussi : il n’est fait que pour un seul type d’utilisateurs. Si on en veut plusieurs, il va vous falloir installer un bundle supplémentaire : le PUGXMultiUserBundle. Quelques heures plus tard après l’installation et la configuration, on m’a demandé de créer un rôle modérateur, qui doit pouvoir valider les fiches d’inscription, c’est à dire pouvoir afficher n’importe quelle fiche. Je ne peux même pas vous expliquer comment faire, car j’ai demandé aux experts Symfony – ce sont des personnes qui m’ont sous-traité le second travail que j’ai fait sur Symfony, de le faire eux même parce que je n’en pouvais plus de perdre autant de temps inutilement. J’ai beau chercher, mais un bon développeur Php va perdre beaucoup plus de temps à rechercher tout ça, installer, comprendre le fonctionnement, copier coller puis modifier le code, plutôt que de le faire direct à la main, avec des variables dans $_SERVER pour suivre quelques infos.
  • Symfony et le redimensionnement d’images. Je voulais juste, une fois qu’une image est arrivée en base de données, créer une vignette de cette dernière, afin de pouvoir l’afficher plus rapidement. J’ai donc cherché et voici le résultat – vous noterez bien qu’il est affligeant si on veut simplement redimensionner une image : en théorie  :
    • il faudrait mettre en place un service,
    • à partir de ce service, aller chercher des informations,
    • mettre en place toute une configuration, avec un système de hook (les « écouteurs »)

    … bref, refaire des milliers de choses d’une inutilité absolument affligeante, tout ça pour faire un simple resize. Après avoir passé plusieurs heures à installer le LiipImagineBundle : et m’être aperçu que pour faire un resize, il fallait déclarer vouloir utiliser un service dans ‘app/config/config.yml’, je vois qu’il y a un exemple qui est censé fonctionner sur stackoverflow ici. Et puis là le code qui fonctionne utilise getRequest(). Et le container. Moi j’en avais besoin dans l’entité, car c’est ici qu’on écrit les informations en base de données. Pour pouvoir accéder à un container dans une entité, il faut faire un méga hack, ou pire (lire les deux réponses ici). La seule réponse viable c’et de.. créer un service ! Non mais allô quoi ! Juste pour un crop ! Vraiment on est en plein délire. Hop, top chrono : google =» php imageresize, copier coller, et en moins de 5 minutes, tout fonctionnait. Symfony clame haut et fort qu’il vous fait gagner des heures de boulot ? C’est une blague, une grosse blague ! Posez les pieds sur terre, voici la réalité concrète du terrain : Symfony essaie de pousser encore et encore l’abstraction à son maximum mais on arrive à des non-sens comme celui-là : une entité qui appelle deux services différents, surchargés à mort, des centaines de lignes de code totalement inutiles, juste pour faire un resize. C’est tellement grossier comme problème qu’on en arriverait presque à croire que c’est faux… mais c’est vrai, et les liens que j’ai mis sont bien là pour le prouver.

  • Doctrine est quelque chose… que je ne comprends pas. Il a été crée pour « optimiser » les requêtes. Déjà, rien que d’écrire ça, c’est inévitablement un non sens : on n’optimise pas les requêtes via du Php… À moins que je ne me trompe, on n’a jamais besoin d’optimiser les requêtes à ce niveau ! L’optimisation et la mise en cache doit se faire au niveau du moteur de bases de données. Pourtant, là, on doit écrire du SQL, mais pas vraiment. Olivier arrête de dire n’importe quoi me direz vous. Pourtant ça n’est pas une blague, lisez tout ça ici. Tout y est expliqué : ils ont « surchargé » l’écriture de requêtes, afin de… pouvoir optimiser les requête et mieux faire du cache. Si si. Et ils ont même inventé un concept super novateur, que personne ne connaissait avant (grincement de dents cynique), j’ai nommé : l’hydratation (ou en anglais : « hydration »). Non non, j’insiste, arrêtez de rigoler, c’est pas une blague, ils prennent ça très au sérieux là bas, regardez sur la documentation officielle et cherchez « hydration ». Par contre pour bosser il faut absolument savoir ce que ce mot d’une débilité profonde – au sens informatique – signifie : j’ai mené un entretien d’embauche avec une personne (qui se disait expert), et comme je n’ai pas parlé d’hydratation des données, ça ne lui a pas plu. Bon quand je dis « expert », il faut dire qu’il s’était fait dégager à grands coups de pieds au derrière par sa précédente boîte… j’arrête là je vais être méchant 😉
  • Avec Doctrine, on se prend tout le temps la tête sur les requêtes, ce qui fait qu’au final, comme jamais rien ne fonctionne comme on voudrait avec Doctrine, on se retrouve à vouloir connaitre quelle est la vraie requête faite en base. Voir ici. Réponse officielle : on ne peut pas. On ne peut pas voir les requêtes qui sont envoyées au moteur de base de données. Mais si continuez de lire, arrêtez de pleurer de rire ! Allez vous chercher un mouchoir, essuyez-vous les yeux et revenez.
  • Doctrine est censé simplifier la vie en proposant un modèle d’héritage. Exemple de ce que j’ai réalisé : une personne est la classe de base, et de là descendent les professeurs et les étudiants. Pourtant il est lourd. Très lourd. Très (très³²) lourd. Tellement lourd, que même sur la documentation officielle ils reconnaissent que Doctrine est lourd. La preuve sur la documentation officielle. En fait il est censé simplifier la vie du développeur, mais avec Doctrine il faut une machine énorme pour pouvoir faire tourner la moindre requête un tant soit peu complexe.
  • Pire. Encore bien pire (si c’est possible !). Si, comme moi, vous voulez utiliser des requêtes comprenant des calculs de distance, il vous faudra utiliser des fonctions mathématiques et là, de base, Doctrine ne connait rien, et il vous faudra passer quelques heures à installer un bundle dans votre installation, en modifiant pas mal de fichiers un peu partout. Si, là aussi, essuyez vos larmes de rire et lisez bien ce qui suit, car c’est vrai : il est impossible de faire immédiatement cette requête sous Symfony : "SELECT COS(5) as distance;". Non non ça n’est pas une blague, c’est du sérieux. Voir tout mon article détaillé ici.
  • Symfony est tellement complexe qu’il faut absolument avoir un débogueur intégré tel que xdebug et pouvoir faire du pas à pas dans un environnement tel que PhpStorm qui donne la possibilité de suivre tout, avec la pile d’appel. Rendez vous compte : pour développer un site Web simple, des frameworks comme symfony sont tellement complexes qu’il faut obligatoirement pouvoir faire du pas à pas. Ce sont des experts Symfony qui me l’ont expliqué. Sur le coup j’ai sincèrement (honnêtement, ce n’est pas ironique, c’est véridique) cru que c’était une blague. Php est tellement simple et fluide quand il est bien développé que je n’ai jamais eu à utiliser de débogueur pas à pas en plus de dix ans ! Et ma dernière prestation était en tant qu’expert Web chez Business & Décision, prestation pour la banque, et on n’a jamais eu besoin de faire du pas à pas ! D’ailleurs pourquoi je me justifie ? N’importe quel béotien doit se douter de ça…

Je n’ai pas le temps de lister toutes les autres choses qui m’ont fait perdre un temps fou. J’ai vendu un site que je comptais faire en maximum dix jours, et je l’ai fait en un mois. Symfony était tout bonnement un mauvais choix.

En conclusion :

Symfony est censé faire gagner du temps mais en pratique, on est obligé d’apprendre la globalité de tout le framework et au final on ne gagne absolument pas de temps, et bien pire : si le projet est petit, il faut à tout prix éviter des usines à gaz telles que Symfony.

Pour la petite note, les personnes qui m’ont sous traité le projet – qui tourne bien actuellement – ont été déçues parce que je n’ai pas tenu mes délais, et que mes premières livraisons n’étaient pas de bonne qualité – alors que je « semblais » compétent. Je ne jette pas tout sur Symfony, mais bon sang quelle grossière erreur de ma part ! Pour vous donner une idée de comparaison : j’ai terminé en 3 jours un site qui devrait être mis en ligne incessamment sous peu : un site spécialisé la constatation fiable de sites Web. Il contient :

  • Un formulaire d’inscription contenant tout ce qu’il faut (CSRF protection, re-Captcha etc) ;
  • Un CSS full responsive ;
  • Un blog WordPress (évidemment full responsive) ;
  • Un outil complet de capture d’écran basé sur du Webkit (très gros boulot) ;
  • Du code pour gérer un multi-partenariat en marque blanche ;
  • Du templating Smarty pour modifier l’habillage rapidement ;
  • Une documentation entièrement compatible PhpDocumentor ;
  • Une classe (assez longue) pour le traitement des images (archivage, signature chiffrée etc) ;
  • Un ORM qui :
    • ne fait des requêtes que quand c’est nécessaire,
    • fait des requêtes en SQL pur. Ouf, je respire, des heures entières de gagnées.
    • délègue au maximum à la base de données, ce qui est absolument vital pour avoir une application stable, rapide et pérenne sur le temps.
  • Une classe simple d’envoi de mail, avec la possibilité d’envoyer le texte alternatif (= si jamais le client ne peut pas lire du HTML, on peut préciser le texte). Merci PHPMailer, un outil simple, ultra facile d’utilisation, et utilisé dans ma classe, finit par un code ainsi :
    $mail = new ObjetMail(
        EMAIL_DEFAUT_FROM,
        EMAIL_DEFAUT_FROM_NAME,
        EMAIL_DEFAUT_REPLYTO,
        EMAIL_DEFAUT_REPLYTO_NAME,
        $email,
        'Capture bien prise',
        /*... Texte HTML ...*/,
        /*... Texte alternatif ...*/
    );
    try {
        $mail->send();
        array_push(
            $this->_TabResult,
            $this->trad->get(
                'mail_envoye_en_attente_confirmation'
            )
        );
    } catch (Exception $e) {
        $this->addTabErr(
            $this->trad->get('err_envoi_email')
        );
        $ok = false;
    }
  • Tout est tracé et archivé, aussi bien en base que dans les captures d’écran, et tout est prévu pour une grosse évolution : un compte, ce compte pourra avoir plusieurs emails, plusieurs captures, toutes les connexions – réussies ou échouées – sont déjà tracées, ils pourront avoir une ou plusieurs adresses etc.

Faire la même chose avec Symfony ? Je vous laisse estimer le temps de faire les estimations, le cahier des charges, le devis, le développement, mais attention, grâce à Symfony vous allez gagner énormément de temps : Symfony peut générer le CRUD pour l’administration en ligne de commande, regardez ici. Oulah que de temps gagné ! Sachant que je génère un custom getter-setter en 3 touches avec vim, même s’il y a plusieurs centaines de champs, et plusieurs dizaines de tables, je ferais ça « à la main » au maximum en une journée… enfin bon en comparant avec Symfony je peux dire sans éxagérer : sans Symfony, une journée de perdue, mais dix d’économisées ! Mais pourquoi je continue à essayer de prouver que Symfony ne fait jamais gagner de temps, moi ? Même ces personnes expertes Symfony ont décidé de le laisser tomber et de n’utiliser que Silex pour tous leur nouveaux projets à moins qu’ils n’aient vraiment besoin de choses spécifiques à Symfony… et personnellement je n’arrive pas à voir ce que Symfony a de si spécifique dont on ne peut pas se passer 🙂 … A l’inverse, je vois vraiment toutes les raisons pour lesquelles on peut se passer de Symfony.

Symfony 2 Embedded Forms: Catchable Fatal Error: Argument 1 passed to Entity::addProperty must be an instance of XX\MyClass, array given

Encore une abstraction qui fuit.

Si jamais vous avec un problème avec un ManyToMany et que vous rencontrez une erreur du type : Symfony 2 Embedded Forms: Catchable Fatal Error: Argument 1 passed to Entity::addProperty must be an instance of XX\MyClass, array given, c’est que vous avez peut-être le même problème que moi, à savoir : une mauvaise déclaration de votre type de formulaire.

Enfin… mauvaise… disons qu’il manque des choses, et que logiquement, tant qu’on n’a pas une connaissance parfaite des rouages de Symfony, on ne comprend pas pourquoi ça ne fonctionne pas.

Je vais donc vous dire ce qu’il me manquait : j’avais une table personne et une table adresse.
Ces deux tables étaient reliées par une relation ManyToMany : une personne peut avoir une ou plusieurs adresses, et à une même adresse, plusieurs personnes peuvent habiter (heureusement ! 😉 ).

Dans la déclaration du type d’adresse, tout semblait cohérent, il n’y avait aucune erreur, mais il manquait une seule chose : la fonction setDefaultOption(). Je vous la fais courte : le code qui suit fonctionne.

Même si cette astuce ne fonctionne pas pour vous, essayez de suivre mon raisonnement : j’ai refait complètement un projet en partant de zéro, en suivant les instructions ici (Symfony 2 : Comment imbriquer une Collection de Formulaires) : et ensuite j’ai comparé pas à pas avec mon code. Pour en arriver aux six petites lignes qui manquaient : la fonction setDefaultOption().

<?php

namespace MaSociete\Bundle\MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;

class AdresseType extends AbstractType
{
   /**
    * Builds the form.
    *
    * This method is called for each type in the hierarchy
    * starting form the top most type. Type extensions can
    * further modify the form.
    *
    * @see FormTypeExtensionInterface::buildForm()
    *
    * @param FormBuilderInterface $builder The form builder
    * @param array                $options The options
    */
    public function buildForm(
        FormBuilderInterface $builder,
        array $options
    ) {
        $builder->add('adresse1', 'text')
            ->add('adresse2', 'text')
            ->add('cp', 'text')
            ->add('ville', 'text');
        }
    public function setDefaultOptions(
        \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver
    ) {
        $resolver->setDefaults(array(
            'data_class' => 'MaSociete\Bundle\MyBundle\Entity\Adresse',
        ));
    }
    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'MaSociete\Bundle\MyBundle\Entity\Adresse',
        );
    }
    public function getName()
    {
        return 'adresse';
    }
}

Symfony 2 Notice: Undefined offset: 0 in vendor/…/Hydration/SimpleObjectHydrator.php line 103

Voici un petit problème que j’ai résolu.

Supposons que vous ayez plusieurs entités qui descendent d’une seule.
Dans l’entité mère, vous précisez ce qui va différencier les autres via la propriété DiscriminatorColumn :

/**
 * @ORM\Entity
 * @ORM\Table(name="table_de_base")
 * @ORM\InheritanceType("JOINED")
 * @ORM\Entity(
 *   repositoryClass="SOCIETE\Bundle\MegaSuperBundle\Repository\PersonRepository"
 * )
 * @ORM\DiscriminatorColumn(name="person_type", type="integer")
 * @ORM\DiscriminatorMap({"1" = "PersonneBoss", "2" = "PersonneLarbin"})
 */
abstract class Personne extends BaseUser
{
...
}

Cela signifie en clair : l’entité «PersonneBoss», descendante de «Personne», aura aura la valeur «1» dans le champ person_type, et l’entité descendante «PersonneLarbin», de «Personne», aura la valeur «2» dans le champ person_type. C’est, comme son nom l’indique, un «discriminateur».

Si jamais vous vous retrouvez avec une erreur de ce type : Notice: Undefined offset: 0 in vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php line 103, alors il se peut, comme c’était le cas pour moi, que les valeurs pour le «discriminateur» n’aient pas été correctement renseignées : dans mon cas, les valeurs étaient soit null, soit 0 et en fait cette valeur sert apparemment d’indice dans une table afin de pouvoir faire la jointure avec les descendants : donc il essayait de chercher une table avec l’indice 0, alors que je n’ai précisé que 1PersonneBoss») ou 2PersonneLarbin»)… donc pour la valeur 0, forcément, Symfony sort un Notice: Undefined offset: 0.

Bref, si vous avez une erreur comme celle-là, vérifiez que les valeurs en base de données sont cohérentes avec vos déclarations !

symfony 2 : isset et null avec Twig

Une des questions qu’on cherche le plus souvent avec Twig, c’est comment vérifier si une variable existe ou pas ?

La réponse se fait en trois étapes :

  1. Vérifier si une variable existe (= si elle a été initialisée) :
    En php, c’est isset().
    En Twig, c’est defined.
    Exemple de code Twig :

    {% if app.user is defined %}
    Code html
    {% endif %}
  2. Vérifier si une variable n’est pas null (= elle est définie, mais contient la valeur null) :
    En php, c’est !is_null().
    En Twig, c’est is not null.
    Exemple de code Twig :

    {% if app.user is not null%}
    Code html
    {% endif %}
  3. Vérifier si une variable existe et qu’elle n’est pas null (= elle est définie, et contient autre chose que la valeur null) :
    En php, c’est isset() && (!is_null()).
    En Twig, c’est is defined and is not null.
    Exemple de code Twig :

    {% if app.user is defined and app.user is not null%}
    Code html
    {% endif %}

En espérant que cela aide du monde !

Symfony: The form’s view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is…

J’ai eu l’erreur qui est le titre de mon post.

Pourtant, j’avais tout fait correctement. J’avais mis en place une collection ManyToMany simple : un partenaire peut être relié à une ou plusieurs personnes et une personne peut être reliée à un ou plusieurs partenaires.

Sur symfony 2.1, tout fonctionnait correctement, et j’ai fait la mise à jour sur symfony 2.3 (pour rester à la pointe 😉 ) mais là une erreur apparaissait :

« The form’s view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class HQF\Bundle\PizzasBundle\Entity\Personne. You can avoid this error by setting the « data_class » option to « HQF\Bundle\PizzasBundle\Entity\Personne » or by adding a view transformer that transforms an instance of class HQF\Bundle\PizzasBundle\Entity\Personne to scalar, array or an instance of \ArrayAccess. »

Après un bon moment de recherche, j’ai trouvé ma solution ici.

Dans mon bundle j’avais mon fichier « Bundle/Form/Type/PartenaireType.php » dans lequel j’avais déclaré tout comme il fallait :

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('nom',                    'text');
        $builder->add('site_actif',             'checkbox');
        $builder->add('afficher_img_produits',  'checkbox');
        $builder->add('commande_en_ligne',      'checkbox');
        $builder->add('livraison',              'checkbox');
        $builder->add('livraison_delai',        'integer');
        $builder->add(
            'personnes',
            'collection',
            array(
                'type' => new PersonneType(),
            )
        );
    }

Tout était parfait, et fonctionnait sous Symfony 2.1. Mais lors de la mise à jour, ça ne fonctionnait plus. Il manquait cela : préciser à nouveau que la PersonneType était liée à un type Personne :

'options' => array('data_class' => 'HQF\Bundle\PizzasBundle\Entity\Personne')

Donc dans mon code ça donne ça :

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('nom',                    'text');
        $builder->add('site_actif',             'checkbox');
        $builder->add('afficher_img_produits',  'checkbox');
        $builder->add('commande_en_ligne',      'checkbox');
        $builder->add('livraison',              'checkbox');
        $builder->add('livraison_delai',        'integer');
        $builder->add(
            'personnes',
            'collection',
            array(
                'type' => new PersonneType(),
                'options' => array(
                    'data_class' => 'HQF\Bundle\PizzasBundle\Entity\Personne'
                ),
            )
        );
    }

Un grand merci à Stackoverflow, sinon j’aurais mis bien plus de temps à m’en sortir, je pense !

Symfony 2 : mon site de démonstration est sorti : livrepizzas

Mon site de démonstration pour l’utilisation du framework Symfony 2 est sorti.

http://www.livrepizzas.fr

Ce n’est qu’un site de démonstration, il est loin d’être parfait !

  • Bien sûr il est optimisé pour google et pour les mobiles et tablettes.
  • Bien sûr il est compatible w3c.
  • Bien sûr il est moderne et utilise du HTML5.
  • Bien sûr il est en responsive design.
  • Bien sûr c’est facile à faire. 🙂

Si vous avez des commentaires ou suggestions à faire, n’hésitez pas !

Symfony 2 et Doctrine et repository : faire un leftJoin avec createQueryBuilder

Comment utiliser createQueryBuilder() ?

La documentation n’est pas très claire sur le sujet. Enfin, disons que si vous êtes comme moi, il va vous manquer des exemples pour mieux comprendre. Je vais essayer de vous faire gagner du temps.

Voilà le problème : j’ai crée un repository pour mes partenaires, que j’ai appelé PartenaireRepository.php.

Dans la plupart des exemples, ils utilisent createQueryBuilder('p'), qui semble pratique (puisqu’il référence immédiatement la table et en fait un alias (dans mon exemple c’est p)

J’ai donc voulu utiliser createQueryBuilder() mais j’ai eu besoin deux jointures d’affilée : les partenaires avaient une ou plusieurs adresses, et ces adresses étaient réliées à des villes. La solution est en fait simple, à partir du moment où on a compris le principe :

    class PartenaireRepository extends EntityRepository
    {
        /**
         * Récupération de tous les partenaires donnés pour un
         * code postal donné.
         */
        public function findAllActiveByCp($cp)
        {
            return $this->createQueryBuilder('p')
                ->leftJoin('p.adresses', 'a')
                ->leftJoin('a.ville', 'v')
                ->where('v.cp=:cp')
                ->setParameter('cp', $cp);
        ... blabla
        }
    }

Ici, le leftJoin('p.adresses', 'a') signifie : dans la classe Partenaire que j’ai déclarée dans le fichier Entity\Partenaire.php, il y a la propriété adresses et tu vas faire une jointure dessus, et cette jointure, tu vas l’aliaser "a". On aura donc, à partir de cette jointure, une référence à une table adresse qu’on pourra utiliser via le "a".

Il est possible de refaire une jointure avec cet alias !

La preuve : la jointure juste après : ->leftJoin('a.ville', 'v') qui signifie, exactement sur le même principe : dans le fichier Entity\Adresse.php, il y a la propriété "ville" et tu vas faire une jointure dessus, et cette jointure, tu vas l’aliaser "v".

Enfin, je termine sur le "where" classique :

->where('v.cp=:cp').

D’après ce que j’ai compris, on ne peut faire des jointures que sur des propriétés qui sont elles même déclarées en tant que jointures. Donc, sur mon fichier « entité » Partenaire, ma jointure est déclarée ainsi :

/**
 * @ORM\ManyToMany(targetEntity="Adresse")
 * @ORM\JoinTable(name="partenaire_adresse",
 *      joinColumns={@ORM\JoinColumn(name="id_partenaire", referencedColumnName="id")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="id_adresse", referencedColumnName="id")}
 *      )
 */
private $adresses;

Et de la même façon, sur mon fichier « entité » Ville, ma jointure est déclarée ainsi :

/**
 * @var string
 *
 * @ORM\ManyToOne(targetEntity="Ville")
 * @ORM\JoinColumn(name="id_ville", referencedColumnName="id")
 */
private $ville;

J’espère vous avoir fait gagner du temps, parce que pour moi, la syntaxe n’était pas évidente à trouver.

Symfony 2 et Doctrine: comment ajouter les fonctions spécifiques MySQL

Imaginons que vous fassiez un Repository dans lequel vous essayez d’éxecuter une fonction ainsi :

    <?php
    public function findNearest(...) {
        return $this->createQueryBuilder('p')
            ->select(array(
                'p.id',
                'COS(5) as distance'
            ))
            ->having('distance>:dmin')
            ->addOrderBy('distance', 'DESC')
            ->setParameters(array(
                'dmin' => $distance_min,
            ));
    }

Alors vous verrez que Doctrine ne connait pas la fonction cosinus COS().
Vous aurez l’erreur :

[Syntax Error] line 0, col 27: Error: Expected known function, got 'COS'

Doctrine ne connait pas la fonction cosinus COS(), mais aussi plein d’autres fonctions.

Pour les avoir disponibles, il faut aller les chercher sur github, ici.

Ici, vous pourrez récupérer toutes ces fonctions : DateAdd(), Sha1(), NullIf(), Field(), Month(), Sin(), MatchAgainst(), Acos(), Day(), CountIf(), GroupConcat(), Radians(), TimestampDiff(), ConcatWs(), Cot(), IfElse(), FindInSet(), IfNull(), Cos(), Asin(), Md5(), Year(), Sha2(), Week(), CharLength(), DateDiff(), StrToDate(), Atan(), Tan(), Atan2() et Degrees()

Ensuite, comme je ne voulais pas tous les fichiers, mais que les extensions, je les ai toutes mises dans mon répertoire dédié : src/HQF/Bundle/PizzasBundle/DQL/MySQL/.
Puis j’ai modifié à la main tous les fichiers en les déclarant dans le namespace adéquat (namespace HQF\Bundle\PizzasBundle\DQL\MySQL;).

Une fois cela fait, j’ai ajouté les déclarations de ces fonctions dans le fichier de config général app/config/config.yml. La petite astuce très importante et qui m’a fait perdre un temps fou (et c’est pour ça que je fait cet article en fait), qui n’est pas très claire dans la documentation : le nom qu’on déclare doit correspondre au texte qu’on trouve dans la fonction. Donc, dans le fichier, on a [nom de la fonction]:[classe] :

J’essayais de déclarer mafonction_acos: HQF\Bundle\PizzasBundle\DQL\MySQL\Acos et ça ne fonctionnait pas. Il fallait donner le nom de la fonction : acos: HQF\Bundle\PizzasBundle\DQL\MySQL\Acos

doctrine:
    ...blabla...
    orm:
        default_entity_manager: default
        entity_managers:
            default:
                connection: default
                ...blabla...
                dql:
                    numeric_functions:
                        acos: HQF\Bundle\PizzasBundle\DQL\MySQL\Acos
                        asin: HQF\Bundle\PizzasBundle\DQL\MySQL\Asin
                        atan2: HQF\Bundle\PizzasBundle\DQL\MySQL\Atan2
                        atan: HQF\Bundle\PizzasBundle\DQL\MySQL\Atan
                        charlength: HQF\Bundle\PizzasBundle\DQL\MySQL\CharLength
                        concatws: HQF\Bundle\PizzasBundle\DQL\MySQL\ConcatWs
                        cos: HQF\Bundle\PizzasBundle\DQL\MySQL\Cos
                        cot: HQF\Bundle\PizzasBundle\DQL\MySQL\Cot
                        countif: HQF\Bundle\PizzasBundle\DQL\MySQL\CountIf
                        dateadd: HQF\Bundle\PizzasBundle\DQL\MySQL\DateAdd
                        datediff: HQF\Bundle\PizzasBundle\DQL\MySQL\DateDiff
                        day: HQF\Bundle\PizzasBundle\DQL\MySQL\Day
                        degrees: HQF\Bundle\PizzasBundle\DQL\MySQL\Degrees
                        field: HQF\Bundle\PizzasBundle\DQL\MySQL\Field
                        findinset: HQF\Bundle\PizzasBundle\DQL\MySQL\FindInSet
                        groupconcat: HQF\Bundle\PizzasBundle\DQL\MySQL\GroupConcat
                        ifelse: HQF\Bundle\PizzasBundle\DQL\MySQL\IfElse
                        ifnull: HQF\Bundle\PizzasBundle\DQL\MySQL\IfNull
                        matchagainst: HQF\Bundle\PizzasBundle\DQL\MySQL\MatchAgainst
                        md5: HQF\Bundle\PizzasBundle\DQL\MySQL\Md5
                        month: HQF\Bundle\PizzasBundle\DQL\MySQL\Month
                        nullif: HQF\Bundle\PizzasBundle\DQL\MySQL\NullIf
                        radians: HQF\Bundle\PizzasBundle\DQL\MySQL\Radians
                        sha1: HQF\Bundle\PizzasBundle\DQL\MySQL\Sha1
                        sha2: HQF\Bundle\PizzasBundle\DQL\MySQL\Sha2
                        sin: HQF\Bundle\PizzasBundle\DQL\MySQL\Sin
                        strtodate: HQF\Bundle\PizzasBundle\DQL\MySQL\StrToDate
                        tan: HQF\Bundle\PizzasBundle\DQL\MySQL\Tan
                        timestampdiff: HQF\Bundle\PizzasBundle\DQL\MySQL\TimestampDiff
                        week: HQF\Bundle\PizzasBundle\DQL\MySQL\Week
                        year: HQF\Bundle\PizzasBundle\DQL\MySQL\Year

Symfony 2: générer une url dynamique dans twig dans du code javascript

J’ai eu à faire face à un problème que vous allez très certainement rencontrer si vous faites Symfony2. Dans les fichiers de template, vous allez sûrement mettre du Javascript. Exemple :

<script type="text/javascript">
<!--
function verif_formulaire(){
    window.location = '/test/mon/url/';
}
-->
</script>

Maintenant, si on essaie de faire ça en Twig, c’est simple. Je ne m’attarderai que sur l’URL :

    window.location = '{{ path('my_path') }}';

Supposons que votre path nécessite un paramètre, par exemple, dans mon cas, le code postal :

    window.location = '{{ path('my_path', {'cp': "13480" }) }}';

Facile. Mais supposons que votre code en JavaScript veuille le générer dynamiquement :

/* récupération de la valeur quelque part : */
var monCP = document.getElementById('cp').value;
window.location = ="{{ path('hqf_pizzas_searchpage', {'cp': monCP }) }}";

Eh bien ça ne fonctionnera pas sur Symfony2. Ne cherchez pas c’est comme ça. Vous aurez une erreur. L’erreur, pour reprendre mon code, était :

Variable "monCP" does not exist in HQFPizzasBundle:Default:index.html.twig at line 11

Voici la solution que j’ai trouvée : je reprends ma configuration et il vous suffira de l’adapter à vos besoins. Dans mon fichier de routing src/HQF/Bundle/PizzasBundle/Resources/config/routing.yml, j’ai ce path qui nécessite le paramètre cp :

hqf_pizzas_searchpage:
    pattern:  /search/cp/{cp}
    defaults: { _controller: ... }

L’objectif est de ressortir le path avec un '%s' dedans, de manière à pouvoir avoir une URL qui ressemble à :

/search/cp/%s

Ainsi, il suffira juste après d’utiliser la fonction Twig ‘format‘ et d’y passer la variable JavaScript, par exemple monCP.
Ainsi cet ordre twig:

{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | format('monCP') }}

génèrera ceci :

/search/cp/monCP

L’objectif final est de sortir du vrai code JavaScript qui ressemble à :

window.location="/search/cp/"+monCP+"";

Donc si veut y arriver, le mélange code Twig + JavaScript, avec les guillemets, devrait être :

window.location = "{{ path('hqf_pizzas_searchpage', {'cp': "%s" })  | format('"+monCP+"') }}"

Seulement, problème : il escape tout ! Le code généré sera ainsi :

window.location ="/pizzas/search/cp/%25s";

Solution : pour que twig n’échappe pas le texte, il faut créer son propre filtre Twig !

Voici les étapes à suivre :

J’ai crée mon fichier src/HQF/Bundle/PizzasBundle/Twig/UrlDecodeExtension.php dans lequel j’ai mis ce code :

<?php

namespace HQF\Bundle\PizzasBundle\Twig;

class UrlDecodeExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return array(
            'url_decode' => new \Twig_Filter_Method($this, 'urlDecode'),
        );
    }

    public function urlDecode( $url )
    {
        return urldecode( $url );
    }

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

Ensuite, je l’ai enregistré dans les services.
C’est dans le fichier src/HQF/Bundle/PizzasBundle/Resources/config/services.yml les lignes :

services:
    cme.twig.url_decode_extension:
        class: HQF\Bundle\PizzasBundle\Twig\UrlDecodeExtension
        tags:
            - { name: twig.extension }

A partir de là le filtre url_decode fonctionne. Il suffit de faire le code qui suit :

/* récupération de la valeur quelque part */
var monCP = document.getElementById('cp').value;
window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode | format('"+monCP+"') | raw }}";

Afin de générer cela en JavaScript :

/* récupération de la valeur quelque part */
var monCP = document.getElementById('cp').value;
window.location ="/pizzas/search/cp/"+monCP+"";

Ce qui est du code JavaScript parfaitement exécutable.

Voici les explications pas à pas :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) }}";

Génère cela :

window.location ="/pizzas/search/cp/%25s";

C’est échappé, et il ne le faut pas ! Donc ajouter le filtre url_decode :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode }}";

Là le résultat sera celui attendu :

window.location ="/pizzas/search/cp/%s";

Ensuite on y ajoute la fonction format afin d’y ajouter la variable JavaScript :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode | format('"+monCP+"') }}";

Mais là encore le résultat sera échappé :

window.location ="/pizzas/search/cp/&quot;+monCP+&quot;";

Donc il faut lui dire de ressortir le résultat final au format raw :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode | format('"+monCP+"') | raw }}";

Et le résultat de sortie sera (enfin !) parfait :

window.location ="/pizzas/search/cp/"+monCP+"";

Cet article est un mélange de l’explication de création des filtres Twig, ici, et de la question qui ressemble très fortement à la mienne sur stackoverflow, ici.

En espérant que cela serve à quelqu’un, un jour 😉

Symfony 2 CheatSheet – French

Fondamentaux HTTP

Php

  • Vider le cache :
    php app/console cache:clear
  • Deboguer les routes :
    php app/console router:debug
  • Configuration avancée de routes :
    blog:
        pattern:   /blog/{page}
        defaults:  {
            _controller: AcmeBlogBundle:Blog:index,
            page: 1
        }
        requirements:
            page:  \d+
  • Voir ce qui arrive dans le post/get d’une requête :
    print_r($request->request->all());
  • Génération d’une URL :
    $uri = $router->generate(
        'blog_show',
        array('slug' => 'my-blog-post')
    );
  • Génération d’une URL absolue :
    $router->generate(
        'blog_show',
        array('slug' => 'my-blog-post'),
        true
    );
  • Changement de l’hôte :
    $router->getContext()->setHost('www.example.com');

Templating Twig

  • Commentaires :
    {# Commentaire #}
  • Afficher = double crochets :
    {{ afficher }}
  • Exécuter = crochets et «%» :
    {% exec %}
  • Afficher une variable sans l’«escaper» :
    {{ article.body|raw }}
  • Variable simple :
    {{ page_title }}
  • Déboguage d’une variable :
    {{ dump(articles) }}
  • Vérifier la syntaxe de tous les fichiers d’un bundle :
    php app/console twig:lint @HQFPizzasBundle
  • Traduire en appliquant un bundle spécifique :
    {{ message|trans({}, 'FOSUserBundle') }}
  • Boucle for :
    {% for item in navigation %}
        <a href="{{ item.href }}">
            {{ item.caption }}
        </a>
    {% endfor %}
  • Boucle for avancée :
    {% for i in 0..10 %}
        <div class="{{ cycle(['odd', 'even'], i) }}">
            <!-- some HTML here -->
        </div>
    {% endfor %}
  • Si une variable existe et n’est pas vide :
    {% if maVar is defined and maVar is not null%}
    {% endif %}
    
  • Génération d’une URL :
    {{ path('blog_show', {'slug': 'my-blog-post'}) }}
  • Génération d’une URL absolue :
    {{ url('blog_show', {'slug': 'my-blog-post'}) }}
  • Déclaration d’un descendant d’un fichier :
    {% extends '::base.html.twig' %}
  • Déclaration d’un bloc :
    {% block title %}My cool blog posts{% endblock %}
  • Ré-inclure ce qu’il y a dans le bloc parent :
    {% block sidebar %}
        <h3>Table of Contents</h3>
        {# ... #}
        {{ parent() }}
        {# ... #}
    {% endblock %}
  • Fichiers Css et Js : dans le répertoire Resources/public.
    Déploiement via la ligne de commande :

    php app/console assets:install target [--symlink]

    Pour l’environnement de développment :

    php app/console assetic:dump --symlink

    Pour l’environnement de production :

    php app/console assetic:dump --env=prod --no-debug --symlink

Doctrine

  • Dumper une requête pour voir comment elle est construite :
    $marequete->getQuery();
    print_r(array(
        'sql'        => $marequete->getSQL(),
        'parameters' => $marequete->getParameters(),
    ));
  • Générer les getter / setter :
    php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product
  • Grande ligne qui génère les getter / setter pour tout un bundle, et qui supprime tous les espaces inutiles ajoutés par les getter / setter, ainsi que les fichiers de backup :
    php app/console doctrine:generate:entities MaSocieteMonSuperBundle && find src/MaSociete/MonSuperBundle/Entity/ -type f -print0 | xargs -r0 sed -e 's/[[:blank:]]\+$//' -i && find src/MaSociete/MonSuperBundle/Entity/*~ -type f | xargs rm -f
  • Supprimer physiquement toutes les tables déclarées dans les entities :
    php app/console doctrine:schema:drop --force
  • Créer la base de données au complet :
    php app/console doctrine:schema:create