This commit is contained in:
2024-09-01 18:54:23 +05:00
parent 76d18365a5
commit 061f09eca1
1597 changed files with 109451 additions and 1 deletions

View File

@@ -0,0 +1,51 @@
<?php
namespace Laravel\Nova\Query;
use Laravel\Nova\Http\Requests\NovaRequest;
class ApplyFilter
{
/**
* The filter instance.
*
* @var \Laravel\Nova\Filters\Filter
*/
public $filter;
/**
* The value of the filter.
*
* @var mixed
*/
public $value;
/**
* Create a new invokable filter applier.
*
* @param \Laravel\Nova\Filters\Filter $filter
* @param mixed $value
* @return void
*/
public function __construct($filter, $value)
{
$this->value = $value;
$this->filter = $filter;
}
/**
* Apply the filter to the given query.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function __invoke(NovaRequest $request, $query)
{
$this->filter->apply(
$request, $query, $this->value
);
return $query;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Laravel\Nova\Query;
use Laravel\Nova\TrashedStatus;
class ApplySoftDeleteConstraint
{
/**
* Apply the trashed state constraint to the query.
*
* @param \Illuminate\Database\Eloquent\Builder|\Laravel\Scout\Builder $query
* @param string $withTrashed
* @return \Illuminate\Database\Eloquent\Builder|\Laravel\Scout\Builder
*/
public function __invoke($query, $withTrashed)
{
if ($withTrashed == TrashedStatus::WITH) {
$query = $query->withTrashed();
} elseif ($withTrashed == TrashedStatus::ONLY) {
$query = $query->onlyTrashed();
}
return $query;
}
}

394
nova/src/Query/Builder.php Normal file
View File

