Doctrine : Recherche dans une table contenant des latitudes et longitudes celle situés à moins de « XX » km

Il y’a quelques mois je présentais une fonction PHP permettant de calculer la distance entre 2 coordonnées GPS (latitude et longitude). Coordonnées qui peuvent par exemple être récupérer via l’API Google Map

Nous allons voir dans cette article, comment récupérer toutes les enregistrements d’une table qui sont situé à moins de « 50km » d’une position précise. Il nous faut donc les coordonées de la position pour laquelle on fait la recherche, ainsi qu’une table avec les colonnes « lat » et « lng »

La formule permettant de faire le calcul (en km) directement en MySql et la suivante :

//la formule utilisant nos coordonées "$lat" et "$lng" ainsi que les colonne "lat" et "lng" de la table "table"
$sqlDistance = '(6378 * acos(cos(radians(' . $lat . ')) * cos(radians(table.lat)) * cos(radians(table.lng) - radians(' . $lng . ')) + sin(radians(' . $lat . ')) * sin(radians(table.lat))))';

Formule utilisable pour rechercher les enregistrements inférieur à 50km de notre position, via le requête suivante :

$requete = 'SELECT * FROM table WHERE '.$sqlDistance.' < 50 ';

Si l’on travaille sous Symfony et avec Doctrine, en ajoutant notre condition dans un « querybuilder » comme ci-dessous ça ne va pas fonctionner.

//Imaginons que nous sommes dans une requête classique crée avec le querybuilder $qb
//On veut rajouter la condition de la distance (en utilisant une $distance variable dans ce cas).
$qb->andWhere("" . $sqlDistance . " < :distance")->setParameter('distance', $distance);

Doctrine ne connait pas les différentes fonctions de calculs « acos », « cos », « radian » et « sin » utilisées dans la formule. Nous allons les importer grâce à la bibliothèque DoctrineExtensions. Pour cela, il suffit de faire un petit coup de composer sur notre projet :

composer require beberlei/DoctrineExtensions

Et ensuite d’indiquer les fonctions dont on à besoin dans notre configuration de doctrine de la manière suivante :

doctrine:
    orm
:
        dql
:
            numeric_functions
:
                acos
: DoctrineExtensions\Query\Mysql\Acos
                cos
: DoctrineExtensions\Query\Mysql\Cos
                radians
: DoctrineExtensions\Query\Mysql\Radians
                sin
: DoctrineExtensions\Query\Mysql\Sin

Tout doit à présente fonctionner. Vous pouvez de cette manière importer une flopée de fonctions, la configuration complète étant disponible Sur cette page

Symfony : Obtenir un enregistrement aléatoire avec doctrine

Doctrine ne permet pas par défaut d’obtenir un enregistrement aléatoire, ce qui est pourtant en principe très simple avec mysql. Par contre il est possible de faire une requête native et de demander a doctrine de « mapper » les résultats. Il devient alors facile de récupérer notre enregistrement aléatoire sous le format habituel (entité doctrine). Voici ci-dessous la méthode d’un repository permettant de la faire.

class ExempleRepository extends \Doctrine\ORM\EntityRepository {

    public function findRandom() {
        $rsm = new \Doctrine\ORM\Query\ResultSetMappingBuilder($this->getEntityManager());
        $rsm->addRootEntityFromClassMetadata(\AppBundle\Entity\Exemple::class, 'exemple');
        $sql = "SELECT * FROM exemple ORDER BY RAND() LIMIT 1";
        return $this->getEntityManager()->createNativeQuery($sql, $rsm)->getOneOrNullResult();
    }

}

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 : 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;