Symfony Api Platform фильтрует объекты после их извлечения из уровня персистентности - PullRequest
0 голосов
/ 14 октября 2019

У меня есть ситуация, когда мне нужно извлечь объекты из постоянного слоя после применения некоторых фильтров , а затем выполнить некоторые математические операции с данными объекта и базой фильтров другого параметра запроса.

Вариант использования: получить все местоположения, которые находятся в радиусе 10 км от заданной широты и долготы.

Что может быть переведено в конечную точку API как:

https://api.testdomain.com/api/location?latitude=10&longitude=20&distance=10

У меня есть сущность местоположения с:

 * @ApiFilter(SearchFilter::class, properties={
 *      "longitude": "start",
 *      "latitude":"start",
 *      "city":"partial",
 *      "postal_code":"partial",
 *      "address":"partial",
 *    }
 * )
 class Location
 {
  ... 

   public function withinDistance($latitude, $longitude, $distance):?bool
   {
      $location_distance=$this->distanceFrom($latitude,$longitude);
      return $location_distance<=$distance;
   }

 }

Так как latitude и longitude - это атрибуты сущности, будет применяться поиск и применяется фильтр запросов sql, а distance это не атрибут, мы должны применять этот вид фильтра после того, как все объекты извлечены из БД, что для меня загадка.

Я хочу разместить следующий код где-нибудь после возвращения результата запроса:

 public function getCollection($collection){
  //return after search filtered applied on location.longitute and location.latitude
  $all_locations_of_lat_long=$collection;
  $locations_within_distance=[];
  $query = $this->requestStack->getCurrentRequest()->query;
  $lat= $query->get('latitude',0);
  $lng= $query->get('longitude',0);
  $distance= $query->get('distance',null);

  if($distance==null){
    return $all_locations_of_lat_long;
  }

  for($all_locations_of_lat_long as $location){
    if($location->withinDistance($lat,$lng,$distance))
       $locations_within_distance[]=$location;
  }

  return $locations_within_distance; 
 }

Как правильно применять такой фильтр к возвращаемым коллекциям объектов сущностей? Я не думаю, что ORM фильтр будет полезен в этом случае.

Ответы [ 2 ]

1 голос
/ 15 октября 2019

Вы можете применить условие в вашем SQL, например, в вашем хранилище сущностей.

class YourEntityRepository {

    public function findByLongLatDist(float lat, float long, float dist) {
        // create your query builder here and return results
    }

}

Также отметьте https://gis.stackexchange.com/questions/31628/find-points-within-a-distance-using-mysql, чтобы получить точки, используя запрос MySQL. И этот репозиторий https://github.com/beberlei/DoctrineExtensions для использования определенных функций MySQL.

0 голосов
/ 15 октября 2019

Я обнаружил, что легко фильтровать сущности, написав настраиваемое действие контроллера и отфильтровав сущности после их извлечения из слоя постоянства. Это может означать, что мне нужно было извлечь все записи и затем отфильтровать, что очень дорого.

Альтернативный вариант, как предлагалось qdequippe , заключался в простой записи пользовательского фильтра для определения расстояния следующим образом:

Определение фильтра расстояния:

src / Filter / DistanceFilter

<?php
namespace App\Filter;


use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

final class DistanceFilter extends AbstractContextAwareFilter
{

    const DISTANCE=10.0;
    const LAT='latitude';
    const LON='longitude';

    private $appliedAlready=false;

    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        // otherwise filter is applied to order and page as well
        if ($this->appliedAlready && !$this->isPropertyEnabled($property, $resourceClass) ) {
            return;
        }

        //make sure latitude and longitude are part of specs    
        if(!($this->isPropertyMapped(self::LAT, $resourceClass) && $this->isPropertyMapped(self::LON, $resourceClass)) ){
            return ;
        }

        $query=$this->requestStack->getCurrentRequest()->query;

        $values=[];
        foreach($this->properties as $prop=>$val){
            $this->properties[$prop]=$query->get($prop,null);
        }

        //distance is optional 
        if($this->properties[self::LAT]!=null && $this->properties[self::LON]!=null){
            if($this->properties['distance']==null)
                $this->properties['distance']=self::DISTANCE;
        }else{
            //may be we should raise exception 
            return;
        }

        $this->appliedAlready=True;

        // Generate a unique parameter name to avoid collisions with other filters
        $latParam = $queryNameGenerator->generateParameterName(self::LAT);
        $lonParam = $queryNameGenerator->generateParameterName(self::LON);
        $distParam = $queryNameGenerator->generateParameterName('distance');


        $locationWithinXKmDistance="(
            6371.0 * acos (
                cos ( radians(:$latParam) )
                * cos( radians(o.latitude) )
                * cos( radians(o.longitude) - radians(:$lonParam) )
                + sin ( radians(:$latParam) )
                * sin( radians(o.latitude) )
           )
        )<=:$distParam";

        $queryBuilder
            ->andWhere($locationWithinXKmDistance)
            ->setParameter($latParam, $this->properties[self::LAT])
            ->setParameter($lonParam, $this->properties[self::LON])
            ->setParameter($distParam, $this->properties['distance']);
    }

    // This function is only used to hook in documentation generators (supported by Swagger and Hydra)
    public function getDescription(string $resourceClass): array
    {
        if (!$this->properties) {
            return [];
        }

        $description = [];
        foreach ($this->properties as $property => $strategy) {
            $description["distance_$property"] = [
                'property' => $property,
                'type' => 'string',
                'required' => false,
                'swagger' => [
                    'description' => 'Find locations within given radius',
                    'name' => 'distance_filter',
                    'type' => 'filter',
                ],
            ];
        }

        return $description;
    }
}

Идея состоит в том, что мы ожидаем latitude, longitude и, необязательно, distance параметров в строке запроса. Если по одному из обязательных параметров отсутствует, фильтр не вызывается. Если расстояние отсутствует, мы примем расстояние по умолчанию 10km.

Так как вместо этого нам нужно добавить функции DQL для acos, cos, sin и radians вместомы используем расширения доктрины следующим образом:

Установить расширения доктрины:

composer require beberlei/doctrineextensions

src / config / packages / doctrine_extensions.yaml

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

src / config / services.yaml

services:
    ....
    App\Filter\DistanceFilter:
      arguments: [ '@doctrine', '@request_stack', '@?logger', {latitude: ~, longitude: ~, distance: ~} ]
      tags:
          - { name: 'api_platform.filter', id: 'location.distance_filter' }
      autowire: false
      autoconfigure: false

    app.location.search_filter:
        parent:        'api_platform.doctrine.orm.search_filter'
        arguments:     [ {"city":"partial","postal_code":"partial","address":"partial"}]
        tags:          [ { name: 'api_platform.filter', id: 'location.search_filter' } ]
        autowire:  false
        autoconfigure: false

Настройка фильтров API для объекта местоположения:

namespace App\Entity;

use App\Dto\LocationOutput;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiFilter;

/**
 * Location
 * 
 * @ApiResource(
 *      collectionOperations={
 *          "get"={
 *              "path"="/getLocationList", 
 *               "filters"={
 *                      "location.distance_filter",
 *                       "location.search_filter"
 *                }
 *           }
 *      },
 *      itemOperations={"get"},
 *      output=LocationOutput::class
 * )
...