Skip to content

Instantly share code, notes, and snippets.

@teohhanhui
Last active April 21, 2020 13:23
Show Gist options
  • Save teohhanhui/d0193b1e080d3ae31f1fb22a71c1173f to your computer and use it in GitHub Desktop.
Save teohhanhui/d0193b1e080d3ae31f1fb22a71c1173f to your computer and use it in GitHub Desktop.
OrderFilter with distance
<?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']);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment