Complex classes like Paginator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Paginator, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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(); |
||
|
|||
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) |
||
263 | |||
264 | /** |
||
265 | * Get paging params after pagination operation. |
||
266 | * |
||
267 | * @return array |
||
268 | */ |
||
269 | public function getPagingParams() |
||
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) |
||
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) |
||
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) |
||
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) |
||
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) |
||
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) |
||
496 | } |
||
497 |
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.