Symfony Api 从持久层检索后的平台过滤实体
Symfony Api Platform filtering entities after they are retrieved from persistance layer
我有这样一种情况,我需要在应用 some filters 之后从持久层中提取对象,然后对对象数据进行一些数学运算并过滤另一个查询参数的基础。
用例:获取给定经纬度 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
不是我们必须应用此属性从 db 检索所有对象后的一种过滤器,这对我来说是个谜。
我希望在返回查询结果后将以下代码放在某处:
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 过滤器在这种情况下会很有帮助。
您可以在 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 to retrieve points using MySQL query. And this repository https://github.com/beberlei/DoctrineExtensions 以使用特定的 MySQL 功能。
我发现通过编写 custom controller action 并在从持久层检索实体后过滤实体很容易过滤实体。这可能意味着我必须获取所有记录然后进行过滤,这是非常昂贵的。
替代选项是,如 所建议的那样,只需编写一个自定义过滤器来查找距离,如下所示:
定义距离过滤器:
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
.
因为我们必须add DQL functions for acos
, cos
,sin
and radians
, instead we use doctrine extensions如下:
安装学说扩展:
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
* )
我有这样一种情况,我需要在应用 some filters 之后从持久层中提取对象,然后对对象数据进行一些数学运算并过滤另一个查询参数的基础。
用例:获取给定经纬度 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
不是我们必须应用此属性从 db 检索所有对象后的一种过滤器,这对我来说是个谜。
我希望在返回查询结果后将以下代码放在某处:
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 过滤器在这种情况下会很有帮助。
您可以在 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 to retrieve points using MySQL query. And this repository https://github.com/beberlei/DoctrineExtensions 以使用特定的 MySQL 功能。
我发现通过编写 custom controller action 并在从持久层检索实体后过滤实体很容易过滤实体。这可能意味着我必须获取所有记录然后进行过滤,这是非常昂贵的。
替代选项是,如
定义距离过滤器:
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
.
因为我们必须add DQL functions for acos
, cos
,sin
and radians
, instead we use doctrine extensions如下:
安装学说扩展:
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
* )