@@ -0,0 +1,394 @@
<?php
namespace Laravel\Nova\Query;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Pagination\Paginator;
use Laravel\Nova\Contracts\QueryBuilder;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\TrashedStatus;
use Laravel\Scout\Builder as ScoutBuilder;
use Laravel\Scout\Contracts\PaginatesEloquentModels;
use RuntimeException;
class Builder implements QueryBuilder
{
/**
* The resource class.
*
* @var class-string<\Laravel\Nova\Resource>
*/
protected $resourceClass;
/**
* The original query builder instance.
*
* @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|null
*/
protected $originalQueryBuilder;
/**
* The query builder instance.
*
* @var \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|null
*/
protected $queryBuilder;
/**
* Optional callbacks before model query execution.
*
* @var array<int, callable(\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation):void>
*/
protected $queryCallbacks = [];
/**
* Determine query callbacks has been applied.
*
* @var bool
*/
protected $appliedQueryCallbacks = false;
/**
* Construct a new query builder for a resource.
*
* @param class-string<\Laravel\Nova\Resource> $resourceClass
* @return void
*/
public function __construct($resourceClass)
{
$this->resourceClass = $resourceClass;
}
/**
* Build a "whereKey" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string $key
* @return $this
*/
public function whereKey($query, $key)
{
$this->setOriginalQueryBuilder($this->queryBuilder = $query);
$this->tap(function ($query) use ($key) {
$query->whereKey($key);
});
return $this;
}
/**
* Build a "search" query for the given resource.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string|null $search
* @param array<int, \Laravel\Nova\Query\ApplyFilter> $filters
* @param array<string, string> $orderings
* @param string $withTrashed
* @return $this
*/
public function search(NovaRequest $request, $query, $search = null,
array $filters = [], array $orderings = [],
$withTrashed = TrashedStatus::DEFAULT)
{
$this->setOriginalQueryBuilder($query);
$hasSearchKeyword = ! empty(trim($search ?? ''));
$hasOrderings = collect($orderings)->filter()->isNotEmpty();
if ($this->resourceClass::usesScout()) {
if ($hasSearchKeyword) {
$this->queryBuilder = $this->resourceClass::buildIndexQueryUsingScout($request, $search, $withTrashed);
$search = '';
if ($query instanceof MorphToMany || $query instanceof BelongsToMany) {
$this->tap(function ($queryBuilder) use ($query) {
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder */
$queryBuilder->whereIn(
$this->resourceClass::newModel()->getQualifiedKeyName(),
$query->allRelatedIds()
);
});
}
}
if (! $hasSearchKeyword && ! $hasOrderings) {
$this->tap(function ($query) {
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */
$this->resourceClass::defaultOrderings($query);
});
}
}
if (! isset($this->queryBuilder)) {
$this->queryBuilder = $query;
}
$this->tap(function ($query) use ($request, $search, $filters, $orderings, $withTrashed) {
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */
$this->resourceClass::buildIndexQuery(
$request, $query, $search, $filters, $orderings, $withTrashed
);
});
return $this;
}
/**
* Pass the query to a given callback.
*
* @param callable(\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation):void $callback
* @return $this
*/
public function tap($callback)
{
$this->queryCallbacks[] = $callback;
return $this;
}
/**
* Set the "take" directly to Scout or Eloquent builder.
*
* @param int $limit
* @return $this
*/
public function take($limit)
{
$this->queryBuilder->take($limit);
return $this;
}
/**
* Defer setting a "limit" using query callback and only executed via Eloquent builder.
*
* @param int $limit
* @return $this
*/
public function limit($limit)
{
return $this->tap(function ($query) use ($limit) {
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */
$query->limit($limit);
});
}
/**
* Get the results of the search.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public function get()
{
return $this->applyQueryCallbacks($this->queryBuilder)->get();
}
/**
* Get a lazy collection for the given query by chunks of the given size.
*
* @param int $chunkSize
* @return \Illuminate\Support\LazyCollection
*/
public function lazy($chunkSize = 1000)
{
if (! method_exists($this->queryBuilder, 'lazy')) {
return $this->cursor();
}
return $this->applyQueryCallbacks($this->queryBuilder)->lazy($chunkSize);
}
/**
* Get a lazy collection for the given query.
*
* @return \Illuminate\Support\LazyCollection
*/
public function cursor()
{
$queryBuilder = $this->applyQueryCallbacks($this->queryBuilder);
if (
method_exists($queryBuilder, 'cursor')
&& (! $queryBuilder instanceof ScoutBuilder && empty($queryBuilder->getEagerLoads()))
) {
return $queryBuilder->cursor();
}
return $queryBuilder->get()->lazy();
}
/**
* Get the paginated results of the query.
*
* @param int $perPage
* @return array{0: \Illuminate\Contracts\Pagination\Paginator, 1: int|null, 2: bool}
*/
public function paginate($perPage)
{
$queryBuilder = $this->applyQueryCallbacks($this->queryBuilder);
if (! $queryBuilder instanceof ScoutBuilder) {
return [
$queryBuilder->simplePaginate($perPage),
$this->getCountForPagination(),
true,
];
}
return $this->paginateFromScout($queryBuilder, $perPage);
}
/**
* Get the paginated results of the Scout query.
*
* @param \Laravel\Scout\Builder $queryBuilder
* @param int $perPage
* @return array{0: \Illuminate\Contracts\Pagination\Paginator, 1: int|null, 2: false}
*/
protected function paginateFromScout(ScoutBuilder $queryBuilder, $perPage)
{
$originalQueryBuilder = clone $this->originalQueryBuilder;
[$sql, $bindings] = [$originalQueryBuilder->toSql(), $originalQueryBuilder->getBindings()];
$modelQueryBuilder = $this->handleQueryCallbacks($originalQueryBuilder);
if ($sql === $modelQueryBuilder->toSql() && array_diff($bindings, $modelQueryBuilder->getBindings()) === []) {
/** @var \Illuminate\Pagination\LengthAwarePaginator $paginated */
$paginated = $queryBuilder->paginate($perPage);
$items = $paginated->items();
$hasMorePages = ($paginated->perPage() * $paginated->currentPage()) < $paginated->total();
return [
app()->makeWith(Paginator::class, [
'items' => $items,
'perPage' => $paginated->perPage(),
'currentPage' => $paginated->currentPage(),
'options' => $paginated->getOptions(),
])->hasMorePagesWhen($hasMorePages),
$paginated->total(),
false,
];
}
/** @var array<int, string|int> $scoutResultKeys */
$scoutResultKeys = $queryBuilder->keys()->all();
/** @var \Illuminate\Database\Eloquent\Model&\Laravel\Scout\Searchable $model */
$model = $this->resourceClass::newModel();
$paginated = tap($model->queryScoutModelsByIds(
$queryBuilder, $scoutResultKeys
), function ($query) {
/** @var \Illuminate\Database\Eloquent\Builder $query */
$this->originalQueryBuilder = $query;
})->simplePaginate($perPage);
if (! $model->searchableUsing() instanceof PaginatesEloquentModels) {
/** @var array<int|string, int> $objectIdPositions */
$objectIdPositions = collect($scoutResultKeys)->values()->flip()->all();
$paginated->setCollection(
$paginated->getCollection()
->sortBy(function ($model) use ($objectIdPositions) {
return $objectIdPositions[$model->getScoutKey()];
}, SORT_NUMERIC)->values()
);
}
return [$paginated, $this->getCountForPagination(), false];
}
/**
* Get the count of the total records for the paginator.
*
* @return int|null
*/
public function getCountForPagination()
{
return $this->toBaseQueryBuilder()->getCountForPagination();
}
/**
* Convert the query builder to an Eloquent query builder (skip using Scout).
*
* @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation
*/
public function toBase()
{
return $this->applyQueryCallbacks($this->originalQueryBuilder);
}
/**
* Convert the query builder to an fluent query builder (skip using Scout).
*
* @return \Illuminate\Database\Query\Builder
*/
public function toBaseQueryBuilder()
{
return $this->toBase()->toBase();
}
/**
* Set original query builder instance.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder
* @return void
*/
protected function setOriginalQueryBuilder($queryBuilder)
{
if (isset($this->originalQueryBuilder)) {
throw new RuntimeException('Unable to override $originalQueryBuilder, please create a new '.self::class);
}
$this->originalQueryBuilder = $queryBuilder;
}
/**
* Apply any query callbacks to the query builder.
*
* @param \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder
* @return \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation
*/
protected function applyQueryCallbacks($queryBuilder)
{
if (! $this->appliedQueryCallbacks) {
$this->handleQueryCallbacks($queryBuilder);
$this->appliedQueryCallbacks = true;
}
return $queryBuilder;
}
/**
* Handle any query callbacks to the query builder.
*
* @param \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder
* @return \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation
*/
protected function handleQueryCallbacks($queryBuilder)
{
$callback = function ($query) {
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query */
collect($this->queryCallbacks)
->filter()
->each(function ($callback) use ($query) {
call_user_func($callback, $query);
});
};
if ($queryBuilder instanceof ScoutBuilder) {
$queryBuilder->query($callback);
} else {
$queryBuilder->tap($callback);
}
return $queryBuilder;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Laravel\Nova\Query\Mixin;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class BelongsToMany
{
/**
* Get default pivot attributes using mixin.
*
* @return \Closure
*/
public function getDefaultPivotAttributes()
{
return function () {
return collect($this->pivotValues)->mapWithKeys(function ($pivot) {
return [$pivot['column'] => $pivot['value']];
})->all();
};
}
/**
* Apply default pivot query using mixin.
*
* @return \Closure
*/
public function applyDefaultPivotQuery()
{
return function ($query) {
$query->from($this->table);
if ($this instanceof MorphToMany) {
$query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass);
}
foreach ($this->pivotWheres as $arguments) {
$query->where(...$arguments);
}
foreach ($this->pivotWhereIns as $arguments) {
$query->whereIn(...$arguments);
}
foreach ($this->pivotWhereNulls as $arguments) {
$query->whereNull(...$arguments);
}
return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey});
};
}
}

