PaginatorComponent::paginate()   F
last analyzed

Complexity

Conditions 21
Paths > 20000

Size

Total Lines 121
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 80
c 0
b 0
f 0
nc 27650
nop 3
dl 0
loc 121
rs 2

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
 * Paginator Component
4
 *
5
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
13
 * @link          http://cakephp.org CakePHP(tm) Project
14
 * @package       Cake.Controller.Component
15
 * @since         CakePHP(tm) v 2.0
16
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
17
 */
18
19
App::uses('Component', 'Controller');
20
App::uses('Hash', 'Utility');
21
22
/**
23
 * This component is used to handle automatic model data pagination. The primary way to use this
24
 * component is to call the paginate() method. There is a convenience wrapper on Controller as well.
25
 *
26
 * ### Configuring pagination
27
 *
28
 * You configure pagination using the PaginatorComponent::$settings. This allows you to configure
29
 * the default pagination behavior in general or for a specific model. General settings are used when there
30
 * are no specific model configuration, or the model you are paginating does not have specific settings.
31
 *
32
 * {{{
33
 *	$this->Paginator->settings = array(
34
 *		'limit' => 20,
35
 *		'maxLimit' => 100
36
 *	);
37
 * }}}
38
 *
39
 * The above settings will be used to paginate any model. You can configure model specific settings by
40
 * keying the settings with the model name.
41
 *
42
 * {{{
43
 *	$this->Paginator->settings = array(
44
 *		'Post' => array(
45
 *			'limit' => 20,
46
 *			'maxLimit' => 100
47
 *		),
48
 *		'Comment' => array( ... )
49
 *	);
50
 * }}}
51
 *
52
 * This would allow you to have different pagination settings for `Comment` and `Post` models.
53
 *
54
 * #### Paginating with custom finders
55
 *
56
 * You can paginate with any find type defined on your model using the `findType` option.
57
 *
58
 * {{{
59
 * $this->Paginator->settings = array(
60
 *		'Post' => array(
61
 *			'findType' => 'popular'
62
 *		)
63
 * );
64
 * }}}
65
 *
66
 * Would paginate using the `find('popular')` method.
67
 *
68
 * @package       Cake.Controller.Component
69
 * @link http://book.cakephp.org/2.0/en/core-libraries/components/pagination.html
70
 */
