├── .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 |
3 |
4 |
5 |
6 |
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 |
--------------------------------------------------------------------------------