From ecf4c969bdf03f60f8137bf1e7d52a4d8e74ffc2 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 5 Jul 2021 17:35:11 +0200 Subject: [PATCH 1/6] Handle pivot columns for cursor pagination --- .../Eloquent/Relations/BelongsToMany.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 741f296632f6..4e245f987ea2 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -862,6 +862,25 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p }); } + /** + * Paginate the given query into a cursor paginator. + * + * @param int|null $perPage + * @param array $columns + * @param string $cursorName + * @param string|null $cursor + * @return \Illuminate\Contracts\Pagination\CursorPaginator + * @throws \Illuminate\Pagination\CursorPaginationException + */ + public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) + { + $this->query->addSelect($this->shouldSelect($columns)); + + return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function ($paginator) { + $this->hydratePivotRelation($paginator->items()); + }); + } + /** * Chunk the results of the query. * From fdec468f2df92d0767fe4571531d142ad5fdedba Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 5 Jul 2021 17:35:38 +0200 Subject: [PATCH 2/6] Reverse aliases for column ordering with cursor pagination --- .../Database/Concerns/BuildsQueries.php | 31 ++++++++++++++++++- tests/Database/DatabaseQueryBuilderTest.php | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index 6b91a673676a..ad76db456fb5 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Concerns; use Illuminate\Container\Container; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\MultipleRecordsFoundException; use Illuminate\Database\RecordsNotFoundException; use Illuminate\Pagination\CursorPaginator; @@ -305,7 +306,9 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = $builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i) { ['column' => $column, 'direction' => $direction] = $orders[$i]; - $builder->where($column, $direction === 'asc' ? '>' : '<', $cursor->parameter($column)); + $original = $this->reverseColumnAliasingForCursorPagination($this, $column); + + $builder->where($original, $direction === 'asc' ? '>' : '<', $cursor->parameter($column)); if ($i < $orders->count() - 1) { $builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) { @@ -327,6 +330,32 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = ]); } + /** + * Reverse any aliases columns for column ordering. + * + * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder + * @param string $parameter + * @return string + */ + protected function reverseColumnAliasingForCursorPagination($builder, string $parameter) + { + $columns = $builder instanceof Builder ? $builder->getQuery()->columns : $builder->columns; + + if (! is_null($columns)) { + foreach ($columns as $column) { + if (stripos($column, ' as ') !== false) { + [$original, $alias] = explode(' as ', $column); + + if ($parameter === $alias) { + return $original; + } + } + } + } + + return $parameter; + } + /** * Create a new length-aware paginator instance. * diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 6a511b9232e1..8f07c34c9ffb 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -4271,7 +4271,7 @@ protected function getMySqlBuilderWithProcessor() } /** - * @return m\MockInterface|Builder + * @return \Mockery\MockInterface|\Illuminate\Database\Query\Builder */ protected function getMockQueryBuilder() { From c0c9987099874998474a1e59b8aee48aa8eb6fe5 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 12 Jul 2021 18:37:57 +0200 Subject: [PATCH 3/6] Fix retrieving model relationship values for cursor pagination --- .../Pagination/AbstractCursorPaginator.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Illuminate/Pagination/AbstractCursorPaginator.php b/src/Illuminate/Pagination/AbstractCursorPaginator.php index 72698705205e..a4d0155d95d3 100644 --- a/src/Illuminate/Pagination/AbstractCursorPaginator.php +++ b/src/Illuminate/Pagination/AbstractCursorPaginator.php @@ -195,6 +195,21 @@ public function getParametersForItem($item) return collect($this->parameters) ->flip() ->map(function ($_, $parameterName) use ($item) { + if ($item instanceof \Illuminate\Database\Eloquent\Model) { + $table = Str::beforeLast($parameterName, '.'); + + /** @var \Illuminate\Database\Eloquent\Model $relation */ + foreach ($item->getRelations() as $relation) { + if ($relation->getTable() === $table) { + $attribute = Str::afterLast($parameterName, '.'); + + return $this->ensureParameterIsPrimitive( + $relation->getAttribute($attribute) + ); + } + } + } + if ($item instanceof ArrayAccess || is_array($item)) { return $this->ensureParameterIsPrimitive( $item[$parameterName] ?? $item[Str::afterLast($parameterName, '.')] From 5da882cc145f56a0685028966efd4a498f60a88b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 12 Jul 2021 12:13:57 -0500 Subject: [PATCH 4/6] formatting --- src/Illuminate/Database/Concerns/BuildsQueries.php | 11 ++++++----- src/Illuminate/Pagination/AbstractCursorPaginator.php | 6 ++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index ad76db456fb5..639b2e88a306 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -306,9 +306,10 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = $builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i) { ['column' => $column, 'direction' => $direction] = $orders[$i]; - $original = $this->reverseColumnAliasingForCursorPagination($this, $column); - - $builder->where($original, $direction === 'asc' ? '>' : '<', $cursor->parameter($column)); + $builder->where( + $this->getOriginalColumnNameForCursorPagination($this, $column), + $direction === 'asc' ? '>' : '<', $cursor->parameter($column) + ); if ($i < $orders->count() - 1) { $builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) { @@ -331,13 +332,13 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = } /** - * Reverse any aliases columns for column ordering. + * Get the original column name of the given column, without any aliasing. * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder * @param string $parameter * @return string */ - protected function reverseColumnAliasingForCursorPagination($builder, string $parameter) + protected function getOriginalColumnNameForCursorPagination($builder, string $parameter) { $columns = $builder instanceof Builder ? $builder->getQuery()->columns : $builder->columns; diff --git a/src/Illuminate/Pagination/AbstractCursorPaginator.php b/src/Illuminate/Pagination/AbstractCursorPaginator.php index a4d0155d95d3..02d20dd9b022 100644 --- a/src/Illuminate/Pagination/AbstractCursorPaginator.php +++ b/src/Illuminate/Pagination/AbstractCursorPaginator.php @@ -6,6 +6,8 @@ use Closure; use Exception; use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -195,12 +197,12 @@ public function getParametersForItem($item) return collect($this->parameters) ->flip() ->map(function ($_, $parameterName) use ($item) { - if ($item instanceof \Illuminate\Database\Eloquent\Model) { + if ($item instanceof Model) { $table = Str::beforeLast($parameterName, '.'); /** @var \Illuminate\Database\Eloquent\Model $relation */ foreach ($item->getRelations() as $relation) { - if ($relation->getTable() === $table) { + if ($relation instanceof Pivot && $relation->getTable() === $table) { $attribute = Str::afterLast($parameterName, '.'); return $this->ensureParameterIsPrimitive( From 609186a8d4f1596d79c6a073664dc47618ea4a1d Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Tue, 13 Jul 2021 11:48:39 +0200 Subject: [PATCH 5/6] cleanup --- src/Illuminate/Database/Concerns/BuildsQueries.php | 3 ++- src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index 639b2e88a306..6dbca8dde7fa 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -308,7 +308,8 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = $builder->where( $this->getOriginalColumnNameForCursorPagination($this, $column), - $direction === 'asc' ? '>' : '<', $cursor->parameter($column) + $direction === 'asc' ? '>' : '<', + $cursor->parameter($column) ); if ($i < $orders->count() - 1) { diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 4e245f987ea2..32bdf8843a0d 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -870,7 +870,6 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p * @param string $cursorName * @param string|null $cursor * @return \Illuminate\Contracts\Pagination\CursorPaginator - * @throws \Illuminate\Pagination\CursorPaginationException */ public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) { From 82d6060067cabc163f93b60926e2079bc70441f5 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 13 Jul 2021 07:35:47 -0500 Subject: [PATCH 6/6] method extraction --- .../Pagination/AbstractCursorPaginator.php | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Pagination/AbstractCursorPaginator.php b/src/Illuminate/Pagination/AbstractCursorPaginator.php index 02d20dd9b022..e65c0cb12e14 100644 --- a/src/Illuminate/Pagination/AbstractCursorPaginator.php +++ b/src/Illuminate/Pagination/AbstractCursorPaginator.php @@ -197,22 +197,10 @@ public function getParametersForItem($item) return collect($this->parameters) ->flip() ->map(function ($_, $parameterName) use ($item) { - if ($item instanceof Model) { - $table = Str::beforeLast($parameterName, '.'); - - /** @var \Illuminate\Database\Eloquent\Model $relation */ - foreach ($item->getRelations() as $relation) { - if ($relation instanceof Pivot && $relation->getTable() === $table) { - $attribute = Str::afterLast($parameterName, '.'); - - return $this->ensureParameterIsPrimitive( - $relation->getAttribute($attribute) - ); - } - } - } - - if ($item instanceof ArrayAccess || is_array($item)) { + if ($item instanceof Model && + ! is_null($parameter = $this->getPivotParameterForItem($item, $parameterName))) { + return $parameter; + } elseif ($item instanceof ArrayAccess || is_array($item)) { return $this->ensureParameterIsPrimitive( $item[$parameterName] ?? $item[Str::afterLast($parameterName, '.')] ); @@ -226,6 +214,26 @@ public function getParametersForItem($item) })->toArray(); } + /** + * Get the cursor parameter value from a pivot model if applicable. + * + * @param \ArrayAccess|\stdClass $item + * @param string $parameterName + * @return string|null + */ + protected function getPivotParameterForItem($item, $parameterName) + { + $table = Str::beforeLast($parameterName, '.'); + + foreach ($item->getRelations() as $relation) { + if ($relation instanceof Pivot && $relation->getTable() === $table) { + return $this->ensureParameterIsPrimitive( + $relation->getAttribute(Str::afterLast($parameterName, '.')) + ); + } + } + } + /** * Ensure the parameter is a primitive type. *