apiplatform requete geolocalisé
25 avril 2020

apiplatform 1/2: récupérer les endroits les plus proches

Par ghaliano

Objectif

Mettre en place une requète géo localisé dans apiplatform vers une liste d’endroit à visiter avec mysql et les trier avec l’endroit le plus proche de ma position actuelle, j’ai choisie d’utiliser mysql pour la facilité de l’implémentation de l’exemple, par contre il existe d’autres sgbd (nosql notamment) qui prennent en charge nativement des requètes géo localisés (comme elasticSearch ou mongodb)

Outils de travail

Pour pouvoir suivre cet article une connaissance préalable des outils suivant est recommandé:

Créer l’entité Place

Après avoir installer api-platform (je ne vais couvrir l’installation ici puisqu’elle est largement documenté dans le site officiel), j’ai ajouté la seule entité de l’exemple: l’entité Place

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
* @ApiResource
*
@ORM\Entity(repositoryClass="App\Repository\PlaceRepository")
*/
class Place
{

/**
*
@ORM\Id()
*
@ORM\GeneratedValue()
*
@ORM\Column(type="integer")
*/
private $id;

/**
*
@ORM\Column(type="string", length=255)
*/
private $name;

/**
*
@ORM\Column(type="text")
*/
private $description;

/**
*
@ORM\Column(type="float")
*/
private $longitude;

/**
*
@ORM\Column(type="float")
*/
private $latitude;

public function getId(): ?int
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(string $name): self
{
$this->name = $name;

return $this;
}

public function getDescription(): ?string
{
return $this->description;
}

public function setDescription(string $description): self
{
$this->description = $description;

return $this;
}

public function getLongitude(): ?float
{
return $this->longitude;
}

public function setLongitude(float $longitude): self
{
$this->longitude = $longitude;

return $this;
}

public function getLatitude(): ?float
{
return $this->latitude;
}

public function setLatitude(float $latitude): self
{
$this->latitude = $latitude;

return $this;
}
}

La requette sql géolocalisé

Nous devons ajouter ce hack pour pouvoir détourné la requête par défaut effectué par api-platform, et exactement pour cette raison on aura besoin des DataProvider

<?php

namespace App\DataProvider;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use App\Entity\Place;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrineOrmPaginator;
use Symfony\Component\HttpFoundation\RequestStack;

final class PlaceDataProvider implements CollectionDataProviderInterface
{
    private $em;
    private $request;
    private $pageParameterName;
    private $itemsPerPageParameterName;

    public function __construct(EntityManagerInterface $em, RequestStack $request)//, string $pageParameterName, string $itemsPerPageParameterName)
    {
        $this->em = $em;
        $this->request = $request;
    }

    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return Place::class === $resourceClass;
    }

    public function getCollection(string $resourceClass, string $operationName = null)
    {
        $dataQueryParam = [];
        $queryBuilder = $this->em->createQueryBuilder()
            ->addSelect('p')
            ->from(Place::class, 'p');

        $queryBuilder->addSelect(" ( 6371 * acos( cos( radians(:latitude) ) * cos( radians( p.latitude ) ) * cos( radians( p.longitude ) - radians(:longitude) ) + sin( radians(:latitude) ) * sin( radians( p.latitude ) ) ) ) AS HIDDEN distance");

        $queryBuilder->addOrderBy("distance", "ASC");
        $dataQueryParam['latitude'] = $this->getParam('latitude');
        $dataQueryParam['longitude'] = $this->getParam('longitude');

        $queryBuilder->setParameters($dataQueryParam);

        return $queryBuilder->getQuery()->getResult();
    }

    private function getParam($param, $default = null)
    {
        return $this->request->getCurrentRequest()->get($param, $default);
    }
}

Explication du code

Le script n’est pas trop complexe, il permet de récupérer les endroits triés par ordre croissant (le plus proches au plus lointain) à un point GPS envoyé en paramètre GET

Installer les extensions doctrine manquantes

Deuxième étape consiste à ajouter les différents clauses sql de calcul de distance (cos, sin, radian) qui ne sont pas par défaut supporté par doctrine, et pour cette raison vous devez installer ce plugin

Voici les extensions dont vous aurez besoin

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

Le routage (optionnel)

Ajouter le prefix \api\

api:
resource: '.'
type: 'api_platform'
prefix: '/api'

Les fixtures (données de test)

Ajoutant quelques fixtures avec le bundle DataFixtureBundle

<?php

namespace App\DataFixtures;

use App\Entity\Product;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class PlaceFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        $places = [
            [
                "name" => "Djerba",
                "description" => "Ile de Djerba",
                "latitude" => 33.77, 339055,
                "longitude" => 10.885904100766005
            ], [
                "name" => "Zembra",
                "description" => "Ile de Zembra",
                "latitude" => 37.12, 9625950000005,
                "longitude" => 10.803341508644678
            ], [
                "name" => "Ile de Galite",
                "description" => "Ile de Galite",
                "latitude" => 37.5274008,
                "longitude" => 8.9340395
            ], [
                "name" => "kerkennah",
                "description" => "Ile de kerkennah",
                "latitude" => 34.64, 349125,
                "longitude" => 11.037559016310981
            ]];

        $manager->flush();
    }
}

j’ai utilisé ce site pour récupérer quelques addresses et mes coordonnées aussi 🙂

Et enfin l’appel à l’api

Effectuer la requète avec votre outil préféré, j’utilise curljson

curljson -X GET « http://127.0.0.1:8000/api/places?longitude=10.134243803351655&latitude=36.80935715 »

Résultat de la requetet retourné par apiplatform
Résultat de la requette retourné par apiplatform

Conclusion

Voila, nous venons d’éffectuer une requète géo localisé dans mysql à l’aide d’api-platform
un nouveau tutorial en cours de préparation pour lié ces résultats avec une map google dans un projet angular.