vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
@@ -0,0 +1,202 @@
<?php
namespace Spatie\QueryBuilder;
use Illuminate\Support\Collection;
use Spatie\QueryBuilder\Filters\Filter;
use Spatie\QueryBuilder\Filters\FiltersBeginsWithStrict;
use Spatie\QueryBuilder\Filters\FiltersCallback;
use Spatie\QueryBuilder\Filters\FiltersEndsWithStrict;
use Spatie\QueryBuilder\Filters\FiltersExact;
use Spatie\QueryBuilder\Filters\FiltersPartial;
use Spatie\QueryBuilder\Filters\FiltersScope;
use Spatie\QueryBuilder\Filters\FiltersTrashed;
class AllowedFilter
{
/** @var Filter */
protected $filterClass;
/** @var string */
protected $name;
/** @var string */
protected $internalName;
/** @var \Illuminate\Support\Collection */
protected $ignored;
/** @var mixed */
protected $default;
/** @var bool */
protected $hasDefault = false;
/** @var bool */
protected $nullable = false;
public function __construct(string $name, Filter $filterClass, ?string $internalName = null)
{
$this->name = $name;
$this->filterClass = $filterClass;
$this->ignored = Collection::make();
$this->internalName = $internalName ?? $name;
}
public function filter(QueryBuilder $query, $value)
{
$valueToFilter = $this->resolveValueForFiltering($value);
if (! $this->nullable && is_null($valueToFilter)) {
return;
}
($this->filterClass)($query->getEloquentBuilder(), $valueToFilter, $this->internalName);
}
public static function setFilterArrayValueDelimiter(string $delimiter = null): void
{
if (isset($delimiter)) {
QueryBuilderRequest::setFilterArrayValueDelimiter($delimiter);
}
}
public static function exact(string $name, ?string $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
return new static($name, new FiltersExact($addRelationConstraint), $internalName);
}
public static function partial(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
return new static($name, new FiltersPartial($addRelationConstraint), $internalName);
}
public static function beginsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
return new static($name, new FiltersBeginsWithStrict($addRelationConstraint), $internalName);
}
public static function endsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
return new static($name, new FiltersEndsWithStrict($addRelationConstraint), $internalName);
}
public static function scope(string $name, $internalName = null, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
return new static($name, new FiltersScope(), $internalName);
}
public static function callback(string $name, $callback, $internalName = null, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
return new static($name, new FiltersCallback($callback), $internalName);
}
public static function trashed(string $name = 'trashed', $internalName = null): self
{
return new static($name, new FiltersTrashed(), $internalName);
}
public static function custom(string $name, Filter $filterClass, $internalName = null, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
return new static($name, $filterClass, $internalName);
}
public function getFilterClass(): Filter
{
return $this->filterClass;
}
public function getName(): string
{
return $this->name;
}
public function isForFilter(string $filterName): bool
{
return $this->name === $filterName;
}
public function ignore(...$values): self
{
$this->ignored = $this->ignored
->merge($values)
->flatten();
return $this;
}
public function getIgnored(): array
{
return $this->ignored->toArray();
}
public function getInternalName(): string
{
return $this->internalName;
}
public function default($value): self
{
$this->hasDefault = true;
$this->default = $value;
if (is_null($value)) {
$this->nullable(true);
}
return $this;
}
public function getDefault()
{
return $this->default;
}
public function hasDefault(): bool
{
return $this->hasDefault;
}
public function nullable(bool $nullable = true): self
{
$this->nullable = $nullable;
return $this;
}
public function unsetDefault(): self
{
$this->hasDefault = false;
unset($this->default);
return $this;
}
protected function resolveValueForFiltering($value)
{
if (is_array($value)) {
$remainingProperties = array_map([$this, 'resolveValueForFiltering'], $value);
return ! empty($remainingProperties) ? $remainingProperties : null;
}
return ! $this->ignored->contains($value) ? $value : null;
}
}
@@ -0,0 +1,112 @@
<?php
namespace Spatie\QueryBuilder;
use Closure;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\QueryBuilder\Includes\IncludedCallback;
use Spatie\QueryBuilder\Includes\IncludedCount;
use Spatie\QueryBuilder\Includes\IncludedExists;
use Spatie\QueryBuilder\Includes\IncludedRelationship;
use Spatie\QueryBuilder\Includes\IncludeInterface;
class AllowedInclude
{
/** @var string */
protected $name;
/** @var IncludeInterface */
protected $includeClass;
/** @var string|null */
protected $internalName;
public function __construct(string $name, IncludeInterface $includeClass, ?string $internalName = null)
{
$this->name = $name;
$this->includeClass = $includeClass;
$this->internalName = $internalName ?? $this->name;
}
public static function relationship(string $name, ?string $internalName = null): Collection
{
$internalName = $internalName ?? $name;
return IncludedRelationship::getIndividualRelationshipPathsFromInclude($internalName)
->zip(IncludedRelationship::getIndividualRelationshipPathsFromInclude($name))
->flatMap(function ($args): Collection {
[$relationship, $alias] = $args;
$includes = collect([
new self($alias, new IncludedRelationship(), $relationship),
]);
if (! Str::contains($relationship, '.')) {
$countSuffix = config('query-builder.count_suffix', 'Count');
$existsSuffix = config('query-builder.exists_suffix', 'Exists');
$includes = $includes
->merge(self::count(
$alias.$countSuffix,
$relationship.$countSuffix
))
->merge(self::exists(
$alias.$existsSuffix,
$relationship.$existsSuffix
));
}
return $includes;
});
}
public static function count(string $name, ?string $internalName = null): Collection
{
return collect([
new static($name, new IncludedCount(), $internalName),
]);
}
public static function exists(string $name, ?string $internalName = null): Collection
{
return collect([
new static($name, new IncludedExists(), $internalName),
]);
}
public static function callback(string $name, Closure $callback, ?string $internalName = null): Collection
{
return collect([
new static($name, new IncludedCallback($callback), $internalName),
]);
}
public static function custom(string $name, IncludeInterface $includeClass, ?string $internalName = null): Collection
{
return collect([
new static($name, $includeClass, $internalName),
]);
}
public function include(QueryBuilder $query): void
{
if (property_exists($this->includeClass, 'getRequestedFieldsForRelatedTable')) {
$this->includeClass->getRequestedFieldsForRelatedTable = function (...$args) use ($query) {
return $query->getRequestedFieldsForRelatedTable(...$args);
};
}
($this->includeClass)($query->getEloquentBuilder(), $this->internalName);
}
public function getName(): string
{
return $this->name;
}
public function isForInclude(string $includeName): bool
{
return $this->name === $includeName;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace Spatie\QueryBuilder;
use Spatie\QueryBuilder\Enums\SortDirection;
use Spatie\QueryBuilder\Exceptions\InvalidDirection;
use Spatie\QueryBuilder\Sorts\Sort;
use Spatie\QueryBuilder\Sorts\SortsCallback;
use Spatie\QueryBuilder\Sorts\SortsField;
class AllowedSort
{
/** @var \Spatie\QueryBuilder\Sorts\Sort */
protected $sortClass;
/** @var string */
protected $name;
/** @var string */
protected $defaultDirection;
/** @var string */
protected $internalName;
public function __construct(string $name, Sort $sortClass, ?string $internalName = null)
{
$this->name = ltrim($name, '-');
$this->sortClass = $sortClass;
$this->defaultDirection = static::parseSortDirection($name);
$this->internalName = $internalName ?? $this->name;
}
public static function parseSortDirection(string $name): string
{
return strpos($name, '-') === 0 ? SortDirection::DESCENDING : SortDirection::ASCENDING;
}
public function sort(QueryBuilder $query, ?bool $descending = null): void
{
$descending = $descending ?? ($this->defaultDirection === SortDirection::DESCENDING);
($this->sortClass)($query->getEloquentBuilder(), $descending, $this->internalName);
}
public static function field(string $name, ?string $internalName = null): self
{
return new static($name, new SortsField(), $internalName);
}
public static function custom(string $name, Sort $sortClass, ?string $internalName = null): self
{
return new static($name, $sortClass, $internalName);
}
public static function callback(string $name, $callback, ?string $internalName = null): self
{
return new static($name, new SortsCallback($callback), $internalName);
}
public function getName(): string
{
return $this->name;
}
public function isSort(string $sortName): bool
{
return $this->name === $sortName;
}
public function getInternalName(): string
{
return $this->internalName;
}
public function defaultDirection(string $defaultDirection)
{
if (! in_array($defaultDirection, [
SortDirection::ASCENDING,
SortDirection::DESCENDING,
])) {
throw InvalidDirection::make($defaultDirection);
}
$this->defaultDirection = $defaultDirection;
return $this;
}
}
@@ -0,0 +1,116 @@
<?php
namespace Spatie\QueryBuilder\Concerns;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\QueryBuilder\Exceptions\AllowedFieldsMustBeCalledBeforeAllowedIncludes;
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
use Spatie\QueryBuilder\Exceptions\UnknownIncludedFieldsQuery;
trait AddsFieldsToQuery
{
protected ?Collection $allowedFields = null;
public function allowedFields($fields): static
{
if ($this->allowedIncludes instanceof Collection) {
throw new AllowedFieldsMustBeCalledBeforeAllowedIncludes();
}
$fields = is_array($fields) ? $fields : func_get_args();
$this->allowedFields = collect($fields)
->map(function (string $fieldName) {
return $this->prependField($fieldName);
});
$this->ensureAllFieldsExist();
$this->addRequestedModelFieldsToQuery();
return $this;
}
protected function addRequestedModelFieldsToQuery()
{
$modelTableName = $this->getModel()->getTable();
$fields = $this->request->fields();
$modelFields = $fields->has($modelTableName) ? $fields->get($modelTableName) : $fields->get('_');
if (empty($modelFields)) {
return;
}
$prependedFields = $this->prependFieldsWithTableName($modelFields, $modelTableName);
$this->select($prependedFields);
}
public function getRequestedFieldsForRelatedTable(string $relation): array
{
$tableOrRelation = config('query-builder.convert_relation_names_to_snake_case_plural', true)
? Str::plural(Str::snake($relation))
: $relation;
$fields = $this->request->fields()
->mapWithKeys(fn ($fields, $table) => [$table => $fields])
->get($tableOrRelation);
if (! $fields) {
return [];
}
if (! $this->allowedFields instanceof Collection) {
// We have requested fields but no allowed fields (yet?)
throw new UnknownIncludedFieldsQuery($fields);
}
return $fields;
}
protected function ensureAllFieldsExist()
{
$modelTable = $this->getModel()->getTable();
$requestedFields = $this->request->fields()
->map(function ($fields, $model) use ($modelTable) {
$tableName = $model;
return $this->prependFieldsWithTableName($fields, $model === '_' ? $modelTable : $tableName);
})
->flatten()
->unique();
$unknownFields = $requestedFields->diff($this->allowedFields);
if ($unknownFields->isNotEmpty()) {
throw InvalidFieldQuery::fieldsNotAllowed($unknownFields, $this->allowedFields);
}
}
protected function prependFieldsWithTableName(array $fields, string $tableName): array
{
return array_map(function ($field) use ($tableName) {
return $this->prependField($field, $tableName);
}, $fields);
}
protected function prependField(string $field, ?string $table = null): string
{
if (! $table) {
$table = $this->getModel()->getTable();
}
if (Str::contains($field, '.')) {
// Already prepended
return $field;
}
return "{$table}.{$field}";
}
}
@@ -0,0 +1,103 @@
<?php
namespace Spatie\QueryBuilder\Concerns;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\QueryBuilder\AllowedInclude;
use Spatie\QueryBuilder\Exceptions\InvalidIncludeQuery;
use Spatie\QueryBuilder\Includes\IncludeInterface;
trait AddsIncludesToQuery
{
protected ?Collection $allowedIncludes = null;
public function allowedIncludes($includes): static
{
$includes = is_array($includes) ? $includes : func_get_args();
$this->allowedIncludes = collect($includes)
->reject(function ($include) {
return empty($include);
})
->flatMap(function ($include): Collection {
if ($include instanceof Collection) {
return $include;
}
if ($include instanceof IncludeInterface) {
return collect([$include]);
}
if (Str::endsWith($include, config('query-builder.count_suffix', 'Count'))) {
return AllowedInclude::count($include);
}
if (Str::endsWith($include, config('query-builder.exists_suffix', 'Exists'))) {
return AllowedInclude::exists($include);
}
return AllowedInclude::relationship($include);
})
->unique(function (AllowedInclude $allowedInclude) {
return $allowedInclude->getName();
});
$this->ensureAllIncludesExist();
$includes = $this->filterNonExistingIncludes($this->request->includes());
$this->addIncludesToQuery($includes);
return $this;
}
protected function addIncludesToQuery(Collection $includes)
{
$includes->each(function ($include) {
$include = $this->findInclude($include);
$include->include($this);
});
}
protected function findInclude(string $include): ?AllowedInclude
{
return $this->allowedIncludes
->first(function (AllowedInclude $included) use ($include) {
return $included->isForInclude($include);
});
}
protected function ensureAllIncludesExist()
{
if (config('query-builder.disable_invalid_includes_query_exception', false)) {
return;
}
$includes = $this->request->includes();
$allowedIncludeNames = $this->allowedIncludes->map(function (AllowedInclude $allowedInclude) {
return $allowedInclude->getName();
});
$diff = $includes->diff($allowedIncludeNames);
if ($diff->count()) {
throw InvalidIncludeQuery::includesNotAllowed($diff, $allowedIncludeNames);
}
// TODO: Check for non-existing relationships?
}
protected function filterNonExistingIncludes(Collection $includes): Collection
{
if (config('query-builder.disable_invalid_includes_query_exception', false) == false) {
return $includes;
}
return $includes->filter(function ($include) {
return $this->findInclude($include);
});
}
}
@@ -0,0 +1,81 @@
<?php
namespace Spatie\QueryBuilder\Concerns;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
trait FiltersQuery
{
/** @var \Illuminate\Support\Collection */
protected $allowedFilters;
public function allowedFilters($filters): static
{
$filters = is_array($filters) ? $filters : func_get_args();
$this->allowedFilters = collect($filters)->map(function ($filter) {
if ($filter instanceof AllowedFilter) {
return $filter;
}
return AllowedFilter::partial($filter);
});
$this->ensureAllFiltersExist();
$this->addFiltersToQuery();
return $this;
}
protected function addFiltersToQuery()
{
$this->allowedFilters->each(function (AllowedFilter $filter) {
if ($this->isFilterRequested($filter)) {
$value = $this->request->filters()->get($filter->getName());
$filter->filter($this, $value);
return;
}
if ($filter->hasDefault()) {
$filter->filter($this, $filter->getDefault());
return;
}
});
}
protected function findFilter(string $property): ?AllowedFilter
{
return $this->allowedFilters
->first(function (AllowedFilter $filter) use ($property) {
return $filter->isForFilter($property);
});
}
protected function isFilterRequested(AllowedFilter $allowedFilter): bool
{
return $this->request->filters()->has($allowedFilter->getName());
}
protected function ensureAllFiltersExist()
{
if (config('query-builder.disable_invalid_filter_query_exception', false)) {
return;
}
$filterNames = $this->request->filters()->keys();
$allowedFilterNames = $this->allowedFilters->map(function (AllowedFilter $allowedFilter) {
return $allowedFilter->getName();
});
$diff = $filterNames->diff($allowedFilterNames);
if ($diff->count()) {
throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
}
}
}
@@ -0,0 +1,116 @@
<?php
namespace Spatie\QueryBuilder\Concerns;
use Spatie\QueryBuilder\AllowedSort;
use Spatie\QueryBuilder\Exceptions\InvalidSortQuery;
trait SortsQuery
{
/** @var \Illuminate\Support\Collection */
protected $allowedSorts;
public function allowedSorts($sorts): static
{
$sorts = is_array($sorts) ? $sorts : func_get_args();
$this->allowedSorts = collect($sorts)->map(function ($sort) {
if ($sort instanceof AllowedSort) {
return $sort;
}
return AllowedSort::field(ltrim($sort, '-'));
});
$this->ensureAllSortsExist();
$this->addRequestedSortsToQuery(); // allowed is known & request is known, add what we can, if there is no request, -wait
return $this;
}
/**
* @param array|string|\Spatie\QueryBuilder\AllowedSort $sorts
*
* @return \Spatie\QueryBuilder\QueryBuilder
*/
public function defaultSort($sorts): static
{
$sorts = is_array($sorts) ? $sorts : func_get_args();
return $this->defaultSorts($sorts);
}
/**
* @param array|string|\Spatie\QueryBuilder\AllowedSort $sorts
*
* @return \Spatie\QueryBuilder\QueryBuilder
*/
public function defaultSorts($sorts): static
{
if ($this->request->sorts()->isNotEmpty()) {
// We've got requested sorts. No need to parse defaults.
return $this;
}
$sorts = is_array($sorts) ? $sorts : func_get_args();
collect($sorts)
->map(function ($sort) {
if ($sort instanceof AllowedSort) {
return $sort;
}
return AllowedSort::field($sort);
})
->each(function (AllowedSort $sort) {
$sort->sort($this);
});
return $this;
}
protected function addRequestedSortsToQuery()
{
$this->request->sorts()
->each(function (string $property) {
$descending = $property[0] === '-';
$key = ltrim($property, '-');
$sort = $this->findSort($key);
$sort?->sort($this, $descending);
});
}
protected function findSort(string $property): ?AllowedSort
{
return $this->allowedSorts
->first(function (AllowedSort $sort) use ($property) {
return $sort->isSort($property);
});
}
protected function ensureAllSortsExist(): void
{
if (config('query-builder.disable_invalid_sort_query_exception', false)) {
return;
}
$requestedSortNames = $this->request->sorts()->map(function (string $sort) {
return ltrim($sort, '-');
});
$allowedSortNames = $this->allowedSorts->map(function (AllowedSort $sort) {
return $sort->getName();
});
$unknownSorts = $requestedSortNames->diff($allowedSortNames);
if ($unknownSorts->isNotEmpty()) {
throw InvalidSortQuery::sortsNotAllowed($unknownSorts, $allowedSortNames);
}
}
}
@@ -0,0 +1,10 @@
<?php
namespace Spatie\QueryBuilder\Enums;
class SortDirection
{
public const DESCENDING = 'desc';
public const ASCENDING = 'asc';
}
@@ -0,0 +1,13 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use BadMethodCallException;
class AllowedFieldsMustBeCalledBeforeAllowedIncludes extends BadMethodCallException
{
public function __construct()
{
parent::__construct("The QueryBuilder's `allowedFields` method must be called before the `allowedIncludes` method.");
}
}
@@ -0,0 +1,32 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class InvalidAppendQuery extends InvalidQuery
{
/** @var \Illuminate\Support\Collection */
public $appendsNotAllowed;
/** @var \Illuminate\Support\Collection */
public $allowedAppends;
public function __construct(Collection $appendsNotAllowed, Collection $allowedAppends)
{
$this->appendsNotAllowed = $appendsNotAllowed;
$this->allowedAppends = $allowedAppends;
$appendsNotAllowed = $appendsNotAllowed->implode(', ');
$allowedAppends = $allowedAppends->implode(', ');
$message = "Requested append(s) `{$appendsNotAllowed}` are not allowed. Allowed append(s) are `{$allowedAppends}`.";
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
}
public static function appendsNotAllowed(Collection $appendsNotAllowed, Collection $allowedAppends)
{
return new static(...func_get_args());
}
}
@@ -0,0 +1,14 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Exception;
use Spatie\QueryBuilder\Enums\SortDirection;
class InvalidDirection extends Exception
{
public static function make(string $sort)
{
return new static('The direction should be either `'.SortDirection::DESCENDING.'` or `'.SortDirection::ASCENDING)."`. {$sort} given.";
}
}
@@ -0,0 +1,32 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class InvalidFieldQuery extends InvalidQuery
{
/** @var \Illuminate\Support\Collection */
public $unknownFields;
/** @var \Illuminate\Support\Collection */
public $allowedFields;
public function __construct(Collection $unknownFields, Collection $allowedFields)
{
$this->unknownFields = $unknownFields;
$this->allowedFields = $allowedFields;
$unknownFields = $unknownFields->implode(', ');
$allowedFields = $allowedFields->implode(', ');
$message = "Requested field(s) `{$unknownFields}` are not allowed. Allowed field(s) are `{$allowedFields}`.";
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
}
public static function fieldsNotAllowed(Collection $unknownFields, Collection $allowedFields)
{
return new static(...func_get_args());
}
}
@@ -0,0 +1,32 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class InvalidFilterQuery extends InvalidQuery
{
/** @var \Illuminate\Support\Collection */
public $unknownFilters;
/** @var \Illuminate\Support\Collection */
public $allowedFilters;
public function __construct(Collection $unknownFilters, Collection $allowedFilters)
{
$this->unknownFilters = $unknownFilters;
$this->allowedFilters = $allowedFilters;
$unknownFilters = $this->unknownFilters->implode(', ');
$allowedFilters = $this->allowedFilters->implode(', ');
$message = "Requested filter(s) `{$unknownFilters}` are not allowed. Allowed filter(s) are `{$allowedFilters}`.";
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
}
public static function filtersNotAllowed(Collection $unknownFilters, Collection $allowedFilters)
{
return new static(...func_get_args());
}
}
@@ -0,0 +1,13 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Exception;
class InvalidFilterValue extends Exception
{
public static function make($value)
{
return new static("Filter value `{$value}` is invalid.");
}
}
@@ -0,0 +1,39 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class InvalidIncludeQuery extends InvalidQuery
{
/** @var \Illuminate\Support\Collection */
public $unknownIncludes;
/** @var \Illuminate\Support\Collection */
public $allowedIncludes;
public function __construct(Collection $unknownIncludes, Collection $allowedIncludes)
{
$this->unknownIncludes = $unknownIncludes;
$this->allowedIncludes = $allowedIncludes;
$unknownIncludes = $unknownIncludes->implode(', ');
$message = "Requested include(s) `{$unknownIncludes}` are not allowed. ";
if ($allowedIncludes->count()) {
$allowedIncludes = $allowedIncludes->implode(', ');
$message .= "Allowed include(s) are `{$allowedIncludes}`.";
} else {
$message .= 'There are no allowed includes.';
}
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
}
public static function includesNotAllowed(Collection $unknownIncludes, Collection $allowedIncludes)
{
return new static(...func_get_args());
}
}
@@ -0,0 +1,9 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Symfony\Component\HttpKernel\Exception\HttpException;
abstract class InvalidQuery extends HttpException
{
}
@@ -0,0 +1,32 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class InvalidSortQuery extends InvalidQuery
{
/** @var \Illuminate\Support\Collection */
public $unknownSorts;
/** @var \Illuminate\Support\Collection */
public $allowedSorts;
public function __construct(Collection $unknownSorts, Collection $allowedSorts)
{
$this->unknownSorts = $unknownSorts;
$this->allowedSorts = $allowedSorts;
$allowedSorts = $allowedSorts->implode(', ');
$unknownSorts = $unknownSorts->implode(', ');
$message = "Requested sort(s) `{$unknownSorts}` is not allowed. Allowed sort(s) are `{$allowedSorts}`.";
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
}
public static function sortsNotAllowed(Collection $unknownSorts, Collection $allowedSorts)
{
return new static(...func_get_args());
}
}
@@ -0,0 +1,20 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use InvalidArgumentException;
class InvalidSubject extends InvalidArgumentException
{
public static function make($subject)
{
return new static(
sprintf(
'Subject %s is invalid.',
is_object($subject)
? sprintf('class `%s`', get_class($subject))
: sprintf('type `%s`', gettype($subject))
)
);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Spatie\QueryBuilder\Exceptions;
use Illuminate\Http\Response;
class UnknownIncludedFieldsQuery extends InvalidQuery
{
/** @var \Illuminate\Support\Collection */
public $unknownFields;
public function __construct(array $unknownFields)
{
$this->unknownFields = collect($unknownFields);
$unknownFields = $this->unknownFields->implode(', ');
$message = "Requested field(s) `{$unknownFields}` are not allowed (yet). ";
$message .= "If you want to allow these fields, please make sure to call the QueryBuilder's `allowedFields` method before the `allowedIncludes` method.";
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Spatie\QueryBuilder\Filters;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
*/
interface Filter
{
/**
* @param \Illuminate\Database\Eloquent\Builder<TModelClass> $query
* @param mixed $value
* @param string $property
*
* @return mixed
*/
public function __invoke(Builder $query, $value, string $property);
}
@@ -0,0 +1,18 @@
<?php
namespace Spatie\QueryBuilder\Filters;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersBeginsWithStrict extends FiltersPartial implements Filter
{
protected function getWhereRawParameters($value, string $property, string $driver): array
{
return [
"{$property} LIKE ?".static::maybeSpecifyEscapeChar($driver),
[static::escapeLike($value).'%'],
];
}
}
@@ -0,0 +1,29 @@
<?php
namespace Spatie\QueryBuilder\Filters;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersCallback implements Filter
{
/**
* @var callable a PHP callback of the following signature:
* `function (\Illuminate\Database\Eloquent\Builder $builder, mixed $value, string $property)`
*/
private $callback;
public function __construct($callback)
{
$this->callback = $callback;
}
/** {@inheritdoc} */
public function __invoke(Builder $query, $value, string $property)
{
return call_user_func($this->callback, $query, $value, $property);
}
}
@@ -0,0 +1,19 @@
<?php
namespace Spatie\QueryBuilder\Filters;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersEndsWithStrict extends FiltersPartial implements Filter
{
protected function getWhereRawParameters($value, string $property, string $driver): array
{
return [
"{$property} LIKE ?".static::maybeSpecifyEscapeChar($driver),
['%'.static::escapeLike($value)],
];
}
}
@@ -0,0 +1,81 @@
<?php
namespace Spatie\QueryBuilder\Filters;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersExact implements Filter
{
protected $relationConstraints = [];
/** @var bool */
protected $addRelationConstraint = true;
public function __construct(bool $addRelationConstraint = true)
{
$this->addRelationConstraint = $addRelationConstraint;
}
/** {@inheritdoc} */
public function __invoke(Builder $query, $value, string $property)
{
if ($this->addRelationConstraint) {
if ($this->isRelationProperty($query, $property)) {
$this->withRelationConstraint($query, $value, $property);
return;
}
}
if (is_array($value)) {
$query->whereIn($query->qualifyColumn($property), $value);
return;
}
$query->where($query->qualifyColumn($property), '=', $value);
}
protected function isRelationProperty(Builder $query, string $property): bool
{
if (! Str::contains($property, '.')) {
return false;
}
if (in_array($property, $this->relationConstraints)) {
return false;
}
$firstRelationship = explode('.', $property)[0];
if (! method_exists($query->getModel(), $firstRelationship)) {
return false;
}
return is_a($query->getModel()->{$firstRelationship}(), Relation::class);
}
protected function withRelationConstraint(Builder $query, $value, string $property)
{
[$relation, $property] = collect(explode('.', $property))
->pipe(function (Collection $parts) {
return [
$parts->except(count($parts) - 1)->implode('.'),
$parts->last(),
];
});
$query->whereHas($relation, function (Builder $query) use ($value, $property) {
$this->relationConstraints[] = $property = $query->qualifyColumn($property);
$this->__invoke($query, $value, $property);
});
}
}
@@ -0,0 +1,82 @@
<?php
namespace Spatie\QueryBuilder\Filters;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersPartial extends FiltersExact implements Filter
{
/** {@inheritdoc} */
public function __invoke(Builder $query, $value, string $property)
{
if ($this->addRelationConstraint) {
if ($this->isRelationProperty($query, $property)) {
$this->withRelationConstraint($query, $value, $property);
return;
}
}
$wrappedProperty = $query->getQuery()->getGrammar()->wrap($query->qualifyColumn($property));
$databaseDriver = $this->getDatabaseDriver($query);
if (is_array($value)) {
if (count(array_filter($value, 'strlen')) === 0) {
return $query;
}
$query->where(function (Builder $query) use ($databaseDriver, $value, $wrappedProperty) {
foreach (array_filter($value, 'strlen') as $partialValue) {
[$sql, $bindings] = $this->getWhereRawParameters($partialValue, $wrappedProperty, $databaseDriver);
$query->orWhereRaw($sql, $bindings);
}
});
return;
}
[$sql, $bindings] = $this->getWhereRawParameters($value, $wrappedProperty, $databaseDriver);
$query->whereRaw($sql, $bindings);
}
protected function getDatabaseDriver(Builder $query): string
{
return $query->getConnection()->getDriverName();
}
protected function getWhereRawParameters($value, string $property, string $driver): array
{
$value = mb_strtolower((string) $value, 'UTF8');
return [
"LOWER({$property}) LIKE ?".self::maybeSpecifyEscapeChar($driver),
['%'.self::escapeLike($value).'%'],
];
}
protected static function escapeLike(string $value): string
{
return str_replace(
['\\', '_', '%'],
['\\\\', '\\_', '\\%'],
$value,
);
}
/**
* @param 'sqlite'|'pgsql'|'sqlsrc'|'mysql' $driver
* @return string
*/
protected static function maybeSpecifyEscapeChar(string $driver): string
{
if(! in_array($driver, ['sqlite','pgsql','sqlsrv'])) {
return '';
}
return " ESCAPE '\'";
}
}
@@ -0,0 +1,103 @@
<?php
namespace Spatie\QueryBuilder\Filters;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionException;
use ReflectionObject;
use ReflectionParameter;
use ReflectionUnionType;
use Spatie\QueryBuilder\Exceptions\InvalidFilterValue;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersScope implements Filter
{
/** {@inheritdoc} */
public function __invoke(Builder $query, $values, string $property): Builder
{
$propertyParts = collect(explode('.', $property));
$scope = Str::camel($propertyParts->pop()); // TODO: Make this configurable?
$values = array_values(Arr::wrap($values));
$values = $this->resolveParameters($query, $values, $scope);
$relation = $propertyParts->implode('.');
if ($relation) {
return $query->whereHas($relation, function (Builder $query) use (
$scope,
$values
) {
return $query->$scope(...$values);
});
}
return $query->$scope(...$values);
}
protected function resolveParameters(Builder $query, $values, string $scope): array
{
try {
$parameters = (new ReflectionObject($query->getModel()))
->getMethod('scope' . ucfirst($scope))
->getParameters();
} catch (ReflectionException $e) {
return $values;
}
foreach ($parameters as $parameter) {
if (! optional($this->getClass($parameter))->isSubclassOf(Model::class)) {
continue;
}
$model = $this->getClass($parameter)->newInstance();
$index = $parameter->getPosition() - 1;
$value = $values[$index];
$result = $model->resolveRouteBinding($value);
if ($result === null) {
throw InvalidFilterValue::make($value);
}
$values[$index] = $result;
}
return $values;
}
protected function getClass(ReflectionParameter $parameter): ?ReflectionClass
{
if (version_compare(PHP_VERSION, '8.0', '<')) {
return $parameter->getClass();
}
$type = $parameter->getType();
if (is_null($type)) {
return null;
}
if ($type instanceof ReflectionUnionType) {
return null;
}
if ($type->isBuiltin()) {
return null;
}
if ($type->getName() === 'self') {
return $parameter->getDeclaringClass();
}
return new ReflectionClass($type->getName());
}
}
@@ -0,0 +1,30 @@
<?php
namespace Spatie\QueryBuilder\Filters;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersTrashed implements Filter
{
/** {@inheritdoc} */
public function __invoke(Builder $query, $value, string $property)
{
if ($value === 'with') {
$query->withTrashed();
return;
}
if ($value === 'only') {
$query->onlyTrashed();
return;
}
$query->withoutTrashed();
}
}
@@ -0,0 +1,19 @@
<?php
namespace Spatie\QueryBuilder\Includes;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
*/
interface IncludeInterface
{
/**
* @param \Illuminate\Database\Eloquent\Builder<TModelClass> $query
* @param string $include
*
* @return mixed
*/
public function __invoke(Builder $query, string $include);
}
@@ -0,0 +1,23 @@
<?php
namespace Spatie\QueryBuilder\Includes;
use Closure;
use Illuminate\Database\Eloquent\Builder;
class IncludedCallback implements IncludeInterface
{
protected Closure $callback;
public function __construct(Closure $callback)
{
$this->callback = $callback;
}
public function __invoke(Builder $query, string $relation)
{
$query->with([
$relation => $this->callback,
]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Spatie\QueryBuilder\Includes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
class IncludedCount implements IncludeInterface
{
public function __invoke(Builder $query, string $count)
{
$query->withCount(Str::before($count, config('query-builder.count_suffix', 'Count')));
}
}
@@ -0,0 +1,20 @@
<?php
namespace Spatie\QueryBuilder\Includes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
class IncludedExists implements IncludeInterface
{
public function __invoke(Builder $query, string $exists)
{
$exists = Str::before($exists, config('query-builder.exists_suffix', 'Exists'));
$query
->withExists($exists)
->withCasts([
"{$exists}_exists" => 'boolean',
]);
}
}
@@ -0,0 +1,50 @@
<?php
namespace Spatie\QueryBuilder\Includes;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class IncludedRelationship implements IncludeInterface
{
/** @var Closure|null */
public $getRequestedFieldsForRelatedTable;
public function __invoke(Builder $query, string $relationship)
{
$relatedTables = collect(explode('.', $relationship));
$withs = $relatedTables
->mapWithKeys(function ($table, $key) use ($query, $relatedTables) {
$fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');
if ($this->getRequestedFieldsForRelatedTable) {
$fields = ($this->getRequestedFieldsForRelatedTable)($fullRelationName);
}
if (empty($fields)) {
return [$fullRelationName];
}
return [$fullRelationName => function ($query) use ($fields) {
$query->select($fields);
}];
})
->toArray();
$query->with($withs);
}
public static function getIndividualRelationshipPathsFromInclude(string $include): Collection
{
return collect(explode('.', $include))
->reduce(function (Collection $includes, string $relationship) {
if ($includes->isEmpty()) {
return $includes->push($relationship);
}
return $includes->push("{$includes->last()}.{$relationship}");
}, collect());
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace Spatie\QueryBuilder;
use ArrayAccess;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request;
use Illuminate\Support\Traits\ForwardsCalls;
use Spatie\QueryBuilder\Concerns\AddsFieldsToQuery;
use Spatie\QueryBuilder\Concerns\AddsIncludesToQuery;
use Spatie\QueryBuilder\Concerns\FiltersQuery;
use Spatie\QueryBuilder\Concerns\SortsQuery;
use Spatie\QueryBuilder\Exceptions\InvalidSubject;
/**
* @mixin EloquentBuilder
*/
class QueryBuilder implements ArrayAccess
{
use FiltersQuery;
use SortsQuery;
use AddsIncludesToQuery;
use AddsFieldsToQuery;
use ForwardsCalls;
/** @var \Spatie\QueryBuilder\QueryBuilderRequest */
protected $request;
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation */
protected $subject;
/**
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $subject
* @param null|\Illuminate\Http\Request $request
*/
public function __construct($subject, ?Request $request = null)
{
$this->initializeSubject($subject)
->initializeRequest($request ?? app(Request::class));
}
/**
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $subject
*
* @return $this
*/
protected function initializeSubject($subject): static
{
throw_unless(
$subject instanceof EloquentBuilder || $subject instanceof Relation,
InvalidSubject::make($subject)
);
$this->subject = $subject;
return $this;
}
protected function initializeRequest(?Request $request = null): static
{
$this->request = $request
? QueryBuilderRequest::fromRequest($request)
: app(QueryBuilderRequest::class);
return $this;
}
public function getEloquentBuilder(): EloquentBuilder
{
if ($this->subject instanceof EloquentBuilder) {
return $this->subject;
}
if ($this->subject instanceof Relation) {
return $this->subject->getQuery();
}
throw InvalidSubject::make($this->subject);
}
public function getSubject()
{
return $this->subject;
}
/**
* @param EloquentBuilder|Relation|string $subject
* @param Request|null $request
*
* @return static
*/
public static function for($subject, ?Request $request = null): static
{
if (is_subclass_of($subject, Model::class)) {
$subject = $subject::query();
}
return new static($subject, $request);
}
public function __call($name, $arguments)
{
$result = $this->forwardCallTo($this->subject, $name, $arguments);
/*
* If the forwarded method call is part of a chain we can return $this
* instead of the actual $result to keep the chain going.
*/
if ($result === $this->subject) {
return $this;
}
return $result;
}
public function clone()
{
return clone $this;
}
public function __clone()
{
$this->subject = clone $this->subject;
}
public function __get($name)
{
return $this->subject->{$name};
}
public function __set($name, $value)
{
$this->subject->{$name} = $value;
}
public function offsetExists($offset): bool
{
return isset($this->subject[$offset]);
}
public function offsetGet($offset): bool
{
return $this->subject[$offset];
}
public function offsetSet($offset, $value): void
{
$this->subject[$offset] = $value;
}
public function offsetUnset($offset): void
{
unset($this->subject[$offset]);
}
}
@@ -0,0 +1,221 @@
<?php
namespace Spatie\QueryBuilder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class QueryBuilderRequest extends Request
{
private static $includesArrayValueDelimiter = ',';
private static $appendsArrayValueDelimiter = ',';
private static $fieldsArrayValueDelimiter = ',';
private static $sortsArrayValueDelimiter = ',';
private static $filterArrayValueDelimiter = ',';
public static function setArrayValueDelimiter(string $delimiter): void
{
static::$filterArrayValueDelimiter = $delimiter;
static::$includesArrayValueDelimiter = $delimiter;
static::$appendsArrayValueDelimiter = $delimiter;
static::$fieldsArrayValueDelimiter = $delimiter;
static::$sortsArrayValueDelimiter = $delimiter;
}
public static function fromRequest(Request $request): self
{
return static::createFrom($request, new static());
}
public function includes(): Collection
{
$includeParameterName = config('query-builder.parameters.include', 'include');
$includeParts = $this->getRequestData($includeParameterName);
if (is_string($includeParts)) {
$includeParts = explode(static::getIncludesArrayValueDelimiter(), $includeParts);
}
return collect($includeParts)->filter();
}
public function appends(): Collection
{
$appendParameterName = config('query-builder.parameters.append', 'append');
$appendParts = $this->getRequestData($appendParameterName);
if (! is_array($appendParts) && ! is_null($appendParts)) {
$appendParts = explode(static::getAppendsArrayValueDelimiter(), $appendParts);
}
return collect($appendParts)->filter();
}
public function fields(): Collection
{
$fieldsParameterName = config('query-builder.parameters.fields', 'fields');
$fieldsData = $this->getRequestData($fieldsParameterName);
$fieldsPerTable = collect(is_string($fieldsData) ? explode(static::getFieldsArrayValueDelimiter(), $fieldsData) : $fieldsData);
if ($fieldsPerTable->isEmpty()) {
return collect();
}
$fields = [];
$fieldsPerTable->each(function ($tableFields, $model) use (&$fields) {
if (is_numeric($model)) {
// If the field is in dot notation, we'll grab the table without the field.
// If the field isn't in dot notation we want the base table. We'll use `_` and replace it later.
$model = Str::contains($tableFields, '.') ? Str::beforeLast($tableFields, '.') : '_';
}
if (! isset($fields[$model])) {
$fields[$model] = [];
}
// If the field is in dot notation, we'll grab the field without the tables:
$tableFields = array_map(function (string $field) {
return Str::afterLast($field, '.');
}, explode(static::getFieldsArrayValueDelimiter(), $tableFields));
$fields[$model] = array_merge($fields[$model], $tableFields);
});
return collect($fields);
}
public function sorts(): Collection
{
$sortParameterName = config('query-builder.parameters.sort', 'sort');
$sortParts = $this->getRequestData($sortParameterName);
if (is_string($sortParts)) {
$sortParts = explode(static::getSortsArrayValueDelimiter(), $sortParts);
}
return collect($sortParts)->filter();
}
public function filters(): Collection
{
$filterParameterName = config('query-builder.parameters.filter', 'filter');
$filterParts = $this->getRequestData($filterParameterName, []);
if (is_string($filterParts)) {
return collect();
}
$filters = collect($filterParts);
return $filters->map(function ($value) {
return $this->getFilterValue($value);
});
}
/**
* @param $value
*
* @return array|bool|null
*/
protected function getFilterValue($value)
{
if (empty($value)) {
return $value;
}
if (is_array($value)) {
return collect($value)->map(function ($valueValue) {
return $this->getFilterValue($valueValue);
})->all();
}
if (Str::contains($value, static::getFilterArrayValueDelimiter())) {
return explode(static::getFilterArrayValueDelimiter(), $value);
}
if ($value === 'true') {
return true;
}
if ($value === 'false') {
return false;
}
return $value;
}
protected function getRequestData(?string $key = null, $default = null)
{
return $this->input($key, $default);
}
public static function setIncludesArrayValueDelimiter(string $includesArrayValueDelimiter): void
{
static::$includesArrayValueDelimiter = $includesArrayValueDelimiter;
}
public static function setAppendsArrayValueDelimiter(string $appendsArrayValueDelimiter): void
{
static::$appendsArrayValueDelimiter = $appendsArrayValueDelimiter;
}
public static function setFieldsArrayValueDelimiter(string $fieldsArrayValueDelimiter): void
{
static::$fieldsArrayValueDelimiter = $fieldsArrayValueDelimiter;
}
public static function setSortsArrayValueDelimiter(string $sortsArrayValueDelimiter): void
{
static::$sortsArrayValueDelimiter = $sortsArrayValueDelimiter;
}
public static function setFilterArrayValueDelimiter(string $filterArrayValueDelimiter): void
{
static::$filterArrayValueDelimiter = $filterArrayValueDelimiter;
}
public static function getIncludesArrayValueDelimiter(): string
{
return static::$includesArrayValueDelimiter;
}
public static function getAppendsArrayValueDelimiter(): string
{
return static::$appendsArrayValueDelimiter;
}
public static function getFieldsArrayValueDelimiter(): string
{
return static::$fieldsArrayValueDelimiter;
}
public static function getSortsArrayValueDelimiter(): string
{
return static::$sortsArrayValueDelimiter;
}
public static function getFilterArrayValueDelimiter(): string
{
return static::$filterArrayValueDelimiter;
}
public static function resetDelimiters(): void
{
self::$includesArrayValueDelimiter = ',';
self::$appendsArrayValueDelimiter = ',';
self::$fieldsArrayValueDelimiter = ',';
self::$sortsArrayValueDelimiter = ',';
self::$filterArrayValueDelimiter = ',';
}
}
@@ -0,0 +1,30 @@
<?php
namespace Spatie\QueryBuilder;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
class QueryBuilderServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('laravel-query-builder')
->hasConfigFile();
}
public function registeringPackage()
{
$this->app->bind(QueryBuilderRequest::class, function ($app) {
return QueryBuilderRequest::fromRequest($app['request']);
});
}
public function provides()
{
return [
QueryBuilderRequest::class,
];
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Spatie\QueryBuilder\Sorts;
use Illuminate\Database\Eloquent\Builder;
interface Sort
{
public function __invoke(Builder $query, bool $descending, string $property);
}
@@ -0,0 +1,25 @@
<?php
namespace Spatie\QueryBuilder\Sorts;
use Illuminate\Database\Eloquent\Builder;
class SortsCallback implements Sort
{
/**
* @var callable a PHP callback of the following signature:
* `function (\Illuminate\Database\Eloquent\Builder $builder, bool $descending, string $property)`
*/
private $callback;
public function __construct($callback)
{
$this->callback = $callback;
}
/** {@inheritdoc} */
public function __invoke(Builder $query, bool $descending, string $property)
{
return call_user_func($this->callback, $query, $descending, $property);
}
}
@@ -0,0 +1,13 @@
<?php
namespace Spatie\QueryBuilder\Sorts;
use Illuminate\Database\Eloquent\Builder;
class SortsField implements Sort
{
public function __invoke(Builder $query, bool $descending, string $property)
{
$query->orderBy($property, $descending ? 'desc' : 'asc');
}
}