Test Failed
Pull Request — main (#64)
by Lode
08:15
created

getInvalidParameterValueErrorObject()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 3
nc 4
nop 4
1
<?php
2
3
namespace alsvanzelf\jsonapi\profiles;
4
5
use alsvanzelf\jsonapi\Document;
6
use alsvanzelf\jsonapi\ResourceDocument;
7
use alsvanzelf\jsonapi\helpers\ProfileAliasManager;
8
use alsvanzelf\jsonapi\interfaces\PaginableInterface;
9
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
10
use alsvanzelf\jsonapi\interfaces\ResourceInterface;
11
use alsvanzelf\jsonapi\objects\ErrorObject;
12
use alsvanzelf\jsonapi\objects\LinkObject;
13
14
/**
15
 * cursor-based pagination (aka keyset pagination) is a common pagination strategy that avoids many of the pitfalls of 'offset–limit' pagination
16
 * 
17
 * @see https://jsonapi.org/profiles/ethanresnick/cursor-pagination/
18
 * 
19
 * related query parameters:
20
 * - sort
21
 * - page[size]
22
 * - page[before]
23
 * - page[after]
24
 * 
25
 * handling different use cases:
26
 * 
27
 * 1. handle requests with 'after' nor 'before' when the client requests a first page
28
 *    call {@see setLinksFirstPage} with the last cursor in the first page
29
 * 
30
 * 2. handle requests with 'after' when the client requests a next page
31
 *    call {@see setLinks} with the first and last cursor in the current page
32
 *    call {@see setLinksLastPage} with the first cursor in the current page, when this is the last page
33
 * 
34
 * 3. handle requests with 'before' when the client requests a previous page
35
 *    call {@see setLinks} with the first and last cursor in the current page
36
 * 
37
 * 4. handle requests with 'after' and 'before' when the client requests a specific page
38
 *    call {@see setLinks} with the first and last cursor in the current page, when there are previous/next pages
39
 *    call {@see setPaginationLinkObjectsExplicitlyEmpty}, when the results is everything between after and before
40
 * 
41
 * other interesting methods:
42
 * - {@see setCursor} to expose the cursor of a pagination item to allow custom url building
43
 * - {@see setCount} to expose the total count(s) of the pagination data
44
 * - {@see get*ErrorObject} to generate ErrorObjects for specific error cases
45
 * - {@see generatePreviousLink} {@see generateNextLink} to apply the links manually
46
 */
0 ignored issues
show
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
47
class CursorPaginationProfile extends ProfileAliasManager implements ProfileInterface {
48
	/**
49
	 * human api
50
	 */
51
	
52
	/**
53
	 * set links to paginate the data using cursors of the paginated data
54
	 * 
55
	 * @param PaginableInterface $paginable        a CollectionDocument or RelationshipObject
56
	 * @param string             $baseOrCurrentUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
57
	 * @param string             $firstCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
58
	 * @param string             $lastCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
59
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
60
	public function setLinks(PaginableInterface $paginable, $baseOrCurrentUrl, $firstCursor, $lastCursor) {
61
		$previousLinkObject = new LinkObject($this->generatePreviousLink($baseOrCurrentUrl, $firstCursor));
62
		$nextLinkObject     = new LinkObject($this->generateNextLink($baseOrCurrentUrl, $lastCursor));
63
		
64
		$this->setPaginationLinkObjects($paginable, $previousLinkObject, $nextLinkObject);
65
	}
66
	
67
	/**
68
	 * @param PaginableInterface $paginable        a CollectionDocument or RelationshipObject
69
	 * @param string             $baseOrCurrentUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
70
	 * @param string             $lastCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
71
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
72
	public function setLinksFirstPage(PaginableInterface $paginable, $baseOrCurrentUrl, $lastCursor) {
73
		$this->setPaginationLinkObjectsWithoutPrevious($paginable, $baseOrCurrentUrl, $lastCursor);
74
	}
75
	
76
	/**
77
	 * @param PaginableInterface $paginable        a CollectionDocument or RelationshipObject
78
	 * @param string             $baseOrCurrentUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
79
	 * @param string             $firstCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
80
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
81
	public function setLinksLastPage(PaginableInterface $paginable, $baseOrCurrentUrl, $firstCursor) {
82
		$this->setPaginationLinkObjectsWithoutNext($paginable, $baseOrCurrentUrl, $firstCursor);
83
	}
84
	
85
	/**
86
	 * set the cursor of a specific resource to allow pagination after or before this resource
87
	 * 
88
	 * @param ResourceInterface $resource
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
89
	 * @param string            $cursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
90
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
91
	public function setCursor(ResourceInterface $resource, $cursor) {
92
		$this->setItemMeta($resource, $cursor);
93
	}
94
	
95
	/**
96
	 * set count(s) to tell about the (estimated) total size
97
	 * 
98
	 * @param PaginableInterface $paginable        a CollectionDocument or RelationshipObject
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 8 found
Loading history...
99
	 * @param int                $exactTotal       optional
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 7 found
Loading history...
100
	 * @param int                $bestGuessTotal   optional
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
101
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
102
	public function setCount(PaginableInterface $paginable, $exactTotal=null, $bestGuessTotal=null) {
103
		$this->setPaginationMeta($paginable, $exactTotal, $bestGuessTotal);
104
	}
105
	
106
	/**
107
	 * spec api
108
	 */
109
	
110
	/**
111
	 * helper to get generate a correct page[before] link, use to apply manually
112
	 * 
113
	 * @param  string $baseOrCurrentUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
114
	 * @param  string $beforeCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
115
	 * @return string
116
	 */
117
	public function generatePreviousLink($baseOrCurrentUrl, $beforeCursor) {
118
		return $this->setQueryParameter($baseOrCurrentUrl, $this->getKeyword('page').'[before]', $beforeCursor);
119
	}
120
	
121
	/**
122
	 * helper to get generate a correct page[after] link, use to apply manually
123
	 * 
124
	 * @param  string $baseOrCurrentUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
125
	 * @param  string $afterCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
126
	 * @return string
127
	 */
128
	public function generateNextLink($baseOrCurrentUrl, $afterCursor) {
129
		return $this->setQueryParameter($baseOrCurrentUrl, $this->getKeyword('page').'[after]', $afterCursor);
130
	}
131
	
132
	/**
133
	 * pagination links are inside the links object that is a sibling of the paginated data
134
	 * 
135
	 * ends up at one of:
136
	 * - /links/prev                          & /links/next
137
	 * - /data/relationships/foo/links/prev   & /data/relationships/foo/links/next
138
	 * - /data/0/relationships/foo/links/prev & /data/0/relationships/foo/links/next
139
	 * 
140
	 * @see https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#terms-pagination-links
141
	 * 
142
	 * @param PaginableInterface $paginable
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
143
	 * @param LinkObject         $previousLinkObject
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
144
	 * @param LinkObject         $nextLinkObject
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
145
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
146
	public function setPaginationLinkObjects(PaginableInterface $paginable, LinkObject $previousLinkObject, LinkObject $nextLinkObject) {
147
		$paginable->addLinkObject('prev', $previousLinkObject);
148
		$paginable->addLinkObject('next', $nextLinkObject);
149
	}
150
	
151
	/**
152
	 * @param PaginableInterface $paginable
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
153
	 * @param string             $baseOrCurrentUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
154
	 * @param string             $firstCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
155
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
156
	public function setPaginationLinkObjectsWithoutNext(PaginableInterface $paginable, $baseOrCurrentUrl, $firstCursor) {
157
		$this->setPaginationLinkObjects($paginable, new LinkObject($this->generatePreviousLink($baseOrCurrentUrl, $firstCursor)), new LinkObject());
158
	}
159
	
160
	/**
161
	 * @param PaginableInterface $paginable
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
162
	 * @param string             $baseOrCurrentUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
163
	 * @param string             $lastCursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
164
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
165
	public function setPaginationLinkObjectsWithoutPrevious(PaginableInterface $paginable, $baseOrCurrentUrl, $lastCursor) {
166
		$this->setPaginationLinkObjects($paginable, new LinkObject(), new LinkObject($this->generateNextLink($baseOrCurrentUrl, $lastCursor)));
167
	}
168
	
169
	/**
170
	 * @param PaginableInterface $paginable
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
171
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
172
	public function setPaginationLinkObjectsExplicitlyEmpty(PaginableInterface $paginable) {
173
		$this->setPaginationLinkObjects($paginable, new LinkObject(), new LinkObject());
174
	}
175
	
176
	/**
177
	 * pagination item metadata is the page meta at the top-level of a paginated item
178
	 * 
179
	 * ends up at one of:
180
	 * - /data/meta/page
181
	 * - /data/relationships/foo/meta/page
182
	 * - /data/0/relationships/foo/meta/page
183
	 * 
184
	 * @see https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#terms-pagination-item-metadata
185
	 * 
186
	 * @param ResourceInterface $resource
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
187
	 * @param string            $cursor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
188
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
189
	public function setItemMeta(ResourceInterface $resource, $cursor) {
190
		$metadata = [
191
			'cursor' => $cursor,
192
		];
193
		
194
		if ($resource instanceof ResourceDocument) {
195
			$resource->addMeta($this->getKeyword('page'), $metadata, $level=Document::LEVEL_RESOURCE);
196
		}
197
		else {
198
			$resource->addMeta($this->getKeyword('page'), $metadata);
199
		}
200
	}
201
	
202
	/**
203
	 * pagination metadata is the page meta that is a sibling of the paginated data (and pagination links)
204
	 * 
205
	 * ends up at one of:
206
	 * - /meta/page/total                          & /meta/page/estimatedTotal/bestGuess                          & /meta/page/rangeTruncated
207
	 * - /data/relationships/foo/meta/page/total   & /data/relationships/foo/meta/page/estimatedTotal/bestGuess   & /data/relationships/foo/meta/page/rangeTruncated
208
	 * - /data/0/relationships/foo/meta/page/total & /data/0/relationships/foo/meta/page/estimatedTotal/bestGuess & /data/0/relationships/foo/meta/page/rangeTruncated
209
	 * 
210
	 * @see https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#terms-pagination-metadata
211
	 * 
212
	 * @param PaginableInterface $paginable
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
213
	 * @param int                $exactTotal       optional
214
	 * @param int                $bestGuessTotal   optional
215
	 * @param boolean            $rangeIsTruncated optional, if both after and before are supplied but the items exceed requested or max size
216
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
217
	public function setPaginationMeta(PaginableInterface $paginable, $exactTotal=null, $bestGuessTotal=null, $rangeIsTruncated=null) {
218
		$metadata = [];
219
		
220
		if ($exactTotal !== null) {
221
			$metadata['total'] = $exactTotal;
222
		}
223
		if ($bestGuessTotal !== null) {
224
			$metadata['estimatedTotal'] = [
225
				'bestGuess' => $bestGuessTotal,
226
			];
227
		}
228
		if ($rangeIsTruncated !== null) {
229
			$metadata['rangeTruncated'] = $rangeIsTruncated;
230
		}
231
		
232
		$paginable->addMeta($this->getKeyword('page'), $metadata);
233
	}
234
	
235
	/**
236
	 * get an ErrorObject for when the requested sorting cannot efficiently be paginated
237
	 * 
238
	 * ends up at:
239
	 * - /errors/0/code
240
	 * - /errors/0/status
241
	 * - /errors/0/source/parameter
242
	 * - /errors/0/links/type/0
243
	 * - /errors/0/title            optional
244
	 * - /errors/0/detail           optional
245
	 * 
246
	 * @param  string $genericTitle    optional
247
	 * @param  string $specificDetails optional
248
	 * @return ErrorObject
249
	 */
250 View Code Duplication
	public function getUnsupportedSortErrorObject($genericTitle=null, $specificDetails=null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
251
		$errorObject = new ErrorObject('Unsupported sort');
252
		$errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort');
253
		$errorObject->blameQueryParameter('sort');
254
		$errorObject->setHttpStatusCode(400);
255
		
256
		if ($genericTitle !== null) {
257
			$errorObject->setHumanExplanation($genericTitle, $specificDetails);
258
		}
259
		
260
		return $errorObject;
261
	}
262
	
263
	/**
264
	 * get an ErrorObject for when the requested page size exceeds the server-defined max page size
265
	 * 
266
	 * ends up at:
267
	 * - /errors/0/code
268
	 * - /errors/0/status
269
	 * - /errors/0/source/parameter
270
	 * - /errors/0/links/type/0
271
	 * - /errors/0/meta/page/maxSize
272
	 * - /errors/0/title             optional
273
	 * - /errors/0/detail            optional
274
	 * 
275
	 * @param  int    $maxSize
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
276
	 * @param  string $genericTitle    optional, e.g. 'Page size requested is too large.'
277
	 * @param  string $specificDetails optional, e.g. 'You requested a size of 200, but 100 is the maximum.'
278
	 * @return ErrorObject
279
	 */
280
	public function getMaxPageSizeExceededErrorObject($maxSize, $genericTitle=null, $specificDetails=null) {
281
		$errorObject = new ErrorObject('Max page size exceeded');
282
		$errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded');
283
		$errorObject->blameQueryParameter($this->getKeyword('page').'[size]');
284
		$errorObject->setHttpStatusCode(400);
285
		$errorObject->addMeta($this->getKeyword('page'), $value=['maxSize' => $maxSize]);
286
		
287
		if ($genericTitle !== null) {
288
			$errorObject->setHumanExplanation($genericTitle, $specificDetails);
289
		}
290
		
291
		return $errorObject;
292
	}
293
	
294
	/**
295
	 * get an ErrorObject for when the requested page size is not a positive integer, or when the requested page after/before is not a valid cursor
296
	 * 
297
	 * ends up at:
298
	 * - /errors/0/code
299
	 * - /errors/0/status
300
	 * - /errors/0/source/parameter
301
	 * - /errors/0/links/type/0     optional
302
	 * - /errors/0/title            optional
303
	 * - /errors/0/detail           optional
304
	 * 
305
	 * @param  int    $queryParameter  e.g. 'sort' or 'page[size]', aliasing should already be done using {@see getKeyword}
306
	 * @param  string $typeLink        optional
307
	 * @param  string $genericTitle    optional, e.g. 'Invalid Parameter.'
308
	 * @param  string $specificDetails optional, e.g. 'page[size] must be a positive integer; got 0'
309
	 * @return ErrorObject
310
	 */
311
	public function getInvalidParameterValueErrorObject($queryParameter, $typeLink=null, $genericTitle=null, $specificDetails=null) {
312
		$errorObject = new ErrorObject('Invalid parameter value');
313
		$errorObject->blameQueryParameter($queryParameter);
314
		$errorObject->setHttpStatusCode(400);
315
		
316
		if ($typeLink !== null) {
317
			$errorObject->appendTypeLink($typeLink);
318
		}
319
		
320
		if ($genericTitle !== null) {
321
			$errorObject->setHumanExplanation($genericTitle, $specificDetails);
322
		}
323
		
324
		return $errorObject;
325
	}
326
	
327
	/**
328
	 * get an ErrorObject for when range pagination requests (when both 'page[after]' and 'page[before]' are requested) are not supported
329
	 * 
330
	 * ends up at:
331
	 * - /errors/0/code
332
	 * - /errors/0/status
333
	 * - /errors/0/links/type/0
334
	 * 
335
	 * @param  string $genericTitle    optional
336
	 * @param  string $specificDetails optional
337
	 * @return ErrorObject
338
	 */
339 View Code Duplication
	public function getRangePaginationNotSupportedErrorObject($genericTitle=null, $specificDetails=null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
340
		$errorObject = new ErrorObject('Range pagination not supported');
341
		$errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported');
342
		$errorObject->setHttpStatusCode(400);
343
		
344
		if ($genericTitle !== null) {
345
			$errorObject->setHumanExplanation($genericTitle, $specificDetails);
346
		}
347
		
348
		return $errorObject;
349
	}
350
	
351
	/**
352
	 * internal api
353
	 */
354
	
355
	/**
356
	 * add or adjust a key in the query string of a url
357
	 * 
358
	 * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
359
	 * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
360
	 * @param string $value
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
361
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
362
	private function setQueryParameter($url, $key, $value) {
363
		$originalQuery     = parse_url($url, PHP_URL_QUERY);
364
		$decodedQuery      = urldecode($originalQuery);
365
		$originalIsEncoded = ($decodedQuery !== $originalQuery);
366
		
367
		$originalParameters = [];
368
		parse_str($decodedQuery, $originalParameters);
369
		
370
		$newParameters = [];
371
		parse_str($key.'='.$value, $newParameters);
372
		
373
		$fullParameters = array_replace_recursive($originalParameters, $newParameters);
374
		
375
		$newQuery = http_build_query($fullParameters);
376
		if ($originalIsEncoded === false) {
377
			$newQuery = urldecode($newQuery);
378
		}
379
		
380
		$newUrl = str_replace($originalQuery, $newQuery, $url);
381
		
382
		return $newUrl;
383
	}
384
	
385
	/**
386
	 * ProfileInterface
387
	 */
388
	
389
	/**
390
	 * @inheritDoc
391
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
392
	public function getOfficialLink() {
393
		return 'https://jsonapi.org/profiles/ethanresnick/cursor-pagination/';
394
	}
395
	
396
	/**
397
	 * @inheritDoc
398
	 */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
399
	public function getOfficialKeywords() {
400
		return ['page'];
401
	}
402
}
403