Paginator::paginate()   F
last analyzed

Complexity

Conditions 11
Paths 512

Size

Total Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 512
nop 3
dl 0
loc 82
rs 3.4549
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11
 * @link          http://cakephp.org CakePHP(tm) Project
12
 * @since         3.5.0
13
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Datasource;
16
17
use Cake\Core\InstanceConfigTrait;
18
use Cake\Datasource\Exception\PageOutOfBoundsException;
19
20
/**
21
 * This class is used to handle automatic model data pagination.
22
 */
23
class Paginator implements PaginatorInterface
24
{
25
    use InstanceConfigTrait;
26
27
    /**
28
     * Default pagination settings.
29
     *
30
     * When calling paginate() these settings will be merged with the configuration
31
     * you provide.
32
     *
33
     * - `maxLimit` - The maximum limit users can choose to view. Defaults to 100
34
     * - `limit` - The initial number of items per page. Defaults to 20.
35
     * - `page` - The starting page, defaults to 1.
36
     * - `whitelist` - A list of parameters users are allowed to set using request
37
     *   parameters. Modifying this list will allow users to have more influence
38
     *   over pagination, be careful with what you permit.
39
     *
40
     * @var array
41
     */
42
    protected $_defaultConfig = [
43
        'page' => 1,
44
        'limit' => 20,
45
        'maxLimit' => 100,
46
        'whitelist' => ['limit', 'sort', 'page', 'direction'],
47
    ];
48
49
    /**
50
     * Paging params after pagination operation is done.
51
     *
52
     * @var array
53
     */
54
    protected $_pagingParams = [];
55
56
    /**
57
     * Handles automatic pagination of model records.
58
     *
59
     * ### Configuring pagination
60
     *
61
     * When calling `paginate()` you can use the $settings parameter to pass in
62
     * pagination settings. These settings are used to build the queries made
63
     * and control other pagination settings.
64
     *
65
     * If your settings contain a key with the current table's alias. The data
66
     * inside that key will be used. Otherwise the top level configuration will
67
     * be used.
68
     *
69
     * ```
70
     *  $settings = [
71
     *    'limit' => 20,
72
     *    'maxLimit' => 100
73
     *  ];
74
     *  $results = $paginator->paginate($table, $settings);
75
     * ```
76
     *
77
     * The above settings will be used to paginate any repository. You can configure
78
     * repository specific settings by keying the settings with the repository alias.
79
     *
80
     * ```
81
     *  $settings = [
82
     *    'Articles' => [
83
     *      'limit' => 20,
84
     *      'maxLimit' => 100
85
     *    ],
86
     *    'Comments' => [ ... ]
87
     *  ];
88
     *  $results = $paginator->paginate($table, $settings);
89
     * ```
90
     *
91
     * This would allow you to have different pagination settings for
92
     * `Articles` and `Comments` repositories.
93
     *
94
     * ### Controlling sort fields
95
     *
96
     * By default CakePHP will automatically allow sorting on any column on the
97
     * repository object being paginated. Often times you will want to allow
98
     * sorting on either associated columns or calculated fields. In these cases
99
     * you will need to define a whitelist of all the columns you wish to allow
100
     * sorting on. You can define the whitelist in the `$settings` parameter:
101
     *
102
     * ```
103
     * $settings = [
104
     *   'Articles' => [
105
     *     'finder' => 'custom',
106
     *     'sortWhitelist' => ['title', 'author_id', 'comment_count'],
107
     *   ]
108
     * ];
109
     * ```
110
     *
111
     * Passing an empty array as whitelist disallows sorting altogether.
112
     *
113
     * ### Paginating with custom finders
114
     *
115
     * You can paginate with any find type defined on your table using the
116
     * `finder` option.
117
     *
118
     * ```
119
     *  $settings = [
120
     *    'Articles' => [
121
     *      'finder' => 'popular'
122
     *    ]
123
     *  ];
124
     *  $results = $paginator->paginate($table, $settings);
125
     * ```
126
     *
127
     * Would paginate using the `find('popular')` method.
128
     *
129
     * You can also pass an already created instance of a query to this method:
130
     *
131
     * ```
132
     * $query = $this->Articles->find('popular')->matching('Tags', function ($q) {
133
     *   return $q->where(['name' => 'CakePHP'])
134
     * });
135
     * $results = $paginator->paginate($query);
136
     * ```
137
     *
138
     * ### Scoping Request parameters
139
     *
140
     * By using request parameter scopes you can paginate multiple queries in
141
     * the same controller action:
142
     *
143
     * ```
144
     * $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']);
145
     * $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']);
146
     * ```
147
     *
148
     * Each of the above queries will use different query string parameter sets
149
     * for pagination data. An example URL paginating both results would be:
150
     *
151
     * ```
152
     * /dashboard?articles[page]=1&tags[page]=2
153
     * ```
154
     *
155
     * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The table or query to paginate.
156
     * @param array $params Request params
157
     * @param array $settings The settings/configuration used for pagination.
158
     * @return \Cake\Datasource\ResultSetInterface Query results
159
     * @throws \Cake\Datasource\Exception\PageOutOfBoundsException
160
     */
161
    public function paginate($object, array $params = [], array $settings = [])
162
    {
163
        $query = null;
164
        if ($object instanceof QueryInterface) {
165
            $query = $object;
166
            $object = $query->getRepository();
0 ignored issues
show
Bug introduced by
The method getRepository() does not exist on Cake\Datasource\QueryInterface. Did you maybe mean repository()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
167
        }
168
169
        $alias = $object->getAlias();
170
        $defaults = $this->getDefaults($alias, $settings);
171
        $options = $this->mergeOptions($params, $defaults);
172
        $options = $this->validateSort($object, $options);
173
        $options = $this->checkLimit($options);
174
175
        $options += ['page' => 1, 'scope' => null];
176
        $options['page'] = (int)$options['page'] < 1 ? 1 : (int)$options['page'];
177
        list($finder, $options) = $this->_extractFinder($options);
178
179
        if (empty($query)) {
180
            $query = $object->find($finder, $options);
181
        } else {
182
            $query->applyOptions($options);
183
        }
184
185
        $cleanQuery = clone $query;
186
        $results = $query->all();
187
        $numResults = count($results);
188
        $count = $cleanQuery->count();
189
190
        $page = $options['page'];
191
        $limit = $options['limit'];
192
        $pageCount = max((int)ceil($count / $limit), 1);
193
        $requestedPage = $page;
194
        $page = min($page, $pageCount);
195
196
        $order = (array)$options['order'];
197
        $sortDefault = $directionDefault = false;
198
        if (!empty($defaults['order']) && count($defaults['order']) === 1) {
199
            $sortDefault = key($defaults['order']);
200
            $directionDefault = current($defaults['order']);
201
        }
202
203
        $start = 0;
204
        if ($count >= 1) {
205
            $start = (($page - 1) * $limit) + 1;
206
        }
207
        $end = $start + $limit - 1;
208
        if ($count < $end) {
209
            $end = $count;
210
        }
211
212
        $paging = [
213
            'finder' => $finder,
214
            'page' => $page,
215
            'current' => $numResults,
216
            'count' => $count,
217
            'perPage' => $limit,
218
            'start' => $start,
219
            'end' => $end,
220
            'prevPage' => $page > 1,
221
            'nextPage' => $count > ($page * $limit),
222
            'pageCount' => $pageCount,
223
            'sort' => $options['sort'],
224
            'direction' => isset($options['sort']) ? current($order) : null,
225
            'limit' => $defaults['limit'] != $limit ? $limit : null,
226
            'sortDefault' => $sortDefault,
227
            'directionDefault' => $directionDefault,
228
            'scope' => $options['scope'],
229
            'completeSort' => $order,
230
        ];
231
232
        $this->_pagingParams = [$alias => $paging];
233
234
        if ($requestedPage > $page) {
235
            throw new PageOutOfBoundsException([
236
                'requestedPage' => $requestedPage,
237
                'pagingParams' => $this->_pagingParams,
238
            ]);
239
        }
240
241
        return $results;
242
    }
243
244
    /**
245
     * Extracts the finder name and options out of the provided pagination options.
246
     *
247
     * @param array $options the pagination options.
248
     * @return array An array containing in the first position the finder name
249
     *   and in the second the options to be passed to it.
250
     */
251
    protected function _extractFinder($options)
252
    {
253
        $type = !empty($options['finder']) ? $options['finder'] : 'all';
254
        unset($options['finder'], $options['maxLimit']);
255
256
        if (is_array($type)) {
257
            $options = (array)current($type) + $options;
258
            $type = key($type);
259
        }
260
261
        return [$type, $options];
262
    }
263
264
    /**
265
     * Get paging params after pagination operation.
266
     *
267
     * @return array
268
     */
269
    public function getPagingParams()
270
    {
271
        return $this->_pagingParams;
272
    }
273
274
    /**
275
     * Merges the various options that Paginator uses.
276
     * Pulls settings together from the following places:
277
     *
278
     * - General pagination settings
279
     * - Model specific settings.
280
     * - Request parameters
281
     *
282
     * The result of this method is the aggregate of all the option sets
283
     * combined together. You can change config value `whitelist` to modify
284
     * which options/values can be set using request parameters.
285
     *
286
     * @param array $params Request params.
287
     * @param array $settings The settings to merge with the request data.
288
     * @return array Array of merged options.
289
     */
290
    public function mergeOptions($params, $settings)
291
    {
292
        if (!empty($settings['scope'])) {
293
            $scope = $settings['scope'];
294
            $params = !empty($params[$scope]) ? (array)$params[$scope] : [];
295
        }
296
        $params = array_intersect_key($params, array_flip($this->getConfig('whitelist')));
297
298
        return array_merge($settings, $params);
299
    }
300
301
    /**
302
     * Get the settings for a $model. If there are no settings for a specific
303
     * repository, the general settings will be used.
304
     *
305
     * @param string $alias Model name to get settings for.
306
     * @param array $settings The settings which is used for combining.
307
     * @return array An array of pagination settings for a model,
308
     *   or the general settings.
309
     */
310
    public function getDefaults($alias, $settings)
311
    {
312
        if (isset($settings[$alias])) {
313
            $settings = $settings[$alias];
314
        }
315
316
        $defaults = $this->getConfig();
317
        $maxLimit = isset($settings['maxLimit']) ? $settings['maxLimit'] : $defaults['maxLimit'];
318
        $limit = isset($settings['limit']) ? $settings['limit'] : $defaults['limit'];
319
320
        if ($limit > $maxLimit) {
321
            $limit = $maxLimit;
322
        }
323
324
        $settings['maxLimit'] = $maxLimit;
325
        $settings['limit'] = $limit;
326
327
        return $settings + $defaults;
328
    }
329
330
    /**
331
     * Validate that the desired sorting can be performed on the $object.
332
     *
333
     * Only fields or virtualFields can be sorted on. The direction param will
334
     * also be sanitized. Lastly sort + direction keys will be converted into
335
     * the model friendly order key.
336
     *
337
     * You can use the whitelist parameter to control which columns/fields are
338
     * available for sorting via URL parameters. This helps prevent users from ordering large
339
     * result sets on un-indexed values.
340
     *
341
     * If you need to sort on associated columns or synthetic properties you
342
     * will need to use a whitelist.
343
     *
344
     * Any columns listed in the sort whitelist will be implicitly trusted.
345
     * You can use this to sort on synthetic columns, or columns added in custom
346
     * find operations that may not exist in the schema.
347
     *
348
     * The default order options provided to paginate() will be merged with the user's
349
     * requested sorting field/direction.
350
     *
351
     * @param \Cake\Datasource\RepositoryInterface $object Repository object.
352
     * @param array $options The pagination options being used for this request.
353
     * @return array An array of options with sort + direction removed and
354
     *   replaced with order if possible.
355
     */
356
    public function validateSort(RepositoryInterface $object, array $options)
357
    {
358
        if (isset($options['sort'])) {
359
            $direction = null;
360
            if (isset($options['direction'])) {
361
                $direction = strtolower($options['direction']);
362
            }
363
            if (!in_array($direction, ['asc', 'desc'])) {
364
                $direction = 'asc';
365
            }
366
367
            $order = (isset($options['order']) && is_array($options['order'])) ? $options['order'] : [];
368
            if ($order && $options['sort'] && strpos($options['sort'], '.') === false) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $order of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
369
                $order = $this->_removeAliases($order, $object->getAlias());
370
            }
371
372
            $options['order'] = [$options['sort'] => $direction] + $order;
373
        } else {
374
            $options['sort'] = null;
375
        }
376
        unset($options['direction']);
377
378
        if (empty($options['order'])) {
379
            $options['order'] = [];
380
        }
381
        if (!is_array($options['order'])) {
382
            return $options;
383
        }
384
385
        $inWhitelist = false;
386
        if (isset($options['sortWhitelist'])) {
387
            $field = key($options['order']);
388
            $inWhitelist = in_array($field, $options['sortWhitelist'], true);
389
            if (!$inWhitelist) {
390
                $options['order'] = [];
391
                $options['sort'] = null;
392
393
                return $options;
394
            }
395
        }
396
397
        if (
398
            $options['sort'] === null
399
            && count($options['order']) === 1
400
            && !is_numeric(key($options['order']))
401
        ) {
402
            $options['sort'] = key($options['order']);
403
        }
404
405
        $options['order'] = $this->_prefix($object, $options['order'], $inWhitelist);
406
407
        return $options;
408
    }
