Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for generic paginators #1372

Merged
merged 10 commits into from Nov 4, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- fix: Eloquent builder `whereRelation()` losing the TModelClass generic type by @mad-briller.
- feat: updated return type of the Validator::safe and FormRequest::safe method by @jdjfisher
- feat: Added stub for the DB::transaction method by @jdjfisher
- feat: add support for generic paginators by @erikgaal

## [2.2.0] - 2022-08-31

Expand Down
33 changes: 33 additions & 0 deletions stubs/BelongsToMany.stub
Expand Up @@ -109,4 +109,37 @@ class BelongsToMany extends Relation
* @phpstan-return \Traversable<int, TRelatedModel>
*/
public function getResults();

/**
* Get a paginator for the "select" statement.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\LengthAwarePaginator<TRelatedModel>
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);

/**
* Paginate the given query into a simple paginator.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\Paginator<TRelatedModel>
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);

/**
* Paginate the given query into a cursor paginator.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $cursorName
* @param string|null $cursor
* @return \Illuminate\Pagination\CursorPaginator<TRelatedModel>
*/
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null);
}
27 changes: 18 additions & 9 deletions stubs/Contracts/Pagination.stub
Expand Up @@ -3,22 +3,31 @@
namespace Illuminate\Contracts\Pagination;

/**
* @mixin \Illuminate\Support\Collection
* @mixin \Illuminate\Pagination\Paginator
* @template TItem
*/
interface Paginator
{}
{
/**
* @return array<TItem>
*/
public function items(): array;
}

/**
* @mixin \Illuminate\Support\Collection
* @mixin \Illuminate\Pagination\LengthAwarePaginator
* @template TItem
*
* @extends Paginator<TItem>
*/
interface LengthAwarePaginator extends Paginator
{}

/**
* @mixin \Illuminate\Support\Collection
* @mixin \Illuminate\Pagination\CursorPaginator
* @template TItem
*/
interface CursorPaginator extends Paginator
{}
interface CursorPaginator
{
/**
* @return array<TItem>
*/
public function items(): array;
}
6 changes: 3 additions & 3 deletions stubs/EloquentBuilder.stub
Expand Up @@ -440,7 +440,7 @@ class Builder
* @param array<array-key, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\LengthAwarePaginator
* @return \Illuminate\Pagination\LengthAwarePaginator<TModelClass>
*
* @throws \InvalidArgumentException
*/
Expand All @@ -453,7 +453,7 @@ class Builder
* @param array<array-key, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\Paginator
* @return \Illuminate\Pagination\Paginator<TModelClass>
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);

Expand All @@ -464,7 +464,7 @@ class Builder
* @param array<array-key, mixed> $columns
* @param string $cursorName
* @param \Illuminate\Pagination\Cursor|string|null $cursor
* @return \Illuminate\Pagination\CursorPaginator
* @return \Illuminate\Pagination\CursorPaginator<TModelClass>
*/
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null);

Expand Down
33 changes: 33 additions & 0 deletions stubs/HasManyThrough.stub
Expand Up @@ -14,4 +14,37 @@ class HasManyThrough extends Relation
* @phpstan-return \Traversable<int, TRelatedModel>
*/
public function getResults();

/**
* Get a paginator for the "select" statement.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\LengthAwarePaginator<TRelatedModel>
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);

/**
* Paginate the given query into a simple paginator.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\Paginator<TRelatedModel>
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);

/**
* Paginate the given query into a cursor paginator.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $cursorName
* @param string|null $cursor
* @return \Illuminate\Pagination\CursorPaginator<TRelatedModel>
*/
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null);
}
107 changes: 95 additions & 12 deletions stubs/Pagination.stub
Expand Up @@ -3,33 +3,116 @@
namespace Illuminate\Pagination;

/**
* @mixin \Illuminate\Support\Collection
* @template TValue
*
* @mixin \Illuminate\Support\Collection<mixed, TValue>
*/
abstract class AbstractPaginator implements \Illuminate\Contracts\Support\Htmlable
{}
{
/**
* @return array<TValue>
*/
public function items(): array;

/**
* @return \Illuminate\Support\Collection<array-key, TValue>
*/
public function getCollection(): \Illuminate\Support\Collection;

/**
* @return \ArrayIterator<array-key, TValue>
*/
public function getIterator(): \Traversable;

public function offsetExists(mixed $offset): bool;

/**
* @return TValue|null
*/
public function offsetGet(mixed $offset): mixed;

/**
* @param TValue $value
*/
public function offsetSet(mixed $offset, $value): void;

public function offsetUnset(mixed $offset): void;
}

/**
* @implements \ArrayAccess<mixed, mixed>
* @implements \IteratorAggregate<mixed, mixed>
* @implements \Illuminate\Contracts\Support\Arrayable<array-key, mixed>
* @template TValue
*
* @implements \ArrayAccess<array-key, TValue>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this class implements ArrayAccess we should add the offset* methods here with the correct return/parameter types. This will let PHPStan know User::paginate(10)[3] returns User An example can be seen here.

Also for the other classes below + tests for this 👍🏽

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! ✅

* @implements \IteratorAggregate<array-key, TValue>
* @implements \Illuminate\Contracts\Support\Arrayable<array-key, TValue>
* @implements \Illuminate\Contracts\Pagination\Paginator<TValue>
*
* @extends AbstractPaginator<TValue>
*/
class Paginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\Paginator
{}

