vendor and env first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
+13
@@ -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))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user