409
410
    /**
411
     * Remove alias if needed.
412
     *
413
     * @param array $fields Current fields
414
     * @param string $model Current model alias
415
     * @return array $fields Unaliased fields where applicable
416
     */
417
    protected function _removeAliases($fields, $model)
418
    {
419
        $result = [];
420
        foreach ($fields as $field => $sort) {
421
            if (strpos($field, '.') === false) {
422
                $result[$field] = $sort;
423
                continue;
424
            }
425
426
            list ($alias, $currentField) = explode('.', $field);
427
428
            if ($alias === $model) {
429
                $result[$currentField] = $sort;
430
                continue;
431
            }
432
433
            $result[$field] = $sort;
434
        }
435
436
        return $result;
437
    }
438
439
    /**
440
     * Prefixes the field with the table alias if possible.
441
     *
442
     * @param \Cake\Datasource\RepositoryInterface $object Repository object.
443
     * @param array $order Order array.
444
     * @param bool $whitelisted Whether or not the field was whitelisted.
445
     * @return array Final order array.
446
     */
447
    protected function _prefix(RepositoryInterface $object, $order, $whitelisted = false)
448
    {
449
        $tableAlias = $object->getAlias();
450
        $tableOrder = [];
451
        foreach ($order as $key => $value) {
452
            if (is_numeric($key)) {
453
                $tableOrder[] = $value;
454
                continue;
455
            }
456
            $field = $key;
457
            $alias = $tableAlias;
458
459
            if (strpos($key, '.') !== false) {
460
                list($alias, $field) = explode('.', $key);
461
            }
462
            $correctAlias = ($tableAlias === $alias);
463
464
            if ($correctAlias && $whitelisted) {
465
                // Disambiguate fields in schema. As id is quite common.
466
                if ($object->hasField($field)) {
467
                    $field = $alias . '.' . $field;
468
                }
469
                $tableOrder[$field] = $value;
470
            } elseif ($correctAlias && $object->hasField($field)) {
471
                $tableOrder[$tableAlias . '.' . $field] = $value;
472
            } elseif (!$correctAlias && $whitelisted) {
473
                $tableOrder[$alias . '.' . $field] = $value;
474
            }
475
        }
476
477
        return $tableOrder;
478
    }
479
480
    /**
481
     * Check the limit parameter and ensure it's within the maxLimit bounds.
482
     *
483
     * @param array $options An array of options with a limit key to be checked.
484
     * @return array An array of options for pagination.
485
     */
486
    public function checkLimit(array $options)
487
    {
488
        $options['limit'] = (int)$options['limit'];
489
        if (empty($options['limit']) || $options['limit'] < 1) {
490
            $options['limit'] = 1;
491
        }
492
        $options['limit'] = max(min($options['limit'], $options['maxLimit']), 1);
493
494
        return $options;
495
    }
496
}
497