Archives de catégorie : Symfony

Symfony : Automatiser des actions avec les « Event listeners » de doctrine

Le framework Symfony couplé à l’ORM doctrine permet de créer des « Event Listeners », c’est à dire connaitre dés qu’il y’a un événement (création/modification/suppression) sur une entité. C’est un outil très intéressant pour automatiser des actions quand certaines propriétés d’une entité sont mis à jour.

Prenons un exemple concret : Dans les semaines précédentes, nous avons vu comment récupérer les coordonnées d’une adresse grâce à l’API google map. Afin de limiter les appels à l’API google map (payants si un certain quota et dépassé) nous allons enregistrer ces coordonnées en base de données pour pouvoir les réutiliser directement. Il nous faut cependant les enregistrer/mettre à jour à chaque fois qu’une adresse est crée ou modifiée. C’est ici que nous faisons intervenir les Event Listener.

Commençons par configurer nos listeners dans les services (fichiers services.yml du dossier /app/config/) de symfony.

services:
   #...
    #LISTENER DOCTRINE#
    app.listener.doctrineevent
:
        class
: AppBundle\EventListener\DoctrineEvent
        tags
:
            - { name
: doctrine.event_listener, event: prePersist, lazy: true }
            - { name
: doctrine.event_listener, event: preUpdate, lazy: true }

La configuration indique que nous allons écouter les événements « PrePersist » et « PreUpdate ». Ce qui permettra d’effectuer notre action lors de la création d’une adresse et lors de sa modification. Créons la classe qui va gérer tout ça. Comme indiqué il s’agit du fichier : AppBundle\EventListener\DoctrineEvent

Pour appréhender la suite, consultez les commentaires dans le code

namespace AppBundle\EventListener;

use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
//les adresses de nos utilisateur sont stockés dans une entité "Contact"
use AppBundle\Entity\Contact;

class DoctrineEvent implements EventSubscriber {

    public function getSubscribedEvents() {
        return array('prePersist', 'preUpdate');//les événements écoutés
    }

    public function prePersist(LifecycleEventArgs $args) {
        $entity = $args->getEntity();
        //Si c'est bien une entité Contact qui va être "persisté"
        if ($entity instanceof Contact) {
            $entity->updateGmapData();//on met à jour les coordonnées via l'appel à google map
        }
    }

    public function preUpdate(LifecycleEventArgs $args) {
        $entity = $args->getEntity();
        $changeset = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($entity);
        //Si c'est bien une entité Contact qui va être modifié
        if ($entity instanceof Contact) {
            //Si il y'a eu une mise a jour sur les propriétés en relation avec l'adresse (ici "address", "city" et "postalCode")
            if (array_key_exists("address", $changeset) || array_key_exists("city", $changeset) || array_key_exists("postalCode", $changeset)) {
                $entity->updateGmapData();//on met à jour les coordonnées via l'appel à google map
            }
        }
    }

}

De cette manière nos coordonnées seront toujours à jour. Il manque pour que l’exemple soit complet la fonction « updateGmapData() » de notre entité Contact. Cela ne concerne pas le sujet des Event Listener, mais je vous en met un exemple qui utilise bien sur la fonction « geocodeAddress() » détaillé ici

    public function updateGmapData() {
        $data = \AppBundle\Utils\GmapApi::geocodeAddress($this->getAddress() . ' ' . $this->getZipcode() . ' ' . $this->getCity());
        $this->setGmapLat($data['lat']);
        $this->setGmapLng($data['lng']);
        $this->setGmapAddress($data['address']);
        $this->setGmapPostalCode($data['postal_code']);
        $this->setGmapCity($data['city']);
        $this->setGmapDepartment($data['department']);
        $this->setGmapRegion($data['region']);
        $this->setGmapCountry($data['country']);
    }

Symfony : Afficher toutes les erreurs d’un formulaire dans une liste

Par défaut l’affichage des erreurs sur les formulaires dans symfony se fait de la manière suivante :

  • Les erreurs globales (qui ne concerne pas un champ particulier) sont affichés en haut.
  • Les erreurs concernant chaque champs, sont affiché au niveau du champ.

Dans certains cas, par exemple sur des longs formulaires, il peut être intéressant de retrouver la liste de toutes les erreurs au mêmes endroits. Voici ci-dessous un code permettant de le faire (pour un formulaire « form »)

{% if not form.vars.valid %}
    <ul class="alert alert-danger">
        {# on affiche en premier les erreurs globales (cas classiques) #}
        {% for error in form.vars.errors %}
            <li>{{error.message}}</li>
        {% endfor %}
        {# ainsi que les erreurs concernant chaque champs #}
        {% for child in form.children %}
            {% for error in child.vars.errors %}
                <li>"{{child.vars.label}}" : {{error.message}} </li> {# on ajoute le label du champ devant l'erreur pour plus de précisions. #}
            {% endfor %}
        {% endfor %}
    </ul>
{% endif %}

Symfony : modifier les champs d’un formulaire en fonction de son objet ou d’options

Depuis le controller, c’est un appel du formulaire classique, on ajoute juste les éventuelles options si besoins

$form = $this->createForm(ExampleFormType::class, $example, array('custom' => true));

Dans « ExampleFormType » définition de l’option « custom » et utilisation des « Form Events » pour adapter les champs

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

class ExampleFormType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use($options) {
            $example = $event->getData(); //recuperation de l'objet sur lequel le formulaire se base
            $form = $event->getForm(); //recuperation du formulaire
            //en fonction du type de l'utilisateur les champs sont différents
            if ($example->getStatus() == 1 && $options['custom']) {
                $form->add('firstName', null, array('label' => 'Prénom'));
                //....
            } else {
                $form->add('lastName', null, array('label' => 'Nom'));
                //....
            }
        });
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Example',
            'custom' => false
        ));
    }

}

Savoir si un champs est définis dans twig

{% if form.firstName is defined %}
    {{form_row(form.firstName)}}
{% endif %}

Symfony : Relations entre entités

Exemple de relation entre entités Doctrine dans Symfony

Relation « unique » (ManyToOne) entre « Child » et « School »

    //coté Child

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\School", inversedBy="children")
     * @ORM\JoinColumn(name="school_id", referencedColumnName="id", nullable=false)
     */

    protected $school;
    //coté School

    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Child", mappedBy="school")
     */

    protected $children;

Relation « multiple » (ManyToMany) entre « Child » et « Member »

    //coté Child

    /**
     * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Member", inversedBy="children")
     */

    protected $members;
    //coté Member

    /**
     * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Child", mappedBy="members")
     */

    protected $children;