/**
* @implements \ArrayAccess<mixed, mixed>
* @implements \IteratorAggregate<mixed, mixed>
* @implements \Illuminate\Contracts\Support\Arrayable<array-key, mixed>
* @template TValue
*
* @implements \ArrayAccess<array-key, TValue>
* @implements \IteratorAggregate<array-key, TValue>
* @implements \Illuminate\Contracts\Support\Arrayable<array-key, TValue>
* @implements \Illuminate\Contracts\Pagination\LengthAwarePaginator<TValue>
*
* @extends AbstractPaginator<TValue>
*/
class LengthAwarePaginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\LengthAwarePaginator
{}

/**
* @implements \ArrayAccess<mixed, mixed>
* @implements \IteratorAggregate<mixed, mixed>
* @implements \Illuminate\Contracts\Support\Arrayable<array-key, mixed>
* @template TValue
*
* @mixin \Illuminate\Support\Collection<mixed, TValue>
*/
abstract class AbstractCursorPaginator implements \Illuminate\Contracts\Support\Htmlable
{
/**
* @return array<TValue>
*/
public function items(): array;

/**
* @return \Illuminate\Support\Collection<array-key, TValue>
*/
public function getCollection(): \Illuminate\Support\Collection;

/**
* @return \ArrayIterator<array-key, TValue>
*/
public function getIterator(): \Traversable;

public function offsetExists(mixed $offset): bool;

/**
* @return TValue|null
*/
public function offsetGet(mixed $offset): mixed;

/**
* @param TValue $value
*/
public function offsetSet(mixed $offset, $value): void;

public function offsetUnset(mixed $offset): void;
}

/**
* @template TValue
*
* @implements \ArrayAccess<array-key, TValue>
* @implements \IteratorAggregate<array-key, TValue>
* @implements \Illuminate\Contracts\Support\Arrayable<array-key, TValue>
* @implements \Illuminate\Contracts\Pagination\CursorPaginator<TValue>
*
* @extends AbstractCursorPaginator<TValue>
*/
class CursorPaginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\CursorPaginator
class CursorPaginator extends AbstractCursorPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\CursorPaginator
{}

/**
Expand Down
17 changes: 17 additions & 0 deletions tests/Features/Methods/Builder.php
Expand Up @@ -9,6 +9,7 @@
use App\User;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use function PHPStan\Testing\assertType;

Expand Down Expand Up @@ -345,4 +346,20 @@ public function testQueryBuilderOnEloquentBuilderWithBaseModel(EloquentBuilder $
{
assertType('Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>', $query->select());
}

/**
* @phpstan-return LengthAwarePaginator<User>
*/
public function testPaginate()
{
return User::query()->paginate();
}

/**
* @phpstan-return array<User>
*/
public function testPaginateItems()
{
return User::query()->paginate()->items();
}
}
5 changes: 4 additions & 1 deletion tests/Features/Models/Relations.php
Expand Up @@ -9,7 +9,6 @@
use App\Post;
use App\Role;
use App\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
Expand All @@ -19,6 +18,7 @@
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Pagination\LengthAwarePaginator;
use function PHPStan\Testing\assertType;

class Relations
Expand Down Expand Up @@ -86,6 +86,9 @@ public function testDecrementWithAmountOnRelation(User $user): int
return $user->accounts()->decrement('id', 5);
}

/**
* @return LengthAwarePaginator<Account>
*/
public function testPaginate(User $user): LengthAwarePaginator
{
return $user->accounts()->paginate(5);
Expand Down
25 changes: 22 additions & 3 deletions tests/Type/data/paginator-extension.php
Expand Up @@ -7,6 +7,25 @@
use App\User;
use function PHPStan\Testing\assertType;

assertType('array', User::paginate()->all());
assertType('array', User::simplePaginate()->all());
assertType('array', User::cursorPaginate()->all());
canvural marked this conversation as resolved.
Show resolved Hide resolved
assertType('Illuminate\Pagination\LengthAwarePaginator<App\User>', User::paginate());
assertType('array<App\User>', User::paginate()->all());
assertType('array<App\User>', User::paginate()->items());
assertType('App\User|null', User::paginate()[0]);

assertType('Illuminate\Pagination\Paginator<App\User>', User::simplePaginate());
assertType('array<App\User>', User::simplePaginate()->all());
assertType('array<App\User>', User::simplePaginate()->items());
assertType('App\User|null', User::simplePaginate()[0]);

assertType('Illuminate\Pagination\CursorPaginator<App\User>', User::cursorPaginate());
assertType('array<App\User>', User::cursorPaginate()->all());
assertType('array<App\User>', User::cursorPaginate()->items());
assertType('App\User|null', User::cursorPaginate()[0]);

assertType('ArrayIterator<(int|string), App\User>', User::query()->paginate()->getIterator());

// HasMany
assertType('Illuminate\Pagination\LengthAwarePaginator<App\Account>', (new User())->accounts()->paginate());

// BelongsToMany
assertType('Illuminate\Pagination\LengthAwarePaginator<App\Post>', (new User())->posts()->paginate());