59
nova/src/Query/Search.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace Laravel\Nova\Query;
use Illuminate\Database\Eloquent\Builder;
use Laravel\Nova\Query\Search\Column;
class Search
{
/**
* The Eloquent Query Builder instance.
*
* @var \Illuminate\Database\Eloquent\Builder
*/
public $queryBuilder;
/**
* The search keyword.
*
* @var string
*/
public $searchKeyword;
/**
* Create a new search builder instance.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $queryBuilder
* @param string $searchKeyword
* @return void
*/
public function __construct($queryBuilder, $searchKeyword)
{
$this->queryBuilder = $queryBuilder;
$this->searchKeyword = $searchKeyword;
}
/**
* Get the raw results of the search.
*
* @param class-string<\Laravel\Nova\Resource> $resourceClass
* @param array<int, string|\Laravel\Nova\Query\Search\Column> $searchColumns
* @return mixed
*/
public function handle($resourceClass, array $searchColumns)
{
return $this->queryBuilder->where(function ($query) use ($searchColumns) {
$connectionType = $query->getModel()->getConnection()->getDriverName();
collect($searchColumns)
->each(function ($column) use ($query, $connectionType) {
if ($column instanceof Column || (! is_string($column) && is_callable($column))) {
$column($query, $this->searchKeyword, $connectionType);
} else {
Column::from($column)->__invoke($query, $this->searchKeyword, $connectionType);
}
});
});
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Laravel\Nova\Query\Search;
use Illuminate\Database\Query\Expression;
class Column
{
/**
* The search column.
*
* @var \Illuminate\Database\Query\Expression|string
*/
public $column;
/**
* Construct a new search.
*
* @param \Illuminate\Database\Query\Expression|string $column
* @return void
*/
public function __construct($column)
{
$this->column = $column;
}
/**
* Create Column instance for raw expression value.
*
* @param string $column
* @return mixed
*/
public static function raw($column)
{
return new static(new Expression($column));
}
/**
* Create Column instance from raw expression or fluent string.
*
* @param \Illuminate\Database\Query\Expression|string $column
* @return mixed
*/
public static function from($column)
{
if ($column instanceof Expression) {
return new static($column);
}
if (strpos($column, '->') !== false) {
return new SearchableJson($column);
} elseif (strpos($column, '.') !== false) {
[$relation, $columnName] = explode('.', $column, 2);
return new SearchableRelation($relation, $columnName);
}
return new static($column);
}
/**
* Apply the search.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string $search
* @param string $connectionType
* @param string $whereOperator
* @return \Illuminate\Database\Eloquent\Builder
*/
public function __invoke($query, $search, string $connectionType, string $whereOperator = 'orWhere')
{
return $query->{$whereOperator}(
$this->columnName($query),
$connectionType == 'pgsql' ? 'ilike' : 'like',
"%{$search}%"
);
}
/**
* Get the column name.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @return string
*/
protected function columnName($query)
{
return $this->column instanceof Expression ? $this->column : $query->qualifyColumn($this->column);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Laravel\Nova\Query\Search;
class PrimaryKey extends Column
{
/**
* Max primary key size.
*
* @var int
*/
protected $maxPrimaryKeySize;
/**
* Construct a new search.
*
* @param \Illuminate\Database\Query\Expression|string $column
* @param int $maxPrimaryKeySize
* @return void
*/
public function __construct($column, $maxPrimaryKeySize = PHP_INT_MAX)
{
$this->column = $column;
$this->maxPrimaryKeySize = $maxPrimaryKeySize;
}
/**
* Apply the search.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string|int $search
* @param string $connectionType
* @param string $whereOperator
* @return \Illuminate\Database\Eloquent\Builder
*/
public function __invoke($query, $search, string $connectionType, string $whereOperator = 'orWhere')
{
$model = $query->getModel();
$canSearchPrimaryKey = ctype_digit($search) &&
in_array($model->getKeyType(), ['int', 'integer']) &&
($connectionType != 'pgsql' || $search <= $this->maxPrimaryKeySize);
if (! $canSearchPrimaryKey) {
return parent::__invoke($query, $search, $connectionType, $whereOperator);
}
return $query->{$whereOperator}($model->getQualifiedKeyName(), $search);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Laravel\Nova\Query\Search;
use Illuminate\Support\Str;
class SearchableJson extends Column
{
/**
* The search JSON seletor path.
*
* @var \Illuminate\Database\Query\Expression|string
*/
public $jsonSelectorPath;
/**
* Construct a new search.
*
* @param \Illuminate\Database\Query\Expression|string $jsonSelectorPath
* @return void
*/
public function __construct($jsonSelectorPath)
{
$this->jsonSelectorPath = $jsonSelectorPath;
}
/**
* Apply the search.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string $search
* @param string $connectionType
* @param string $whereOperator
* @return \Illuminate\Database\Eloquent\Builder
*/
public function __invoke($query, $search, string $connectionType, string $whereOperator = 'orWhere')
{
$path = $query->getGrammar()->wrap($this->jsonSelectorPath);
$likeOperator = $connectionType == 'pgsql' ? 'ilike' : 'like';
if (in_array($connectionType, ['pgsql', 'sqlite'])) {
return $query->{$whereOperator}($this->jsonSelectorPath, $likeOperator, "%{$search}%");
}
return $query->{$whereOperator.'Raw'}("lower({$path}) {$likeOperator} ?", ['%'.Str::lower($search).'%']);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Laravel\Nova\Query\Search;
class SearchableMorphToRelation extends SearchableRelation
{
/**
* The available morph types.
*
* @var array<int, class-string<\Illuminate\Database\Eloquent\Model|\Laravel\Nova\Resource>|string>
*/
public $types = [];
/**
* Construct a new search.
*
* @param string $relation
* @param \Illuminate\Database\Query\Expression|string $column
* @param array<int, class-string<\Illuminate\Database\Eloquent\Model|\Laravel\Nova\Resource>|string> $types
* @return void
*/
public function __construct(string $relation, $column, array $types = [])
{
$this->types = $types;
parent::__construct($relation, $column);
}
/**
* Apply the search.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string $search
* @param string $connectionType
* @param string $whereOperator
* @return \Illuminate\Database\Eloquent\Builder
*/
public function __invoke($query, $search, string $connectionType, string $whereOperator = 'orWhere')
{
return $query->{$whereOperator.'HasMorph'}($this->relation, $this->morphTypes(), function ($query) use ($search, $connectionType) {
return Column::from($this->column)->__invoke(
$query, $search, $connectionType, 'where'
);
});
}
/**
* Get available morph types.
*
* @return array<int, class-string<\Illuminate\Database\Eloquent\Model>|string>|string
*/
protected function morphTypes()
{
if (empty($this->types)) {
return '*';
}
return collect($this->types)
->map(function ($resource) {
return $resource::$model ?? $resource;
})->all();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Laravel\Nova\Query\Search;
class SearchableRelation extends Column
{
/**
* The relationship name.
*
* @var string
*/
public $relation;
/**
* Construct a new search.
*
* @param string $relation
* @param \Illuminate\Database\Query\Expression|string $column
* @return void
*/
public function __construct(string $relation, $column)
{
$this->relation = $relation;
parent::__construct($column);
}
/**
* Apply the search.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string $search
* @param string $connectionType
* @param string $whereOperator
* @return \Illuminate\Database\Eloquent\Builder
*/
public function __invoke($query, $search, string $connectionType, string $whereOperator = 'orWhere')
{
return $query->{$whereOperator.'Has'}($this->relation, function ($query) use ($search, $connectionType) {
return Column::from($this->column)->__invoke(
$query, $search, $connectionType, 'where'
);
});
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Laravel\Nova\Query\Search;
class SearchableText extends Column
{
/**
* Apply the search.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string $search
* @param string $connectionType
* @param string $whereOperator
* @return \Illuminate\Database\Eloquent\Builder
*/
public function __invoke($query, $search, string $connectionType, string $whereOperator = 'orWhere')
{
if (in_array($connectionType, ['mysql', 'pgsql'])) {
$query->{$whereOperator.'FullText'}(
$this->columnName($query), $search
);
}
return $query;
}
}