Support filtering by nested relationships/attributes
ie. support `filter[relationship.attribute]=value`
This commit is contained in:
parent
f2aac7f78e
commit
4fe4efc036
|
|
@ -53,26 +53,26 @@ interface AdapterInterface
|
|||
public function filterByAttribute($query, Attribute $attribute, $value, string $operator = '='): void;
|
||||
|
||||
/**
|
||||
* Manipulate the query to only include resources with any one of the given
|
||||
* resource IDs in a has-one relationship.
|
||||
* Manipulate the query to only include resources with a has-one
|
||||
* relationship within the given scope.
|
||||
*
|
||||
* @param $query
|
||||
* @param HasOne $relationship
|
||||
* @param array $ids
|
||||
* @param Closure $scope
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterByHasOne($query, HasOne $relationship, array $ids): void;
|
||||
public function filterByHasOne($query, HasOne $relationship, Closure $scope): void;
|
||||
|
||||
/**
|
||||
* Manipulate the query to only include resources with any one of the given
|
||||
* resource IDs in a has-many relationship.
|
||||
* Manipulate the query to only include resources with a has-many
|
||||
* relationship within the given scope.
|
||||
*
|
||||
* @param $query
|
||||
* @param HasMany $relationship
|
||||
* @param array $ids
|
||||
* @param Closure $scope
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterByHasMany($query, HasMany $relationship, array $ids): void;
|
||||
public function filterByHasMany($query, HasMany $relationship, Closure $scope): void;
|
||||
|
||||
/**
|
||||
* Manipulate the query to sort by the given attribute in the given direction.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
namespace Tobyz\JsonApiServer\Adapter;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
|
@ -164,34 +165,21 @@ class EloquentAdapter implements AdapterInterface
|
|||
$query->where($column, $operator, $value);
|
||||
}
|
||||
|
||||
public function filterByHasOne($query, HasOne $relationship, array $ids): void
|
||||
public function filterByHasOne($query, HasOne $relationship, Closure $scope): void
|
||||
{
|
||||
$relation = $this->getEloquentRelation($query->getModel(), $relationship);
|
||||
|
||||
if ($relation instanceof HasOneThrough) {
|
||||
$query->whereHas($this->getRelationshipProperty($relationship), function ($query) use ($relation, $ids) {
|
||||
$query->whereIn($relation->getQualifiedParentKeyName(), $ids);
|
||||
});
|
||||
} else {
|
||||
$query->whereIn($relation->getQualifiedForeignKeyName(), $ids);
|
||||
}
|
||||
$this->filterByRelationship($query, $relationship, $scope);
|
||||
}
|
||||
|
||||
public function filterByHasMany($query, HasMany $relationship, array $ids): void
|
||||
public function filterByHasMany($query, HasMany $relationship, Closure $scope): void
|
||||
{
|
||||
$this->filterByRelationship($query, $relationship, $scope);
|
||||
}
|
||||
|
||||
private function filterByRelationship($query, Relationship $relationship, Closure $scope): void
|
||||
{
|
||||
$property = $this->getRelationshipProperty($relationship);
|
||||
$relation = $this->getEloquentRelation($query->getModel(), $relationship);
|
||||
$relatedKey = $relation->getRelated()->getQualifiedKeyName();
|
||||
|
||||
if (count($ids)) {
|
||||
foreach ($ids as $id) {
|
||||
$query->whereHas($property, function ($query) use ($relatedKey, $id) {
|
||||
$query->where($relatedKey, $id);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$query->whereDoesntHave($property);
|
||||
}
|
||||
$query->whereHas($property, $scope);
|
||||
}
|
||||
|
||||
public function sortByAttribute($query, Attribute $attribute, string $direction): void
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ use JsonApiPhp\JsonApi\Link\NextLink;
|
|||
use JsonApiPhp\JsonApi\Link\PrevLink;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use ReflectionClass;
|
||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
|
|
@ -24,6 +26,7 @@ use Tobyz\JsonApiServer\JsonApi;
|
|||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||
use Tobyz\JsonApiServer\Schema\Meta;
|
||||
use Tobyz\JsonApiServer\Schema\Relationship;
|
||||
use Tobyz\JsonApiServer\Serializer;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use function Tobyz\JsonApiServer\json_api_response;
|
||||
|
|
@ -64,7 +67,7 @@ class Index
|
|||
$this->sort($query, $context);
|
||||
|
||||
if ($filter = $context->getRequest()->getQueryParams()['filter'] ?? null) {
|
||||
$this->resource->filter($query, $filter, $context);
|
||||
$this->filter($this->resource, $query, $filter, $context);
|
||||
}
|
||||
|
||||
run_callbacks($schema->getListeners('listing'), [$query, $context]);
|
||||
|
|
@ -234,4 +237,72 @@ class Index
|
|||
|
||||
return [$offset, $limit];
|
||||
}
|
||||
|
||||
private function filter(ResourceType $resource, $query, $filter, Context $context): void
|
||||
{
|
||||
if (! is_array($filter)) {
|
||||
throw new BadRequestException('filter must be an array', 'filter');
|
||||
}
|
||||
|
||||
$schema = $resource->getSchema();
|
||||
$adapter = $resource->getAdapter();
|
||||
$filters = $schema->getFilters();
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($filter as $name => $value) {
|
||||
if ($name === 'id') {
|
||||
$adapter->filterByIds($query, explode(',', $value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($filters[$name]) && evaluate($filters[$name]->getVisible(), [$context])) {
|
||||
$filters[$name]->getCallback()($query, $value, $context);
|
||||
continue;
|
||||
}
|
||||
|
||||
[$name, $sub] = explode('.', $name, 2) + [null, null];
|
||||
|
||||
if (isset($fields[$name]) && evaluate($fields[$name]->getFilterable(), [$context])) {
|
||||
if ($fields[$name] instanceof Attribute && $sub === null) {
|
||||
$this->filterByAttribute($adapter, $query, $fields[$name], $value);
|
||||
continue;
|
||||
} elseif ($fields[$name] instanceof Relationship) {
|
||||
if (is_string($relatedType = $fields[$name]->getType())) {
|
||||
$relatedResource = $this->api->getResource($relatedType);
|
||||
$method = 'filterBy'.(new ReflectionClass($fields[$name]))->getShortName();
|
||||
$adapter->$method($query, $fields[$name], function ($query) use ($relatedResource, $sub, $value, $context) {
|
||||
$this->filter($relatedResource, $query, [($sub ?? 'id') => $value], $context);
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestException("Invalid filter [$name]", "filter[$name]");
|
||||
}
|
||||
}
|
||||
|
||||
private function filterByAttribute(AdapterInterface $adapter, $query, Attribute $attribute, $value): void
|
||||
{
|
||||
if (preg_match('/(.+)\.\.(.+)/', $value, $matches)) {
|
||||
if ($matches[1] !== '*') {
|
||||
$adapter->filterByAttribute($query, $attribute, $value, '>=');
|
||||
}
|
||||
if ($matches[2] !== '*') {
|
||||
$adapter->filterByAttribute($query, $attribute, $value, '<=');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (['>=', '>', '<=', '<'] as $operator) {
|
||||
if (strpos($value, $operator) === 0) {
|
||||
$adapter->filterByAttribute($query, $attribute, substr($value, strlen($operator)), $operator);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$adapter->filterByAttribute($query, $attribute, $value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,67 +59,4 @@ final class ResourceType
|
|||
{
|
||||
run_callbacks($this->getSchema()->getListeners('scope'), [$query, $context]);
|
||||
}
|
||||
|
||||
public function filter($query, $filter, Context $context)
|
||||
{
|
||||
if (! is_array($filter)) {
|
||||
throw new BadRequestException('filter must be an array', 'filter');
|
||||
}
|
||||
|
||||
$schema = $this->getSchema();
|
||||
$adapter = $this->getAdapter();
|
||||
$filters = $schema->getFilters();
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($filter as $name => $value) {
|
||||
if ($name === 'id') {
|
||||
$adapter->filterByIds($query, explode(',', $value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($filters[$name]) && evaluate($filters[$name]->getVisible(), [$context])) {
|
||||
$filters[$name]->getCallback()($query, $value, $context);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($fields[$name]) && evaluate($fields[$name]->getFilterable(), [$context])) {
|
||||
if ($fields[$name] instanceof Attribute) {
|
||||
$this->filterByAttribute($adapter, $query, $fields[$name], $value);
|
||||
} elseif ($fields[$name] instanceof HasOne) {
|
||||
$value = array_filter(explode(',', $value));
|
||||
$adapter->filterByHasOne($query, $fields[$name], $value);
|
||||
} elseif ($fields[$name] instanceof HasMany) {
|
||||
$value = array_filter(explode(',', $value));
|
||||
$adapter->filterByHasMany($query, $fields[$name], $value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new BadRequestException("Invalid filter [$name]", "filter[$name]");
|
||||
}
|
||||
}
|
||||
|
||||
private function filterByAttribute(AdapterInterface $adapter, $query, Attribute $attribute, $value)
|
||||
{
|
||||
if (preg_match('/(.+)\.\.(.+)/', $value, $matches)) {
|
||||
if ($matches[1] !== '*') {
|
||||
$adapter->filterByAttribute($query, $attribute, $value, '>=');
|
||||
}
|
||||
if ($matches[2] !== '*') {
|
||||
$adapter->filterByAttribute($query, $attribute, $value, '<=');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (['>=', '>', '<=', '<'] as $operator) {
|
||||
if (strpos($value, $operator) === 0) {
|
||||
$adapter->filterByAttribute($query, $attribute, substr($value, strlen($operator)), $operator);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$adapter->filterByAttribute($query, $attribute, $value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue