Skip to content

Instantly share code, notes, and snippets.

@romeugodoi
Forked from teohhanhui/OrderFilter.php
Last active March 5, 2020 14:49
Show Gist options
  • Save romeugodoi/1b7b837f1976d915989a438c509257d2 to your computer and use it in GitHub Desktop.
Save romeugodoi/1b7b837f1976d915989a438c509257d2 to your computer and use it in GitHub Desktop.
APIPlatform OrderFilter with distance using geometry or geography column type on PostgreSQL database (with PostGIS).
<?php
declare(strict_types=1);
namespace App\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\OrderFilterTrait;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter as BaseOrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
* {@inheritdoc}
*
* Ordering by distance, e.g.:
* Request: `GET /places?order[distance]=ASC:lat,long`
*/
final class OrderFilter extends BaseOrderFilter
{
use OrderFilterTrait;
private const DISTANCE_PROPERTY_NAME = 'distance';
private const COORDINATES_REGEX = '/^(?P<latitude>[-+]?(?:[1-8]?\d(?:\.\d+)?|90(?:\.0+)?)),(?P<longitude>[-+]?(?:180(\.0+)?|(?:(?:1[0-7]\d)|(?:[1-9]?\d))(?:\.\d+)?))$/';
/**
* @var string
*/
private $centroidProperty;
public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null)
{
if (isset($properties[self::DISTANCE_PROPERTY_NAME])) {
$this->centroidProperty = $properties[self::DISTANCE_PROPERTY_NAME]['centroid_property'] ?? null;
if (!\is_string($this->centroidProperty)) {
throw new \InvalidArgumentException(sprintf('The "centroid_property" value must be set for "%s".', self::DISTANCE_PROPERTY_NAME));
}
unset($properties[self::DISTANCE_PROPERTY_NAME]['centroid_property']);
}
parent::__construct($managerRegistry, $requestStack, $orderParameterName, $logger, $properties, $nameConverter);
}
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if (!\in_array($property, [
self::DISTANCE_PROPERTY_NAME,
], true)) {
parent::filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
return;
}
if (self::DISTANCE_PROPERTY_NAME === $property) {
[$direction, $center] = explode(':', $value);
$direction = $this->normalizeValue($direction, $property);
if (null === $direction) {
return;
}
if (!preg_match(self::COORDINATES_REGEX, $center, $matches)) {
return;
}
$alias = $queryBuilder->getRootAliases()[0];
$field = $this->centroidProperty;
if ($this->isPropertyNested($this->centroidProperty, $resourceClass)) {
[$alias, $field] = $this->addJoinsForNestedProperty($this->centroidProperty, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::LEFT_JOIN);
}
if (null !== $nullsComparison = $this->properties[$property]['nulls_comparison'] ?? null) {
$nullsDirection = self::NULLS_DIRECTION_MAP[$nullsComparison][$direction];
$nullRankHiddenField = sprintf('_%s_%s_null_rank', $alias, $field);
$queryBuilder->addSelect(sprintf('CASE WHEN %s.%s IS NULL THEN 0 ELSE 1 END AS HIDDEN %s', $alias, $field, $nullRankHiddenField));
$queryBuilder->addOrderBy($nullRankHiddenField, $nullsDirection);
}
$latitudeParam = $queryNameGenerator->generateParameterName('latitude');
$longitudeParam = $queryNameGenerator->generateParameterName('longitude');
$queryBuilder->addSelect(sprintf(<<<SQL
ST_Distance(ST_Point(:%s, :%s), %s.%s) AS HIDDEN distance
SQL
, $longitudeParam, $latitudeParam, $alias, $field));
$queryBuilder->addOrderBy('distance', $direction);
$queryBuilder->setParameter($latitudeParam, $matches['latitude']);
$queryBuilder->setParameter($longitudeParam, $matches['longitude']);
}
}
// 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 = [];
$hasNoSpatialProperty = false;
foreach ($this->properties as $property => $strategy) {
// Get the parent description (default order)
if ($property !== self::DISTANCE_PROPERTY_NAME) {
$hasNoSpatialProperty = true;
continue;
}
$name = sprintf('%s[%s]', $this->orderParameterName, $property);
$desc = "Example: ASC:lat,long";
$description[$name] = [
'property' => $property,
'type' => 'string',
'required' => false,
'description' => $desc,
'schema' => [
'type' => 'string',
'description' => $desc,
],
'swagger' => [
'description' => $desc,
'name' => $name,
'type' => 'string',
],
];
}
if ($hasNoSpatialProperty) {
$description = array_merge($description, parent::getDescription($resourceClass));
}
return $description;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment