├── .coveralls.yml ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── _ide_helper.php ├── composer.json ├── phpunit.xml ├── src ├── Http │ └── Resources │ │ ├── CollectsPaginationResult.php │ │ └── Json │ │ ├── AnonymousPaginationResultAwareResourceCollection.php │ │ ├── MakesAnonymousPaginationResultAwareResourceCollection.php │ │ ├── PaginationResultResourceResponse.php │ │ └── RespondsWithPaginationResult.php ├── LampagerResourceCollectionTrait.php ├── LampagerResourceTrait.php ├── MacroServiceProvider.php ├── PaginationResult.php ├── Paginator.php └── Processor.php └── tests ├── EloquentDate.php ├── FormatterTest.php ├── JsonResource.php ├── MacroTest.php ├── MySQLGrammarTest.php ├── PaginationResultTest.php ├── Post.php ├── PostResource.php ├── PostResourceCollection.php ├── PostTagPivot.php ├── ProcessorTest.php ├── ResourceTest.php ├── StructuredPostResourceCollection.php ├── Tag.php ├── TagResource.php └── TestCase.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: build/logs/clover.xml 2 | json_path: build/logs/coveralls-upload.json 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | php: [8.2, 8.3, 8.4] 12 | lib: 13 | - laravel: ^13.0.x-dev 14 | - laravel: ^12.0 15 | - laravel: ^11.0 16 | exclude: 17 | - php: 8.2 18 | lib: 19 | laravel: ^13.0.x-dev 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php }} 28 | coverage: xdebug 29 | 30 | - run: composer require "laravel/framework:${{ matrix.lib.laravel }}" --dev 31 | - run: mkdir -p build/logs 32 | - run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml 33 | 34 | - name: Upload Coverage 35 | uses: nick-invision/retry@v2 36 | env: 37 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | COVERALLS_PARALLEL: 'true' 39 | COVERALLS_FLAG_NAME: 'laravel:${{ matrix.lib.laravel }}' 40 | with: 41 | timeout_minutes: 1 42 | max_attempts: 3 43 | command: | 44 | composer global require php-coveralls/php-coveralls 45 | php-coveralls --coverage_clover=build/logs/clover.xml -v 46 | 47 | coverage-aggregation: 48 | needs: build 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Aggregate Coverage 52 | uses: coverallsapp/github-action@master 53 | with: 54 | github-token: ${{ secrets.GITHUB_TOKEN }} 55 | parallel-finished: true 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /.idea/ 3 | /vendor/ 4 | /build/logs/ 5 | .php_cs.cache 6 | /.phpunit.cache/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mpyw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | lampager-laravel 3 |

4 |

5 | Build Status 6 | Coverage Status 7 | 8 | # Lampager for Laravel 9 | 10 | Rapid pagination without using OFFSET 11 | 12 | > [!CAUTION] 13 | > **Now Laravel officialy supports Cursor Pagination as of v8.41. Please don't use if you install such versions unless you choose `SQLServer` as RDBMS.** 14 | > - **[Highly Performant Cursor Pagination in Laravel 8.41 | Laravel News](https://laravel-news.com/cursor-pagination)** 15 | > - **[SQL Feature Comparison](https://www.sql-workbench.eu/dbms_comparison.html)** (See "Tuple Comparison" section) 16 | 17 | ## Requirements 18 | 19 | > [!NOTE] 20 | > Older versions have outdated dependency requirements. If you cannot prepare the latest environment, please refer to past releases. 21 | 22 | - PHP: `^8.2` 23 | - Laravel: `^11.0 || ^12.0` 24 | - [lampager/lampager](https://github.com/lampager/lampager): `^0.5` 25 | 26 | ## Installing 27 | 28 | ```bash 29 | composer require lampager/lampager-laravel 30 | ``` 31 | 32 | ## Basic Usage 33 | 34 | Register service provider. 35 | 36 | `config/app.php`: 37 | 38 | ```php 39 | /* 40 | * Package Service Providers... 41 | */ 42 | Lampager\Laravel\MacroServiceProvider::class, 43 | ``` 44 | 45 | Then you can chain `->lampager()` method from Query Builder, Eloquent Builder and Relation. 46 | 47 | ```php 48 | $cursor = [ 49 | 'id' => 3, 50 | 'created_at' => '2017-01-10 00:00:00', 51 | 'updated_at' => '2017-01-20 00:00:00', 52 | ]; 53 | 54 | $result = App\Post::whereUserId(1) 55 | ->lampager() 56 | ->forward() 57 | ->limit(5) 58 | ->orderByDesc('updated_at') // ORDER BY `updated_at` DESC, `created_at` DESC, `id` DESC 59 | ->orderByDesc('created_at') 60 | ->orderByDesc('id') 61 | ->seekable() 62 | ->paginate($cursor) 63 | ->toJson(JSON_PRETTY_PRINT); 64 | ``` 65 | 66 | It will run the optimized query. 67 | 68 | 69 | ```sql 70 | ( 71 | 72 | SELECT * FROM `posts` 73 | WHERE `user_id` = 1 74 | AND ( 75 | `updated_at` = '2017-01-20 00:00:00' AND `created_at` = '2017-01-10 00:00:00' AND `id` > 3 76 | OR 77 | `updated_at` = '2017-01-20 00:00:00' AND `created_at` > '2017-01-10 00:00:00' 78 | OR 79 | `updated_at` > '2017-01-20 00:00:00' 80 | ) 81 | ORDER BY `updated_at` ASC, `created_at` ASC, `id` ASC 82 | LIMIT 1 83 | 84 | ) UNION ALL ( 85 | 86 | SELECT * FROM `posts` 87 | WHERE `user_id` = 1 88 | AND ( 89 | `updated_at` = '2017-01-20 00:00:00' AND `created_at` = '2017-01-10 00:00:00' AND `id` <= 3 90 | OR 91 | `updated_at` = '2017-01-20 00:00:00' AND `created_at` < '2017-01-10 00:00:00' 92 | OR 93 | `updated_at` < '2017-01-20 00:00:00' 94 | ) 95 | ORDER BY `updated_at` DESC, `created_at` DESC, `id` DESC 96 | LIMIT 6 97 | 98 | ) 99 | ``` 100 | 101 | And you'll get 102 | 103 | 104 | ```json 105 | { 106 | "records": [ 107 | { 108 | "id": 3, 109 | "user_id": 1, 110 | "text": "foo", 111 | "created_at": "2017-01-10 00:00:00", 112 | "updated_at": "2017-01-20 00:00:00" 113 | }, 114 | { 115 | "id": 5, 116 | "user_id": 1, 117 | "text": "bar", 118 | "created_at": "2017-01-05 00:00:00", 119 | "updated_at": "2017-01-20 00:00:00" 120 | }, 121 | { 122 | "id": 4, 123 | "user_id": 1, 124 | "text": "baz", 125 | "created_at": "2017-01-05 00:00:00", 126 | "updated_at": "2017-01-20 00:00:00" 127 | }, 128 | { 129 | "id": 2, 130 | "user_id": 1, 131 | "text": "qux", 132 | "created_at": "2017-01-17 00:00:00", 133 | "updated_at": "2017-01-18 00:00:00" 134 | }, 135 | { 136 | "id": 1, 137 | "user_id": 1, 138 | "text": "quux", 139 | "created_at": "2017-01-16 00:00:00", 140 | "updated_at": "2017-01-18 00:00:00" 141 | } 142 | ], 143 | "has_previous": false, 144 | "previous_cursor": null, 145 | "has_next": true, 146 | "next_cursor": { 147 | "updated_at": "2017-01-18 00:00:00", 148 | "created_at": "2017-01-14 00:00:00", 149 | "id": 6 150 | } 151 | } 152 | ``` 153 | 154 | ## Resource Collection 155 | 156 | Lampager supports Laravel's API Resources. 157 | 158 | - [Eloquent: API Resources - Laravel - The PHP Framework For Web Artisans](https://laravel.com/docs/6.x/eloquent-resources) 159 | 160 | Use helper traits on Resource and ResourceCollection. 161 | 162 | ```php 163 | use Illuminate\Http\Resources\Json\JsonResource; 164 | use Lampager\Laravel\LampagerResourceTrait; 165 | 166 | class PostResource extends JsonResource 167 | { 168 | use LampagerResourceTrait; 169 | } 170 | ``` 171 | 172 | ```php 173 | use Illuminate\Http\Resources\Json\ResourceCollection; 174 | use Lampager\Laravel\LampagerResourceCollectionTrait; 175 | 176 | class PostResourceCollection extends ResourceCollection 177 | { 178 | use LampagerResourceCollectionTrait; 179 | } 180 | ``` 181 | 182 | ```php 183 | $posts = App\Post::lampager() 184 | ->orderByDesc('id') 185 | ->paginate(); 186 | 187 | return new PostResourceCollection($posts); 188 | ``` 189 | 190 | ```json5 191 | { 192 | "data": [/* ... */], 193 | "has_previous": false, 194 | "previous_cursor": null, 195 | "has_next": true, 196 | "next_cursor": {/* ... */} 197 | } 198 | ``` 199 | 200 | ## Classes 201 | 202 | Note: See also [lampager/lampager](https://github.com/lampager/lampager). 203 | 204 | | Name | Type | Parent Class | Description | 205 | |:---|:---|:---|:---| 206 | | Lampager\\Laravel\\`Paginator` | Class | Lampager\\`Paginator` | Fluent factory implementation for Laravel | 207 | | Lampager\\Laravel\\`Processor` | Class | Lampager\\`AbstractProcessor` | Processor implementation for Laravel | 208 | | Lampager\\Laravel\\`PaginationResult` | Class | Lampager\\`PaginationResult` | PaginationResult implementation for Laravel | 209 | | Lampager\\Laravel\\`MacroServiceProvider` | Class | Illuminate\\Support\\`ServiceProvider` | Enable macros chainable from QueryBuilder, ElqouentBuilder and Relation | 210 | | Lampager\\Laravel\\`LampagerResourceTrait` | Trait | | Support for Laravel JsonResource | 211 | | Lampager\\Laravel\\`LampagerResourceCollectionTrait` | Trait | | Support for Laravel ResourceCollection | 212 | 213 | `Paginator`, `Processor` and `PaginationResult` are macroable. 214 | 215 | ## API 216 | 217 | Note: See also [lampager/lampager](https://github.com/lampager/lampager). 218 | 219 | ### Paginator::__construct()
Paginator::create() 220 | 221 | Create a new paginator instance. 222 | If you use Laravel macros, however, you don't need to directly instantiate. 223 | 224 | ```php 225 | static Paginator create(QueryBuilder|EloquentBuilder|Relation $builder): static 226 | Paginator::__construct(QueryBuilder|EloquentBuilder|Relation $builder) 227 | ``` 228 | 229 | - `QueryBuilder` means `\Illuminate\Database\Query\Builder` 230 | - `EloquentBuilder` means `\Illuminate\Database\Eloquent\Builder` 231 | - `Relation` means `\Illuminate\Database\Eloquent\Relation` 232 | 233 | ### Paginator::transform() 234 | 235 | Transform Lampager Query into Illuminate builder. 236 | 237 | ```php 238 | Paginator::transform(Query $query): QueryBuilder|EloquentBuilder|Relation 239 | ``` 240 | 241 | ### Paginator::build() 242 | 243 | Perform configure + transform. 244 | 245 | ```php 246 | Paginator::build(\Lampager\Contracts\Cursor|array $cursor = []): QueryBuilder|EloquentBuilder|Relation 247 | ``` 248 | 249 | ### Paginator::paginate() 250 | 251 | Perform configure + transform + process. 252 | 253 | ```php 254 | Paginator::paginate(\Lampager\Contracts\Cursor|array $cursor = []): \Lampager\Laravel\PaginationResult 255 | ``` 256 | 257 | #### Arguments 258 | 259 | - **`(mixed)`** __*$cursor*__
An associative array that contains `$column => $value` or an object that implements `\Lampager\Contracts\Cursor`. It must be **all-or-nothing**. 260 | - For initial page, omit this parameter or pass empty array. 261 | - For subsequent pages, pass all parameters. Partial parameters are not allowd. 262 | 263 | #### Return Value 264 | 265 | e.g. 266 | 267 | (Default format when using `\Illuminate\Database\Eloquent\Builder`) 268 | 269 | ```php 270 | object(Lampager\Laravel\PaginationResult)#1 (5) { 271 | ["records"]=> 272 | object(Illuminate\Database\Eloquent\Collection)#2 (1) { 273 | ["items":protected]=> 274 | array(5) { 275 | [0]=> 276 | object(App\Post)#2 (26) { ... } 277 | [1]=> 278 | object(App\Post)#3 (26) { ... } 279 | [2]=> 280 | object(App\Post)#4 (26) { ... } 281 | [3]=> 282 | object(App\Post)#5 (26) { ... } 283 | [4]=> 284 | object(App\Post)#6 (26) { ... } 285 | } 286 | } 287 | ["hasPrevious"]=> 288 | bool(false) 289 | ["previousCursor"]=> 290 | NULL 291 | ["hasNext"]=> 292 | bool(true) 293 | ["nextCursor"]=> 294 | array(2) { 295 | ["updated_at"]=> 296 | string(19) "2017-01-18 00:00:00" 297 | ["created_at"]=> 298 | string(19) "2017-01-14 00:00:00" 299 | ["id"]=> 300 | int(6) 301 | } 302 | } 303 | ``` 304 | 305 | ### Paginator::useFormatter()
Paginator::restoreFormatter()
Paginator::process() 306 | 307 | Invoke Processor methods. 308 | 309 | ```php 310 | Paginator::useFormatter(Formatter|callable $formatter): $this 311 | Paginator::restoreFormatter(): $this 312 | Paginator::process(\Lampager\Query $query, \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Collection $rows): \Lampager\Laravel\PaginationResult 313 | ``` 314 | 315 | ### PaginationResult::toArray()
PaginationResult::jsonSerialize() 316 | 317 | Convert the object into array. 318 | 319 | **IMPORTANT: `camelCase` properties are converted into `snake_case` form.** 320 | 321 | ```php 322 | PaginationResult::toArray(): array 323 | PaginationResult::jsonSerialize(): array 324 | ``` 325 | 326 | ### PaginationResult::__call() 327 | 328 | Call macro or Collection methods. 329 | 330 | ```php 331 | PaginationResult::__call(string $name, array $args): mixed 332 | ``` 333 | 334 | e.g. 335 | 336 | ```php 337 | PaginationResult::macro('foo', function () { 338 | return ...; 339 | }); 340 | $foo = $result->foo(); 341 | ``` 342 | 343 | ```php 344 | $first = $result->first(); 345 | ``` 346 | -------------------------------------------------------------------------------- /_ide_helper.php: -------------------------------------------------------------------------------- 1 | =9.0", 34 | "phpunit/phpunit": ">=11.0", 35 | "nilportugues/sql-query-formatter": "^1.2.2" 36 | }, 37 | "minimum-stability": "dev", 38 | "prefer-stable": true, 39 | "extra": { 40 | "laravel": { 41 | "providers": [ 42 | "Lampager\\Laravel\\MacroServiceProvider" 43 | ] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | ./src 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Http/Resources/CollectsPaginationResult.php: -------------------------------------------------------------------------------- 1 | collects(); 32 | 33 | $this->collection = $collects && !$resource->first() instanceof $collects 34 | ? $resource->mapInto($collects) 35 | : $resource->toBase(); 36 | 37 | if ($resource instanceof AbstractPaginator) { 38 | $resource->setCollection($this->collection); 39 | return $resource; 40 | } 41 | if ($resource instanceof PaginationResult) { 42 | $resource->records = $this->collection; 43 | return $resource; 44 | } 45 | 46 | return $this->collection; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Http/Resources/Json/AnonymousPaginationResultAwareResourceCollection.php: -------------------------------------------------------------------------------- 1 | preserveKeys = (new static([]))->preserveKeys === true; 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Resources/Json/PaginationResultResourceResponse.php: -------------------------------------------------------------------------------- 1 | resource->resource->toArray(), 'records'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Resources/Json/RespondsWithPaginationResult.php: -------------------------------------------------------------------------------- 1 | resource instanceof AbstractPaginator) { 25 | return (new PaginatedResourceResponse($this))->toResponse($request); 26 | } 27 | if ($this->resource instanceof PaginationResult) { 28 | return (new PaginationResultResourceResponse($this))->toResponse($request); 29 | } 30 | 31 | return parent::toResponse($request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/LampagerResourceCollectionTrait.php: -------------------------------------------------------------------------------- 1 | macroCall($method, $parameters); 35 | } 36 | 37 | return $this->records->$method(...$parameters); 38 | } 39 | 40 | /** 41 | * Get the instance as an array. 42 | * 43 | * @return array 44 | */ 45 | public function toArray() 46 | { 47 | $array = []; 48 | foreach (get_object_vars($this) as $key => $value) { 49 | $array[Str::snake($key)] = $value; 50 | } 51 | return $array; 52 | } 53 | 54 | /** 55 | * Convert the object into something JSON serializable. 56 | * 57 | * @return mixed 58 | */ 59 | public function jsonSerialize() 60 | { 61 | return $this->toArray(); 62 | } 63 | 64 | /** 65 | * Convert the object to its JSON representation. 66 | * 67 | * @param int $options 68 | * @return string 69 | */ 70 | public function toJson($options = 0) 71 | { 72 | return json_encode($this->jsonSerialize(), $options); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Paginator.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 41 | $this->processor = new Processor(); 42 | } 43 | 44 | /** 45 | * Build Illuminate Builder instance from Query config. 46 | * 47 | * @param Query $query 48 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 49 | */ 50 | public function transform(Query $query) 51 | { 52 | return $this->compileSelectOrUnionAll($query->selectOrUnionAll()); 53 | } 54 | 55 | /** 56 | * Configure -> Transform. 57 | * 58 | * @param Cursor|int[]|string[] $cursor 59 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 60 | */ 61 | public function build($cursor = []) 62 | { 63 | return $this->transform($this->configure($cursor)); 64 | } 65 | 66 | /** 67 | * Execute query and paginate them. 68 | * 69 | * @param Cursor|int[]|string[] $cursor 70 | * @return mixed|PaginationResult 71 | */ 72 | public function paginate($cursor = []) 73 | { 74 | $query = $this->configure($cursor); 75 | return $this->process($query, $this->transform($query)->get()); 76 | } 77 | 78 | /** 79 | * @param SelectOrUnionAll $selectOrUnionAll 80 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 81 | */ 82 | protected function compileSelectOrUnionAll(SelectOrUnionAll $selectOrUnionAll) 83 | { 84 | if ($selectOrUnionAll instanceof Select) { 85 | return $this->compileSelect($selectOrUnionAll); 86 | } 87 | if ($selectOrUnionAll instanceof UnionAll) { 88 | $supportQuery = $this->compileSelect($selectOrUnionAll->supportQuery()); 89 | $mainQuery = $this->compileSelect($selectOrUnionAll->mainQuery()); 90 | return $supportQuery->unionAll($this->addSelectForUnionAll($mainQuery)); 91 | } 92 | // @codeCoverageIgnoreStart 93 | throw new \LogicException('Unreachable here'); 94 | // @codeCoverageIgnoreEnd 95 | } 96 | 97 | /** 98 | * @param Select $select 99 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 100 | */ 101 | protected function compileSelect(Select $select) 102 | { 103 | $builder = clone $this->builder; 104 | $this 105 | ->compileWhere($builder, $select) 106 | ->compileOrderBy($builder, $select) 107 | ->compileLimit($builder, $select); 108 | return $builder; 109 | } 110 | 111 | /** 112 | * @param $builder \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 113 | * @param Select $select 114 | * @return $this 115 | */ 116 | protected function compileWhere($builder, Select $select) 117 | { 118 | $builder->where(function ($builder) use ($select) { 119 | foreach ($select->where() as $i => $group) { 120 | foreach ($group as $j => $condition) { 121 | $builder->{$i !== 0 && $j === 0 ? 'orWhere' : 'where'}( 122 | $this->transformPivotColumn($condition->left()), 123 | $condition->comparator(), 124 | $condition->right() 125 | ); 126 | } 127 | } 128 | }); 129 | return $this; 130 | } 131 | 132 | /** 133 | * @param $builder \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 134 | * @param Select $select 135 | * @return $this 136 | */ 137 | protected function compileOrderBy($builder, Select $select) 138 | { 139 | foreach ($select->orders() as $order) { 140 | $builder->orderBy(...$order->toArray()); 141 | } 142 | return $this; 143 | } 144 | 145 | /** 146 | * @param $builder \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 147 | * @param Select $select 148 | * @return $this 149 | */ 150 | protected function compileLimit($builder, Select $select) 151 | { 152 | $builder->limit($select->limit()->toInteger()); 153 | return $this; 154 | } 155 | 156 | /** 157 | * We need to add columns explicitly for UNION ALL subjects 158 | * because BelongsToMany cannot handle them correctly. 159 | * 160 | * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder $query 161 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation|\Illuminate\Database\Query\Builder 162 | */ 163 | protected function addSelectForUnionAll($query) 164 | { 165 | static $invoker; 166 | if (!$invoker) { 167 | $invoker = function () { 168 | return $this->shouldSelect($this->getBaseQuery()->columns ? [] : ['*']); 169 | }; 170 | } 171 | return $query instanceof BelongsToMany 172 | ? $query->addSelect($invoker->bindTo($query, $query)->__invoke()) 173 | : $query; 174 | } 175 | 176 | /** 177 | * We need to transform aliased columns into non-aliased form 178 | * because SQL standard does not allow column aliases in WHERE conditions. 179 | * 180 | * @param string $column 181 | * @return string 182 | */ 183 | protected function transformPivotColumn($column) 184 | { 185 | return $this->builder instanceof BelongsToMany && strpos($column, 'pivot_') === 0 186 | ? ($this->builder->getTable() . '.' . substr($column, 6)) 187 | : $column; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Processor.php: -------------------------------------------------------------------------------- 1 | builder = $query->builder(); 36 | return parent::process($query, $rows); 37 | } 38 | 39 | /** 40 | * Return comparable value from a row. 41 | * 42 | * @param mixed $row 43 | * @param string $column 44 | * @return int|string 45 | */ 46 | protected function field($row, $column) 47 | { 48 | $column = static::dropTablePrefix($column); 49 | if ($this->builder instanceof BelongsToMany && strpos($column, 'pivot_') === 0) { 50 | return $this->pivotField($row, substr($column, 6), $this->pivotAccessor()); 51 | } 52 | $value = $row->$column; 53 | return is_object($value) ? (string)$value : $value; 54 | } 55 | 56 | /** 57 | * Extract pivot from a row. 58 | * 59 | * @param mixed $row 60 | * @param string $column 61 | * @param string $accessor 62 | * @throws \Exception 63 | * @return int|string 64 | */ 65 | protected function pivotField($row, $column, $accessor) 66 | { 67 | $pivot = $row->$accessor; 68 | if (!isset($pivot->$column)) { 69 | throw new \Exception("The column `$column` is not included in the pivot \"$accessor\"."); 70 | } 71 | return $this->field($pivot, $column); 72 | } 73 | 74 | /** 75 | * Extract pivot accessor from a relation. 76 | * 77 | * @return string 78 | */ 79 | protected function pivotAccessor() 80 | { 81 | return $this->builder->getPivotAccessor(); 82 | } 83 | 84 | /** 85 | * Return the n-th element of collection. 86 | * Must return null if not exists. 87 | * 88 | * @param Collection|Model[]|object[] $rows 89 | * @param int $offset 90 | * @return Model|object 91 | */ 92 | protected function offset($rows, $offset) 93 | { 94 | return isset($rows[$offset]) ? $rows[$offset] : null; 95 | } 96 | 97 | /** 98 | * Slice rows, like PHP function array_slice(). 99 | * 100 | * @param Collection|Model[]|object[] $rows 101 | * @param int $offset 102 | * @param null|int $length 103 | * @return Collection|Model[]|object[] 104 | */ 105 | protected function slice($rows, $offset, $length = null) 106 | { 107 | return $rows->slice($offset, $length)->values(); 108 | } 109 | 110 | /** 111 | * Count rows, like PHP function count(). 112 | * 113 | * @param Collection|Model[]|object[] $rows 114 | * @return int 115 | */ 116 | protected function count($rows) 117 | { 118 | return $rows->count(); 119 | } 120 | 121 | /** 122 | * Reverse rows, like PHP function array_reverse(). 123 | * 124 | * @param Collection|Model[]|object[] $rows 125 | * @return Collection|Model[]|object[] 126 | */ 127 | protected function reverse($rows) 128 | { 129 | return $rows->reverse()->values(); 130 | } 131 | 132 | /** 133 | * Format result. 134 | * 135 | * @param Collection|Model[]|object[] $rows 136 | * @param array $meta 137 | * @param Query $query 138 | * @return PaginationResult 139 | */ 140 | protected function defaultFormat($rows, array $meta, Query $query) 141 | { 142 | return new PaginationResult($rows, $meta); 143 | } 144 | 145 | /** 146 | * Drop table prefix on column name. 147 | * 148 | * @param string $column 149 | * @return string 150 | */ 151 | protected static function dropTablePrefix(string $column) 152 | { 153 | $segments = explode('.', $column); 154 | 155 | return end($segments); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/EloquentDate.php: -------------------------------------------------------------------------------- 1 | toJSON(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/FormatterTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Post::class, $query->builder()->getModel()); 18 | $meta['foo'] = 'bar'; 19 | return new Collection([ 20 | 'records' => $rows, 21 | 'meta' => $meta, 22 | ]); 23 | }); 24 | $result = Post::lampager()->orderBy('id')->paginate(); 25 | $this->assertInstanceOf(Collection::class, $result); 26 | $this->assertEquals('bar', $result['meta']['foo']); 27 | } finally { 28 | Processor::restoreDefaultFormatter(); 29 | } 30 | } 31 | 32 | #[Test] 33 | public function testInstanceCustomFormatter(): void 34 | { 35 | $pager = Post::lampager(); 36 | try { 37 | $result = $pager->orderBy('id')->useFormatter(function ($rows, $meta, Query $query) { 38 | $this->assertInstanceOf(Post::class, $query->builder()->getModel()); 39 | $meta['foo'] = 'bar'; 40 | return new Collection([ 41 | 'records' => $rows, 42 | 'meta' => $meta, 43 | ]); 44 | })->paginate(); 45 | $this->assertInstanceOf(Collection::class, $result); 46 | $this->assertEquals('bar', $result['meta']['foo']); 47 | } finally { 48 | $pager->restoreFormatter(); 49 | } 50 | } 51 | 52 | #[Test] 53 | public function testInvalidFormatter(): void 54 | { 55 | $this->expectException(\InvalidArgumentException::class); 56 | Post::lampager()->useProcessor(function () {}); 57 | } 58 | 59 | #[Test] 60 | public function testInvalidProcessor(): void 61 | { 62 | $this->expectException(\InvalidArgumentException::class); 63 | Post::lampager()->useFormatter(__CLASS__); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/JsonResource.php: -------------------------------------------------------------------------------- 1 | belongsTo(Post::class)->lampager()->orderBy('id')->build()->toSql(); 13 | $x = (new Post())->lampager()->orderBy('id')->build()->toSql(); 14 | $y = (new Post())->newQuery()->getQuery()->lampager()->orderBy('id')->build()->toSql(); 15 | $this->assertEquals($x, $y); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/MySQLGrammarTest.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'mysql'); 17 | } 18 | 19 | protected function setUp(): void 20 | { 21 | BaseTestCase::setUp(); 22 | } 23 | 24 | /** 25 | * @param $expected 26 | * @param $actual 27 | */ 28 | protected function assertSqlEquals($expected, $actual): void 29 | { 30 | $formatter = new Formatter(); 31 | $this->assertEquals($formatter->format($expected), $formatter->format($actual)); 32 | } 33 | 34 | #[Test] 35 | public function testAscendingForwardStart(): void 36 | { 37 | $builder = Post::whereUserId(2) 38 | ->lampager() 39 | ->forward()->limit(3) 40 | ->orderBy('updated_at') 41 | ->orderBy('created_at') 42 | ->orderBy('id') 43 | ->seekable() 44 | ->build(); 45 | $this->assertSqlEquals(' 46 | select * from `posts` 47 | where `user_id` = ? 48 | order by `updated_at` asc, `created_at` asc, `id` asc 49 | limit 4 50 | ', $builder->toSql()); 51 | } 52 | 53 | #[Test] 54 | public function testAscendingForwardInclusive(): void 55 | { 56 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 57 | $builder = Post::whereUserId(2) 58 | ->lampager() 59 | ->forward()->limit(3) 60 | ->orderBy('updated_at') 61 | ->orderBy('created_at') 62 | ->orderBy('id') 63 | ->seekable() 64 | ->build($cursor); 65 | $this->assertSqlEquals(' 66 | ( 67 | select * from `posts` 68 | where `user_id` = ? AND ( 69 | `updated_at` = ? AND `created_at` = ? AND `id` < ? OR 70 | `updated_at` = ? AND `created_at` < ? OR 71 | `updated_at` < ? 72 | ) 73 | order by `updated_at` desc, `created_at` desc, `id` desc 74 | limit 1 75 | ) 76 | union all 77 | ( 78 | select * from `posts` 79 | where `user_id` = ? AND ( 80 | `updated_at` = ? AND `created_at` = ? AND `id` >= ? OR 81 | `updated_at` = ? AND `created_at` > ? OR 82 | `updated_at` > ? 83 | ) 84 | order by `updated_at` asc, `created_at` asc, `id` asc 85 | limit 4 86 | ) 87 | ', $builder->toSql()); 88 | } 89 | 90 | #[Test] 91 | public function testAscendingForwardExclusive(): void 92 | { 93 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 94 | $builder = Post::whereUserId(2) 95 | ->lampager() 96 | ->forward()->limit(3) 97 | ->orderBy('updated_at') 98 | ->orderBy('created_at') 99 | ->orderBy('id') 100 | ->seekable() 101 | ->exclusive() 102 | ->build($cursor); 103 | $this->assertSqlEquals(' 104 | ( 105 | select * from `posts` 106 | where `user_id` = ? AND ( 107 | `updated_at` = ? AND `created_at` = ? AND `id` <= ? OR 108 | `updated_at` = ? AND `created_at` < ? OR 109 | `updated_at` < ? 110 | ) 111 | order by `updated_at` desc, `created_at` desc, `id` desc 112 | limit 1 113 | ) 114 | union all 115 | ( 116 | select * from `posts` 117 | where `user_id` = ? AND ( 118 | `updated_at` = ? AND `created_at` = ? AND `id` > ? OR 119 | `updated_at` = ? AND `created_at` > ? OR 120 | `updated_at` > ? 121 | ) 122 | order by `updated_at` asc, `created_at` asc, `id` asc 123 | limit 4 124 | ) 125 | ', $builder->toSql()); 126 | } 127 | 128 | #[Test] 129 | public function testAscendingBackwardStart(): void 130 | { 131 | $builder = Post::whereUserId(2) 132 | ->lampager() 133 | ->backward()->limit(3) 134 | ->orderBy('updated_at') 135 | ->orderBy('created_at') 136 | ->orderBy('id') 137 | ->seekable() 138 | ->build(); 139 | $this->assertSqlEquals(' 140 | select * from `posts` 141 | where `user_id` = ? 142 | order by `updated_at` desc, `created_at` desc, `id` desc 143 | limit 4 144 | ', $builder->toSql()); 145 | } 146 | 147 | #[Test] 148 | public function testAscendingBackwardInclusive(): void 149 | { 150 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 151 | $builder = Post::whereUserId(2) 152 | ->lampager() 153 | ->backward()->limit(3) 154 | ->orderBy('updated_at') 155 | ->orderBy('created_at') 156 | ->orderBy('id') 157 | ->seekable() 158 | ->build($cursor); 159 | $this->assertSqlEquals(' 160 | ( 161 | select * from `posts` 162 | where `user_id` = ? AND ( 163 | `updated_at` = ? AND `created_at` = ? AND `id` > ? OR 164 | `updated_at` = ? AND `created_at` > ? OR 165 | `updated_at` > ? 166 | ) 167 | order by `updated_at` asc, `created_at` asc, `id` asc 168 | limit 1 169 | ) 170 | union all 171 | ( 172 | select * from `posts` 173 | where `user_id` = ? AND ( 174 | `updated_at` = ? AND `created_at` = ? AND `id` <= ? OR 175 | `updated_at` = ? AND `created_at` < ? OR 176 | `updated_at` < ? 177 | ) 178 | order by `updated_at` desc, `created_at` desc, `id` desc 179 | limit 4 180 | ) 181 | ', $builder->toSql()); 182 | } 183 | 184 | #[Test] 185 | public function testAscendingBackwardExclusive(): void 186 | { 187 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 188 | $builder = Post::whereUserId(2) 189 | ->lampager() 190 | ->backward()->limit(3) 191 | ->orderBy('updated_at') 192 | ->orderBy('created_at') 193 | ->orderBy('id') 194 | ->seekable() 195 | ->exclusive() 196 | ->build($cursor); 197 | $this->assertSqlEquals(' 198 | ( 199 | select * from `posts` 200 | where `user_id` = ? AND ( 201 | `updated_at` = ? AND `created_at` = ? AND `id` >= ? OR 202 | `updated_at` = ? AND `created_at` > ? OR 203 | `updated_at` > ? 204 | ) 205 | order by `updated_at` asc, `created_at` asc, `id` asc 206 | limit 1 207 | ) 208 | union all 209 | ( 210 | select * from `posts` 211 | where `user_id` = ? AND ( 212 | `updated_at` = ? AND `created_at` = ? AND `id` < ? OR 213 | `updated_at` = ? AND `created_at` < ? OR 214 | `updated_at` < ? 215 | ) 216 | order by `updated_at` desc, `created_at` desc, `id` desc 217 | limit 4 218 | ) 219 | ', $builder->toSql()); 220 | } 221 | 222 | #[Test] 223 | public function testDescendingForwardStart(): void 224 | { 225 | $builder = Post::whereUserId(2) 226 | ->lampager() 227 | ->forward()->limit(3) 228 | ->orderByDesc('updated_at') 229 | ->orderByDesc('created_at') 230 | ->orderByDesc('id') 231 | ->seekable() 232 | ->build(); 233 | $this->assertSqlEquals(' 234 | select * from `posts` 235 | where `user_id` = ? 236 | order by `updated_at` desc, `created_at` desc, `id` desc 237 | limit 4 238 | ', $builder->toSql()); 239 | } 240 | 241 | #[Test] 242 | public function testDescendingForwardInclusive(): void 243 | { 244 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 245 | $builder = Post::whereUserId(2) 246 | ->lampager() 247 | ->forward()->limit(3) 248 | ->orderByDesc('updated_at') 249 | ->orderByDesc('created_at') 250 | ->orderByDesc('id') 251 | ->seekable() 252 | ->build($cursor); 253 | $this->assertSqlEquals(' 254 | ( 255 | select * from `posts` 256 | where `user_id` = ? AND ( 257 | `updated_at` = ? AND `created_at` = ? AND `id` > ? OR 258 | `updated_at` = ? AND `created_at` > ? OR 259 | `updated_at` > ? 260 | ) 261 | order by `updated_at` asc, `created_at` asc, `id` asc 262 | limit 1 263 | ) 264 | union all 265 | ( 266 | select * from `posts` 267 | where `user_id` = ? AND ( 268 | `updated_at` = ? AND `created_at` = ? AND `id` <= ? OR 269 | `updated_at` = ? AND `created_at` < ? OR 270 | `updated_at` < ? 271 | ) 272 | order by `updated_at` desc, `created_at` desc, `id` desc 273 | limit 4 274 | ) 275 | ', $builder->toSql()); 276 | } 277 | 278 | #[Test] 279 | public function testDescendingForwardExclusive(): void 280 | { 281 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 282 | $builder = Post::whereUserId(2) 283 | ->lampager() 284 | ->forward()->limit(3) 285 | ->orderByDesc('updated_at') 286 | ->orderByDesc('created_at') 287 | ->orderByDesc('id') 288 | ->seekable() 289 | ->exclusive() 290 | ->build($cursor); 291 | $this->assertSqlEquals(' 292 | ( 293 | select * from `posts` 294 | where `user_id` = ? AND ( 295 | `updated_at` = ? AND `created_at` = ? AND `id` >= ? OR 296 | `updated_at` = ? AND `created_at` > ? OR 297 | `updated_at` > ? 298 | ) 299 | order by `updated_at` asc, `created_at` asc, `id` asc 300 | limit 1 301 | ) 302 | union all 303 | ( 304 | select * from `posts` 305 | where `user_id` = ? AND ( 306 | `updated_at` = ? AND `created_at` = ? AND `id` < ? OR 307 | `updated_at` = ? AND `created_at` < ? OR 308 | `updated_at` < ? 309 | ) 310 | order by `updated_at` desc, `created_at` desc, `id` desc 311 | limit 4 312 | ) 313 | ', $builder->toSql()); 314 | } 315 | 316 | #[Test] 317 | public function testDescendingBackwardStart(): void 318 | { 319 | $builder = Post::whereUserId(2) 320 | ->lampager() 321 | ->backward()->limit(3) 322 | ->orderByDesc('updated_at') 323 | ->orderByDesc('created_at') 324 | ->orderByDesc('id') 325 | ->seekable() 326 | ->build(); 327 | $this->assertSqlEquals(' 328 | select * from `posts` 329 | where `user_id` = ? 330 | order by `updated_at` asc, `created_at` asc, `id` asc 331 | limit 4 332 | ', $builder->toSql()); 333 | } 334 | 335 | #[Test] 336 | public function testDescendingBackwardInclusive(): void 337 | { 338 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 339 | $builder = Post::whereUserId(2) 340 | ->lampager() 341 | ->backward()->limit(3) 342 | ->orderByDesc('updated_at') 343 | ->orderByDesc('created_at') 344 | ->orderByDesc('id') 345 | ->seekable() 346 | ->build($cursor); 347 | $this->assertSqlEquals(' 348 | ( 349 | select * from `posts` 350 | where `user_id` = ? AND ( 351 | `updated_at` = ? AND `created_at` = ? AND `id` < ? OR 352 | `updated_at` = ? AND `created_at` < ? OR 353 | `updated_at` < ? 354 | ) 355 | order by `updated_at` desc, `created_at` desc, `id` desc 356 | limit 1 357 | ) 358 | union all 359 | ( 360 | select * from `posts` 361 | where `user_id` = ? AND ( 362 | `updated_at` = ? AND `created_at` = ? AND `id` >= ? OR 363 | `updated_at` = ? AND `created_at` > ? OR 364 | `updated_at` > ? 365 | ) 366 | order by `updated_at` asc, `created_at` asc, `id` asc 367 | limit 4 368 | ) 369 | ', $builder->toSql()); 370 | } 371 | 372 | #[Test] 373 | public function testDescendingBackwardExclusive(): void 374 | { 375 | $cursor = ['updated_at' => '', 'created_at' => '', 'id' => '']; 376 | $builder = Post::whereUserId(2) 377 | ->lampager() 378 | ->backward()->limit(3) 379 | ->orderByDesc('updated_at') 380 | ->orderByDesc('created_at') 381 | ->orderByDesc('id') 382 | ->seekable() 383 | ->exclusive() 384 | ->build($cursor); 385 | $this->assertSqlEquals(' 386 | ( 387 | select * from `posts` 388 | where `user_id` = ? AND ( 389 | `updated_at` = ? AND `created_at` = ? AND `id` <= ? OR 390 | `updated_at` = ? AND `created_at` < ? OR 391 | `updated_at` < ? 392 | ) 393 | order by `updated_at` desc, `created_at` desc, `id` desc 394 | limit 1 395 | ) 396 | union all 397 | ( 398 | select * from `posts` 399 | where `user_id` = ? AND ( 400 | `updated_at` = ? AND `created_at` = ? AND `id` > ? OR 401 | `updated_at` = ? AND `created_at` > ? OR 402 | `updated_at` > ? 403 | ) 404 | order by `updated_at` asc, `created_at` asc, `id` asc 405 | limit 4 406 | ) 407 | ', $builder->toSql()); 408 | } 409 | 410 | #[Test] 411 | public function testBelongsToManyOrderByPivot(): void 412 | { 413 | $cursor = ['pivot_id' => 2]; 414 | 415 | $tag = new Tag(); 416 | $tag->id = 1; 417 | $tag->exists = true; 418 | 419 | $builder = $tag->posts()->withPivot('id') 420 | ->lampager() 421 | ->forward()->limit(3) 422 | ->orderBy('pivot_id') 423 | ->seekable() 424 | ->build($cursor); 425 | 426 | $this->assertSqlEquals(' 427 | ( 428 | select * from `posts` 429 | inner join `post_tag` on `posts`.`id` = `post_tag`.`post_id` 430 | where `post_tag`.`tag_id` = ? AND ( 431 | `post_tag`.`id` < ? 432 | ) 433 | order by `pivot_id` desc 434 | limit 1 435 | ) 436 | union all 437 | ( 438 | select 439 | `posts`.*, 440 | `post_tag`.`tag_id` as `pivot_tag_id`, 441 | `post_tag`.`post_id` as `pivot_post_id`, 442 | `post_tag`.`id` as `pivot_id` 443 | from `posts` 444 | inner join `post_tag` on `posts`.`id` = `post_tag`.`post_id` 445 | where `post_tag`.`tag_id` = ? AND ( 446 | `post_tag`.`id` >= ? 447 | ) 448 | order by `pivot_id` asc 449 | limit 4 450 | ) 451 | ', $builder->toSql()); 452 | } 453 | 454 | #[Test] 455 | public function testBelongsToManyOrderBySource(): void 456 | { 457 | $cursor = ['posts.id' => 2]; 458 | 459 | $tag = new Tag(); 460 | $tag->id = 1; 461 | $tag->exists = true; 462 | 463 | $builder = $tag->posts()->withPivot('id') 464 | ->lampager() 465 | ->forward()->limit(3) 466 | ->orderBy('posts.id') 467 | ->seekable() 468 | ->build($cursor); 469 | 470 | $this->assertSqlEquals(' 471 | ( 472 | select * from `posts` 473 | inner join `post_tag` on `posts`.`id` = `post_tag`.`post_id` 474 | where `post_tag`.`tag_id` = ? AND ( 475 | `posts`.`id` < ? 476 | ) 477 | order by `posts`.`id` desc 478 | limit 1 479 | ) 480 | union all 481 | ( 482 | select 483 | `posts`.*, 484 | `post_tag`.`tag_id` as `pivot_tag_id`, 485 | `post_tag`.`post_id` as `pivot_post_id`, 486 | `post_tag`.`id` as `pivot_id` 487 | from `posts` 488 | inner join `post_tag` on `posts`.`id` = `post_tag`.`post_id` 489 | where `post_tag`.`tag_id` = ? AND ( 490 | `posts`.`id` >= ? 491 | ) 492 | order by `posts`.`id` asc 493 | limit 4 494 | ) 495 | ', $builder->toSql()); 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /tests/PaginationResultTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 17 | json_decode(json_encode($expected), true), 18 | json_decode(json_encode($actual), true) 19 | ); 20 | } 21 | 22 | #[Test] 23 | public function testMacroCall(): void 24 | { 25 | PaginationResult::macro('meta', function () { 26 | $vars = $this->toArray(); 27 | unset($vars['records']); 28 | return $vars; 29 | }); 30 | 31 | $this->assertResultSame( 32 | [ 33 | 'has_previous' => true, 34 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 1], 35 | 'has_next' => true, 36 | 'next_cursor' => ['updated_at' => '2017-01-01 11:00:00', 'id' => 4], 37 | ], 38 | Post::lampager() 39 | ->forward()->limit(3) 40 | ->orderBy('updated_at') 41 | ->orderBy('id') 42 | ->seekable() 43 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 44 | ->meta() 45 | ); 46 | } 47 | 48 | #[Test] 49 | public function testCollectionCall(): void 50 | { 51 | $result = Post::lampager() 52 | ->forward()->limit(3) 53 | ->orderBy('updated_at') 54 | ->orderBy('id') 55 | ->seekable() 56 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']); 57 | 58 | $this->assertResultSame( 59 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 60 | $result->first() 61 | ); 62 | } 63 | 64 | #[Test] 65 | public function testJsonEncodeWithOption(): void 66 | { 67 | $actual = Post::lampager() 68 | ->forward()->limit(3) 69 | ->orderBy('updated_at') 70 | ->orderBy('id') 71 | ->seekable() 72 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 73 | ->toJson(JSON_PRETTY_PRINT); 74 | 75 | $format = [EloquentDate::class, 'format']; 76 | 77 | $expected = <<assertSame($expected, $actual); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Post.php: -------------------------------------------------------------------------------- 1 | 'int', 25 | 'updated_at' => 'datetime', 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /tests/PostResource.php: -------------------------------------------------------------------------------- 1 | true, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/PostResourceCollection.php: -------------------------------------------------------------------------------- 1 | 'int', 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /tests/ProcessorTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 16 | json_decode(json_encode($expected), true), 17 | json_decode(json_encode($actual), true) 18 | ); 19 | } 20 | 21 | #[Test] 22 | public function testAscendingForwardStartInclusive(): void 23 | { 24 | $this->assertResultSame( 25 | [ 26 | 'records' => [ 27 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 28 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 29 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 30 | ], 31 | 'has_previous' => null, 32 | 'previous_cursor' => null, 33 | 'has_next' => true, 34 | 'next_cursor' => ['updated_at' => '2017-01-01 11:00:00', 'id' => 2], 35 | ], 36 | Post::lampager() 37 | ->forward()->limit(3) 38 | ->orderBy('updated_at') 39 | ->orderBy('id') 40 | ->seekable() 41 | ->paginate() 42 | ); 43 | } 44 | 45 | #[Test] 46 | public function testAscendingForwardStartExclusive(): void 47 | { 48 | $this->assertResultSame( 49 | [ 50 | 'records' => [ 51 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 52 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 53 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 54 | ], 55 | 'has_previous' => null, 56 | 'previous_cursor' => null, 57 | 'has_next' => true, 58 | 'next_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 59 | ], 60 | Post::lampager() 61 | ->forward()->limit(3) 62 | ->orderBy('updated_at') 63 | ->orderBy('id') 64 | ->seekable() 65 | ->exclusive() 66 | ->paginate() 67 | ); 68 | } 69 | 70 | #[Test] 71 | public function testAscendingForwardInclusive(): void 72 | { 73 | $this->assertResultSame( 74 | [ 75 | 'records' => [ 76 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 77 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 78 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 79 | ], 80 | 'has_previous' => true, 81 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 1], 82 | 'has_next' => true, 83 | 'next_cursor' => ['updated_at' => '2017-01-01 11:00:00', 'id' => 4], 84 | ], 85 | Post::lampager() 86 | ->forward()->limit(3) 87 | ->orderBy('updated_at') 88 | ->orderBy('id') 89 | ->seekable() 90 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 91 | ); 92 | } 93 | 94 | #[Test] 95 | public function testAscendingForwardExclusive(): void 96 | { 97 | $this->assertResultSame( 98 | [ 99 | 'records' => [ 100 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 101 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 102 | ['id' => 4, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 103 | ], 104 | 'has_previous' => true, 105 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 106 | 'has_next' => false, 107 | 'next_cursor' => null, 108 | ], 109 | Post::lampager() 110 | ->forward()->limit(3) 111 | ->orderBy('updated_at') 112 | ->orderBy('id') 113 | ->seekable() 114 | ->exclusive() 115 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 116 | ); 117 | } 118 | 119 | #[Test] 120 | public function testAscendingBackwardStartInclusive(): void 121 | { 122 | $this->assertResultSame( 123 | [ 124 | 'records' => [ 125 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 126 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 127 | ['id' => 4, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 128 | ], 129 | 'has_previous' => true, 130 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 3], 131 | 'has_next' => null, 132 | 'next_cursor' => null, 133 | ], 134 | Post::lampager() 135 | ->backward()->limit(3) 136 | ->orderBy('updated_at') 137 | ->orderBy('id') 138 | ->seekable() 139 | ->paginate() 140 | ); 141 | } 142 | 143 | #[Test] 144 | public function testAscendingBackwardStartExclusive(): void 145 | { 146 | $this->assertResultSame( 147 | [ 148 | 'records' => [ 149 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 150 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 151 | ['id' => 4, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 152 | ], 153 | 'has_previous' => true, 154 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 155 | 'has_next' => null, 156 | 'next_cursor' => null, 157 | ], 158 | Post::lampager() 159 | ->backward()->limit(3) 160 | ->orderBy('updated_at') 161 | ->orderBy('id') 162 | ->seekable() 163 | ->exclusive() 164 | ->paginate() 165 | ); 166 | } 167 | 168 | #[Test] 169 | public function testAscendingBackwardInclusive(): void 170 | { 171 | $this->assertResultSame( 172 | [ 173 | 'records' => [ 174 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 175 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 176 | ], 177 | 'has_previous' => false, 178 | 'previous_cursor' => null, 179 | 'has_next' => true, 180 | 'next_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 181 | ], 182 | Post::lampager() 183 | ->backward()->limit(3) 184 | ->orderBy('updated_at') 185 | ->orderBy('id') 186 | ->seekable() 187 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 188 | ); 189 | } 190 | 191 | #[Test] 192 | public function testAscendingBackwardExclusive(): void 193 | { 194 | $this->assertResultSame( 195 | [ 196 | 'records' => [ 197 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 198 | ], 199 | 'has_previous' => false, 200 | 'previous_cursor' => null, 201 | 'has_next' => true, 202 | 'next_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 1], 203 | ], 204 | Post::lampager() 205 | ->backward()->limit(3) 206 | ->orderBy('updated_at') 207 | ->orderBy('id') 208 | ->seekable() 209 | ->exclusive() 210 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 211 | ); 212 | } 213 | 214 | #[Test] 215 | public function testDescendingForwardStartInclusive(): void 216 | { 217 | $this->assertResultSame( 218 | [ 219 | 'records' => [ 220 | ['id' => 4, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 221 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 222 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 223 | ], 224 | 'has_previous' => null, 225 | 'previous_cursor' => null, 226 | 'has_next' => true, 227 | 'next_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 3], 228 | ], 229 | Post::lampager() 230 | ->forward()->limit(3) 231 | ->orderByDesc('updated_at') 232 | ->orderByDesc('id') 233 | ->seekable() 234 | ->paginate() 235 | ); 236 | } 237 | 238 | #[Test] 239 | public function testDescendingForwardStartExclusive(): void 240 | { 241 | $this->assertResultSame( 242 | [ 243 | 'records' => [ 244 | ['id' => 4, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 245 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 246 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 247 | ], 248 | 'has_previous' => null, 249 | 'previous_cursor' => null, 250 | 'has_next' => true, 251 | 'next_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 252 | ], 253 | Post::lampager() 254 | ->forward()->limit(3) 255 | ->orderByDesc('updated_at') 256 | ->orderByDesc('id') 257 | ->seekable() 258 | ->exclusive() 259 | ->paginate() 260 | ); 261 | } 262 | 263 | #[Test] 264 | public function testDescendingForwardInclusive(): void 265 | { 266 | $this->assertResultSame( 267 | [ 268 | 'records' => [ 269 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 270 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 271 | ], 272 | 'has_previous' => true, 273 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 274 | 'has_next' => false, 275 | 'next_cursor' => null, 276 | ], 277 | Post::lampager() 278 | ->forward()->limit(3) 279 | ->orderByDesc('updated_at') 280 | ->orderByDesc('id') 281 | ->seekable() 282 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 283 | ); 284 | } 285 | 286 | #[Test] 287 | public function testDescendingForwardExclusive(): void 288 | { 289 | $this->assertResultSame( 290 | [ 291 | 'records' => [ 292 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 293 | ], 294 | 'has_previous' => true, 295 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 1], 296 | 'has_next' => false, 297 | 'next_cursor' => null, 298 | ], 299 | Post::lampager() 300 | ->forward()->limit(3) 301 | ->orderByDesc('updated_at') 302 | ->orderByDesc('id') 303 | ->seekable() 304 | ->exclusive() 305 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 306 | ); 307 | } 308 | 309 | #[Test] 310 | public function testDescendingBackwardStartInclusive(): void 311 | { 312 | $this->assertResultSame( 313 | [ 314 | 'records' => [ 315 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 316 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 317 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 318 | ], 319 | 'has_previous' => true, 320 | 'previous_cursor' => ['updated_at' => '2017-01-01 11:00:00', 'id' => 2], 321 | 'has_next' => null, 322 | 'next_cursor' => null, 323 | ], 324 | Post::lampager() 325 | ->backward()->limit(3) 326 | ->orderByDesc('updated_at') 327 | ->orderByDesc('id') 328 | ->seekable() 329 | ->paginate() 330 | ); 331 | } 332 | 333 | #[Test] 334 | public function testDescendingBackwardStartExclusive(): void 335 | { 336 | $this->assertResultSame( 337 | [ 338 | 'records' => [ 339 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 340 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 341 | ['id' => 1, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 342 | ], 343 | 'has_previous' => true, 344 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 345 | 'has_next' => null, 346 | 'next_cursor' => null, 347 | ], 348 | Post::lampager() 349 | ->backward()->limit(3) 350 | ->orderByDesc('updated_at') 351 | ->orderByDesc('id') 352 | ->seekable() 353 | ->exclusive() 354 | ->paginate() 355 | ); 356 | } 357 | 358 | #[Test] 359 | public function testDescendingBackwardInclusive(): void 360 | { 361 | $this->assertResultSame( 362 | [ 363 | 'records' => [ 364 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 365 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 366 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 367 | ], 368 | 'has_previous' => true, 369 | 'previous_cursor' => ['updated_at' => '2017-01-01 11:00:00', 'id' => 4], 370 | 'has_next' => true, 371 | 'next_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 1], 372 | ], 373 | Post::lampager() 374 | ->backward()->limit(3) 375 | ->orderByDesc('updated_at') 376 | ->orderByDesc('id') 377 | ->seekable() 378 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 379 | ); 380 | } 381 | 382 | #[Test] 383 | public function testDescendingBackwardExclusive(): void 384 | { 385 | $this->assertResultSame( 386 | [ 387 | 'records' => [ 388 | ['id' => 4, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 389 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 390 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 391 | ], 392 | 'has_previous' => false, 393 | 'previous_cursor' => null, 394 | 'has_next' => true, 395 | 'next_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 5], 396 | ], 397 | Post::lampager() 398 | ->backward()->limit(3) 399 | ->orderByDesc('updated_at') 400 | ->orderByDesc('id') 401 | ->seekable() 402 | ->exclusive() 403 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']) 404 | ); 405 | } 406 | 407 | #[Test] 408 | public function testBelongsToManyOrderByPivot(): void 409 | { 410 | $this->assertResultSame( 411 | [ 412 | 'records' => [ 413 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 414 | ['id' => 5, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 415 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 416 | ], 417 | 'has_previous' => true, 418 | 'previous_cursor' => ['pivot_id' => 1], 419 | 'has_next' => true, 420 | 'next_cursor' => ['pivot_id' => 5], 421 | ], 422 | Tag::find(1)->posts()->withPivot('id') 423 | ->lampager() 424 | ->forward()->limit(3) 425 | ->orderBy('pivot_id') 426 | ->seekable() 427 | ->paginate(['pivot_id' => 2]) 428 | ); 429 | } 430 | 431 | #[Test] 432 | public function testBelongsToManyOrderBySource(): void 433 | { 434 | $this->assertResultSame( 435 | [ 436 | 'records' => [ 437 | ['id' => 2, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 438 | ['id' => 3, 'updated_at' => EloquentDate::format('2017-01-01 10:00:00')], 439 | ['id' => 4, 'updated_at' => EloquentDate::format('2017-01-01 11:00:00')], 440 | ], 441 | 'has_previous' => true, 442 | 'previous_cursor' => ['posts.id' => 1], 443 | 'has_next' => true, 444 | 'next_cursor' => ['posts.id' => 5], 445 | ], 446 | Tag::find(1)->posts()->withPivot('id') 447 | ->lampager() 448 | ->forward()->limit(3) 449 | ->orderBy('posts.id') 450 | ->seekable() 451 | ->paginate(['posts.id' => 2]) 452 | ); 453 | } 454 | 455 | #[Test] 456 | public function testBelongsToManyWithoutPivotKey(): void 457 | { 458 | $this->expectException(\Exception::class); 459 | $this->expectExceptionMessage('The column `id` is not included in the pivot "pivot".'); 460 | 461 | Tag::find(1)->posts() 462 | ->lampager() 463 | ->forward()->limit(3) 464 | ->orderBy('pivot_id') 465 | ->seekable() 466 | ->paginate(); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /tests/ResourceTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 19 | json_decode(json_encode($expected), true), 20 | json_decode(json_encode($actual), true) 21 | ); 22 | } 23 | 24 | /** 25 | * @return \Lampager\Laravel\PaginationResult 26 | */ 27 | protected function getLampagerPagination() 28 | { 29 | return Post::lampager() 30 | ->forward()->limit(3) 31 | ->orderBy('updated_at') 32 | ->orderBy('id') 33 | ->seekable() 34 | ->paginate(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']); 35 | } 36 | 37 | /** 38 | * @return \Illuminate\Contracts\Pagination\Paginator 39 | */ 40 | protected function getStandardPagination() 41 | { 42 | return Post::query() 43 | ->where('id', '>', 1) 44 | ->orderBy('updated_at') 45 | ->orderBy('id') 46 | ->simplePaginate(3); 47 | } 48 | 49 | #[Test] 50 | public function testRawArrayOutput(): void 51 | { 52 | $expected = [ 53 | [ 54 | 'id' => 3, 55 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 56 | 'post_resource' => true, 57 | ], 58 | [ 59 | 'id' => 5, 60 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 61 | 'post_resource' => true, 62 | ], 63 | [ 64 | 'id' => 2, 65 | 'updated_at' => EloquentDate::format('2017-01-01 11:00:00'), 66 | 'post_resource' => true, 67 | ], 68 | ]; 69 | 70 | $pagination = $this->getLampagerPagination(); 71 | $records = $pagination->records; 72 | $standardPagination = $this->getStandardPagination(); 73 | 74 | $this->assertResultSame($expected, (new PostResourceCollection($pagination))->resolve()); 75 | $this->assertResultSame($expected, (new PostResourceCollection($records))->resolve()); 76 | $this->assertResultSame($expected, (new PostResourceCollection($standardPagination))->resolve()); 77 | } 78 | 79 | #[Test] 80 | public function testStructuredArrayOutput(): void 81 | { 82 | $expected = [ 83 | 'data' => [ 84 | [ 85 | 'id' => 3, 86 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 87 | 'post_resource' => true, 88 | ], 89 | [ 90 | 'id' => 5, 91 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 92 | 'post_resource' => true, 93 | ], 94 | [ 95 | 'id' => 2, 96 | 'updated_at' => EloquentDate::format('2017-01-01 11:00:00'), 97 | 'post_resource' => true, 98 | ], 99 | ], 100 | 'post_resource_collection' => true, 101 | ]; 102 | 103 | $pagination = $this->getLampagerPagination(); 104 | $records = $pagination->records; 105 | $standardPagination = $this->getStandardPagination(); 106 | 107 | $this->assertResultSame($expected, (new StructuredPostResourceCollection($pagination))->resolve()); 108 | $this->assertResultSame($expected, (new StructuredPostResourceCollection($records))->resolve()); 109 | $this->assertResultSame($expected, (new StructuredPostResourceCollection($standardPagination))->resolve()); 110 | 111 | $this->assertResultSame($expected, (new StructuredPostResourceCollection($records)) 112 | ->toResponse(Request::create('/'))->getData() 113 | ); 114 | $this->assertResultSame($expected, (new PostResourceCollection($records)) 115 | ->additional(['post_resource_collection' => true]) 116 | ->toResponse(Request::create('/'))->getData() 117 | ); 118 | } 119 | 120 | #[Test] 121 | public function testLampagerPaginationOutput(): void 122 | { 123 | $expected1 = [ 124 | 'data' => [ 125 | [ 126 | 'id' => 3, 127 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 128 | 'post_resource' => true, 129 | ], 130 | [ 131 | 'id' => 5, 132 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 133 | 'post_resource' => true, 134 | ], 135 | [ 136 | 'id' => 2, 137 | 'updated_at' => EloquentDate::format('2017-01-01 11:00:00'), 138 | 'post_resource' => true, 139 | ], 140 | ], 141 | 'post_resource_collection' => true, 142 | 'has_previous' => true, 143 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 1], 144 | 'has_next' => true, 145 | 'next_cursor' => ['updated_at' => '2017-01-01 11:00:00', 'id' => 4], 146 | ]; 147 | // different order 148 | $expected2 = Arr::except($expected1, 'post_resource_collection') + ['post_resource_collection' => true]; 149 | 150 | $pagination = $this->getLampagerPagination(); 151 | 152 | $this->assertResultSame($expected1, (new StructuredPostResourceCollection($pagination)) 153 | ->toResponse(Request::create('/'))->getData() 154 | ); 155 | $this->assertResultSame($expected2, (new PostResourceCollection($pagination)) 156 | ->additional(['post_resource_collection' => true]) 157 | ->toResponse(Request::create('/'))->getData() 158 | ); 159 | } 160 | 161 | #[Test] 162 | public function testStandardPaginationOutput(): void 163 | { 164 | $expected1 = [ 165 | 'data' => [ 166 | [ 167 | 'id' => 3, 168 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 169 | 'post_resource' => true, 170 | ], 171 | [ 172 | 'id' => 5, 173 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 174 | 'post_resource' => true, 175 | ], 176 | [ 177 | 'id' => 2, 178 | 'updated_at' => EloquentDate::format('2017-01-01 11:00:00'), 179 | 'post_resource' => true, 180 | ], 181 | ], 182 | 'post_resource_collection' => true, 183 | 'links' => [ 184 | 'first' => 'http://localhost?page=1', 185 | 'last' => null, 186 | 'prev' => null, 187 | 'next' => 'http://localhost?page=2', 188 | ], 189 | 'meta' => [ 190 | 'current_page' => 1, 191 | 'from' => 1, 192 | 'path' => 'http://localhost', 193 | 'per_page' => 3, 194 | 'to' => 3, 195 | ], 196 | ]; 197 | // different order 198 | $expected2 = Arr::except($expected1, 'post_resource_collection') + ['post_resource_collection' => true]; 199 | 200 | $pagination = $this->getStandardPagination(); 201 | 202 | $this->assertResultSame($expected1, (new StructuredPostResourceCollection($pagination)) 203 | ->toResponse(Request::create('/'))->getData() 204 | ); 205 | $this->assertResultSame($expected2, (new PostResourceCollection($pagination)) 206 | ->additional(['post_resource_collection' => true]) 207 | ->toResponse(Request::create('/'))->getData() 208 | ); 209 | } 210 | 211 | #[Test] 212 | public function testMissingValue(): void 213 | { 214 | $expected = ['id' => 1]; 215 | $actual = (new TagResource(Tag::find(1)))->resolve(); 216 | 217 | $this->assertResultSame($expected, $actual); 218 | } 219 | 220 | #[Test] 221 | public function testAnonymousResourceCollection(): void 222 | { 223 | $collection = PostResource::collection($this->getLampagerPagination()); 224 | $this->assertInstanceOf(AnonymousResourceCollection::class, $collection); 225 | 226 | $expected = [ 227 | 'data' => [ 228 | [ 229 | 'id' => 3, 230 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 231 | 'post_resource' => true, 232 | ], 233 | [ 234 | 'id' => 5, 235 | 'updated_at' => EloquentDate::format('2017-01-01 10:00:00'), 236 | 'post_resource' => true, 237 | ], 238 | [ 239 | 'id' => 2, 240 | 'updated_at' => EloquentDate::format('2017-01-01 11:00:00'), 241 | 'post_resource' => true, 242 | ], 243 | ], 244 | 'has_previous' => true, 245 | 'previous_cursor' => ['updated_at' => '2017-01-01 10:00:00', 'id' => 1], 246 | 'has_next' => true, 247 | 'next_cursor' => ['updated_at' => '2017-01-01 11:00:00', 'id' => 4], 248 | ]; 249 | $this->assertResultSame($expected, $collection->toResponse(Request::create('/'))->getData()); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /tests/StructuredPostResourceCollection.php: -------------------------------------------------------------------------------- 1 | parent::toArray($request), 25 | 'post_resource_collection' => true, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Tag.php: -------------------------------------------------------------------------------- 1 | 'int', 21 | ]; 22 | 23 | public function posts() 24 | { 25 | return $this->belongsToMany(Post::class)->using(PostTagPivot::class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/TagResource.php: -------------------------------------------------------------------------------- 1 | new PostResourceCollection($this->whenLoaded('posts')), 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'test'); 18 | $app['config']->set('database.connections.test', [ 19 | 'driver' => 'sqlite', 20 | 'database' => ':memory:', 21 | ]); 22 | } 23 | 24 | /** 25 | * @param \Illuminate\Foundation\Application $app 26 | * @return array 27 | */ 28 | protected function getPackageProviders($app) 29 | { 30 | return [ 31 | MacroServiceProvider::class, 32 | ]; 33 | } 34 | 35 | protected function setUp(): void 36 | { 37 | parent::setUp(); 38 | 39 | Schema::create('posts', function (Blueprint $table) { 40 | $table->increments('id'); 41 | $table->datetime('updated_at'); 42 | }); 43 | Schema::create('tags', function (Blueprint $table) { 44 | $table->increments('id'); 45 | }); 46 | Schema::create('post_tag', function (Blueprint $table) { 47 | $table->increments('id'); 48 | $table->integer('post_id'); 49 | $table->integer('tag_id'); 50 | }); 51 | 52 | Post::create(['id' => 1, 'updated_at' => '2017-01-01 10:00:00']); 53 | Post::create(['id' => 3, 'updated_at' => '2017-01-01 10:00:00']); 54 | Post::create(['id' => 5, 'updated_at' => '2017-01-01 10:00:00']); 55 | Post::create(['id' => 2, 'updated_at' => '2017-01-01 11:00:00']); 56 | Post::create(['id' => 4, 'updated_at' => '2017-01-01 11:00:00']); 57 | 58 | Tag::create(['id' => 1])->posts()->sync([1, 3, 5, 2, 4]); 59 | } 60 | } 61 | --------------------------------------------------------------------------------