71
class PaginatorComponent extends Component {
72
73
/**
74
 * Pagination settings. These settings control pagination at a general level.
75
 * You can also define sub arrays for pagination settings for specific models.
76
 *
77
 * - `maxLimit` The maximum limit users can choose to view. Defaults to 100
78
 * - `limit` The initial number of items per page. Defaults to 20.
79
 * - `page` The starting page, defaults to 1.
80
 * - `paramType` What type of parameters you want pagination to use?
81
 *      - `named` Use named parameters / routed parameters.
82
 *      - `querystring` Use query string parameters.
83
 *
84
 * @var array
85
 */
86
	public $settings = array(
87
		'page' => 1,
88
		'limit' => 20,
89
		'maxLimit' => 100,
90
		'paramType' => 'named'
91
	);
92
93
/**
94
 * A list of parameters users are allowed to set using request parameters. Modifying
95
 * this list will allow users to have more influence over pagination,
96
 * be careful with what you permit.
97
 *
98
 * @var array
99
 */
100
	public $whitelist = array(
101
		'limit', 'sort', 'page', 'direction'
102
	);
103
104
/**
105
 * Constructor
106
 *
107
 * @param ComponentCollection $collection A ComponentCollection this component can use to lazy load its components
108
 * @param array $settings Array of configuration settings.
109
 */
110
	public function __construct(ComponentCollection $collection, $settings = array()) {
111
		$settings = array_merge($this->settings, (array)$settings);
112
		$this->Controller = $collection->getController();
113
		parent::__construct($collection, $settings);
114
	}
115
116
/**
117
 * Handles automatic pagination of model records.
118
 *
119
 * @param Model|string $object Model to paginate (e.g: model instance, or 'Model', or 'Model.InnerModel')
120
 * @param string|array $scope Additional find conditions to use while paginating
121
 * @param array $whitelist List of allowed fields for ordering. This allows you to prevent ordering
122
 *   on non-indexed, or undesirable columns. See PaginatorComponent::validateSort() for additional details
123
 *   on how the whitelisting and sort field validation works.
124
 * @return array Model query results
125
 * @throws MissingModelException
126
 * @throws NotFoundException
127
 */
128
	public function paginate($object = null, $scope = array(), $whitelist = array()) {
129
		if (is_array($object)) {
130
			$whitelist = $scope;
131
			$scope = $object;
132
			$object = null;
133
		}
134
135
		$object = $this->_getObject($object);
0 ignored issues
show
Bug introduced by
It seems like $object defined by $this->_getObject($object) on line 135 can also be of type null; however, PaginatorComponent::_getObject() does only seem to accept string|object<Model>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
136
137
		if (!is_object($object)) {
138
			throw new MissingModelException($object);
139
		}
140
141
		$options = $this->mergeOptions($object->alias);
142
		$options = $this->validateSort($object, $options, $whitelist);
0 ignored issues
show
Bug introduced by
It seems like $whitelist defined by $scope on line 130 can also be of type string; however, PaginatorComponent::validateSort() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
143
		$options = $this->checkLimit($options);
144
145
		$conditions = $fields = $order = $limit = $page = $recursive = null;
146
147
		if (!isset($options['conditions'])) {
148
			$options['conditions'] = array();
149
		}
150
151
		$type = 'all';
152
153
		if (isset($options[0])) {
154
			$type = $options[0];
155
			unset($options[0]);
156
		}
157
158
		extract($options);
159
160
		if (is_array($scope) && !empty($scope)) {
161
			$conditions = array_merge($conditions, $scope);
162
		} elseif (is_string($scope)) {
163
			$conditions = array($conditions, $scope);
164
		}
165
		if ($recursive === null) {
166
			$recursive = $object->recursive;
167
		}
168
169
		$extra = array_diff_key($options, compact(
170
			'conditions', 'fields', 'order', 'limit', 'page', 'recursive'
171
		));
172
173
		if (!empty($extra['findType'])) {
174
			$type = $extra['findType'];
175
			unset($extra['findType']);
176
		}
177
178
		if ($type !== 'all') {
179
			$extra['type'] = $type;
180
		}
181
182
		if (intval($page) < 1) {
183
			$page = 1;
184
		}
185
		$page = $options['page'] = (int)$page;
186
187
		if ($object->hasMethod('paginate')) {
188
			$results = $object->paginate(
189
				$conditions, $fields, $order, $limit, $page, $recursive, $extra
190
			);
191
		} else {
192
			$parameters = compact('conditions', 'fields', 'order', 'limit', 'page');
193
			if ($recursive != $object->recursive) {
194
				$parameters['recursive'] = $recursive;
195
			}
196
			$results = $object->find($type, array_merge($parameters, $extra));
197
		}
198
		$defaults = $this->getDefaults($object->alias);
199
		unset($defaults[0]);
200
201
		if (!$results) {
202
			$count = 0;
203
		} elseif ($object->hasMethod('paginateCount')) {
204
			$count = $object->paginateCount($conditions, $recursive, $extra);
205
		} else {
206
			$parameters = compact('conditions');
207
			if ($recursive != $object->recursive) {
208
				$parameters['recursive'] = $recursive;
209
			}
210
			$count = $object->find('count', array_merge($parameters, $extra));
211
		}
212
		$pageCount = intval(ceil($count / $limit));
213
		$requestedPage = $page;
214
		$page = max(min($page, $pageCount), 1);
215
216
		$paging = array(
217
			'page' => $page,
218
			'current' => count($results),
219
			'count' => $count,
220
			'prevPage' => ($page > 1),
221
			'nextPage' => ($count > ($page * $limit)),
222
			'pageCount' => $pageCount,
223
			'order' => $order,
224
			'limit' => $limit,
225
			'options' => Hash::diff($options, $defaults),
226
			'paramType' => $options['paramType']
227
		);
228
229
		if (!isset($this->Controller->request['paging'])) {
230
			$this->Controller->request['paging'] = array();
231
		}
232
		$this->Controller->request['paging'] = array_merge(
233
			(array)$this->Controller->request['paging'],
234
			array($object->alias => $paging)
235
		);
236
237
		if ($requestedPage > $page) {
238
			throw new NotFoundException();
239
		}
240
241
		if (
242
			!in_array('Paginator', $this->Controller->helpers) &&
243
			!array_key_exists('Paginator', $this->Controller->helpers)
244
		) {
245
			$this->Controller->helpers[] = 'Paginator';
246
		}
247
		return $results;
248
	}
249
250
/**
251
 * Get the object pagination will occur on.
252
 *
253
 * @param string|Model $object The object you are looking for.
254
 * @return mixed The model object to paginate on.
255
 */
256
	protected function _getObject($object) {
257
		if (is_string($object)) {
258
			$assoc = null;
259
			if (strpos($object, '.') !== false) {
260
				list($object, $assoc) = pluginSplit($object);
261
			}
262
			if ($assoc && isset($this->Controller->{$object}->{$assoc})) {
263
				return $this->Controller->{$object}->{$assoc};
264
			}
265
			if ($assoc && isset($this->Controller->{$this->Controller->modelClass}->{$assoc})) {
266
				return $this->Controller->{$this->Controller->modelClass}->{$assoc};
267
			}
268
			if (isset($this->Controller->{$object})) {
269
				return $this->Controller->{$object};
270
			}
271
			if (isset($this->Controller->{$this->Controller->modelClass}->{$object})) {
272
				return $this->Controller->{$this->Controller->modelClass}->{$object};
273
			}
274
		}
275
		if (empty($object) || $object === null) {
276
			if (isset($this->Controller->{$this->Controller->modelClass})) {
277
				return $this->Controller->{$this->Controller->modelClass};
278
			}
279
280
			$className = null;
281
			$name = $this->Controller->uses[0];
282
			if (strpos($this->Controller->uses[0], '.') !== false) {
283
				list($name, $className) = explode('.', $this->Controller->uses[0]);
284
			}
285
			if ($className) {
286
				return $this->Controller->{$className};
287
			}
288
289
			return $this->Controller->{$name};
290
		}
291
		return $object;
292
	}
293
294
/**
295
 * Merges the various options that Pagination uses.
296
 * Pulls settings together from the following places:
297
 *
298
 * - General pagination settings
299
 * - Model specific settings.
300
 * - Request parameters
301
 *
302
 * The result of this method is the aggregate of all the option sets combined together. You can change
303
 * PaginatorComponent::$whitelist to modify which options/values can be set using request parameters.
304
 *
305
 * @param string $alias Model alias being paginated, if the general settings has a key with this value
306
 *   that key's settings will be used for pagination instead of the general ones.
307
 * @return array Array of merged options.
308
 */
309
	public function mergeOptions($alias) {
310
		$defaults = $this->getDefaults($alias);
311
		switch ($defaults['paramType']) {
312
			case 'named':
313
				$request = $this->Controller->request->params['named'];
314
				break;
315
			case 'querystring':
316
				$request = $this->Controller->request->query;
317
				break;
318
		}
319
		$request = array_intersect_key($request, array_flip($this->whitelist));
0 ignored issues
show
Bug introduced by
The variable $request does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
320
		return array_merge($defaults, $request);
321
	}
322
323
/**
324
 * Get the default settings for a $model. If there are no settings for a specific model, the general settings
325
 * will be used.
326
 *
327
 * @param string $alias Model name to get default settings for.
328
 * @return array An array of pagination defaults for a model, or the general settings.
329
 */
330
	public function getDefaults($alias) {
331
		$defaults = $this->settings;
332
		if (isset($this->settings[$alias])) {
333
			$defaults = $this->settings[$alias];
334
		}
335
		if (isset($defaults['limit']) &&
336
			(empty($defaults['maxLimit']) || $defaults['limit'] > $defaults['maxLimit'])
337
		) {
338
			$defaults['maxLimit'] = $defaults['limit'];
339
		}
340
		return array_merge(
341
			array('page' => 1, 'limit' => 20, 'maxLimit' => 100, 'paramType' => 'named'),
342
			$defaults
343
		);
344
	}
345
346
/**
347
 * Validate that the desired sorting can be performed on the $object. Only fields or
348
 * virtualFields can be sorted on. The direction param will also be sanitized. Lastly
349
 * sort + direction keys will be converted into the model friendly order key.
350
 *
351
 * You can use the whitelist parameter to control which columns/fields are available for sorting.
352
 * This helps prevent users from ordering large result sets on un-indexed values.
353
 *
354
 * Any columns listed in the sort whitelist will be implicitly trusted. You can use this to sort
355
 * on synthetic columns, or columns added in custom find operations that may not exist in the schema.
356
 *
357
 * @param Model $object The model being paginated.
358
 * @param array $options The pagination options being used for this request.
359
 * @param array $whitelist The list of columns that can be used for sorting. If empty all keys are allowed.
360
 * @return array An array of options with sort + direction removed and replaced with order if possible.
361
 */
362
	public function validateSort(Model $object, array $options, array $whitelist = array()) {
363
		if (empty($options['order']) && is_array($object->order)) {
364
			$options['order'] = $object->order;
365
		}
366
367
		if (isset($options['sort'])) {
368
			$direction = null;
369
			if (isset($options['direction'])) {
370
				$direction = strtolower($options['direction']);
371
			}
372
			if (!in_array($direction, array('asc', 'desc'))) {
373
				$direction = 'asc';
374
			}
375
			$options['order'] = array($options['sort'] => $direction);
376
		}
377
378
		if (!empty($whitelist) && isset($options['order']) && is_array($options['order'])) {
379
			$field = key($options['order']);
380
			$inWhitelist = in_array($field, $whitelist, true);
381
			if (!$inWhitelist) {
382
				$options['order'] = null;
383
			}
384
			return $options;
385
		}
386
387
		if (!empty($options['order']) && is_array($options['order'])) {
388
			$order = array();
389
			foreach ($options['order'] as $key => $value) {
390
				$field = $key;
391
				$alias = $object->alias;
392
				if (strpos($key, '.') !== false) {
393
					list($alias, $field) = explode('.', $key);
394
				}
395
				$correctAlias = ($object->alias === $alias);
396
397
				if ($correctAlias && $object->hasField($field)) {
398
					$order[$object->alias . '.' . $field] = $value;
399
				} elseif ($correctAlias && $object->hasField($key, true)) {
400
					$order[$field] = $value;
401
				} elseif (isset($object->{$alias}) && $object->{$alias}->hasField($field, true)) {
402
					$order[$alias . '.' . $field] = $value;
403
				}
404
			}
405
			$options['order'] = $order;
406
		}
407
408
		return $options;
409
	}
410
411
/**
412
 * Check the limit parameter and ensure its within the maxLimit bounds.
413
 *
414
 * @param array $options An array of options with a limit key to be checked.
415
 * @return array An array of options for pagination
416
 */
417
	public function checkLimit(array $options) {
418
		$options['limit'] = (int)$options['limit'];
419
		if (empty($options['limit']) || $options['limit'] < 1) {
420
			$options['limit'] = 1;
421
		}
422
		$options['limit'] = min($options['limit'], $options['maxLimit']);
423
		return $options;
424
	}
425
426
}
427