PaginatorComponent.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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 2.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Controller\Component;
  16. use Cake\Controller\Component;
  17. use Cake\Datasource\QueryInterface;
  18. use Cake\Datasource\RepositoryInterface;
  19. use Cake\Network\Exception\NotFoundException;
  20. use Cake\Utility\Hash;
  21. /**
  22. * This component is used to handle automatic model data pagination. The primary way to use this
  23. * component is to call the paginate() method. There is a convenience wrapper on Controller as well.
  24. *
  25. * ### Configuring pagination
  26. *
  27. * You configure pagination when calling paginate(). See that method for more details.
  28. *
  29. * @link http://book.cakephp.org/3.0/en/controllers/components/pagination.html
  30. */
  31. class PaginatorComponent extends Component
  32. {
  33. /**
  34. * Default pagination settings.
  35. *
  36. * When calling paginate() these settings will be merged with the configuration
  37. * you provide.
  38. *
  39. * - `maxLimit` - The maximum limit users can choose to view. Defaults to 100
  40. * - `limit` - The initial number of items per page. Defaults to 20.
  41. * - `page` - The starting page, defaults to 1.
  42. * - `whitelist` - A list of parameters users are allowed to set using request
  43. * parameters. Modifying this list will allow users to have more influence
  44. * over pagination, be careful with what you permit.
  45. *
  46. * @var array
  47. */
  48. protected $_defaultConfig = [
  49. 'page' => 1,
  50. 'limit' => 20,
  51. 'maxLimit' => 100,
  52. 'whitelist' => ['limit', 'sort', 'page', 'direction']
  53. ];
  54. /**
  55. * Events supported by this component.
  56. *
  57. * @return array
  58. */
  59. public function implementedEvents()
  60. {
  61. return [];
  62. }
  63. /**
  64. * Handles automatic pagination of model records.
  65. *
  66. * ### Configuring pagination
  67. *
  68. * When calling `paginate()` you can use the $settings parameter to pass in pagination settings.
  69. * These settings are used to build the queries made and control other pagination settings.
  70. *
  71. * If your settings contain a key with the current table's alias. The data inside that key will be used.
  72. * Otherwise the top level configuration will be used.
  73. *
  74. * ```
  75. * $settings = [
  76. * 'limit' => 20,
  77. * 'maxLimit' => 100
  78. * ];
  79. * $results = $paginator->paginate($table, $settings);
  80. * ```
  81. *
  82. * The above settings will be used to paginate any Table. You can configure Table specific settings by
  83. * keying the settings with the Table alias.
  84. *
  85. * ```
  86. * $settings = [
  87. * 'Articles' => [
  88. * 'limit' => 20,
  89. * 'maxLimit' => 100
  90. * ],
  91. * 'Comments' => [ ... ]
  92. * ];
  93. * $results = $paginator->paginate($table, $settings);
  94. * ```
  95. *
  96. * This would allow you to have different pagination settings for `Articles` and `Comments` tables.
  97. *
  98. * ### Controlling sort fields
  99. *
  100. * By default CakePHP will automatically allow sorting on any column on the table object being
  101. * paginated. Often times you will want to allow sorting on either associated columns or calculated
  102. * fields. In these cases you will need to define a whitelist of all the columns you wish to allow
  103. * sorting on. You can define the whitelist in the `$settings` parameter:
  104. *
  105. * ```
  106. * $settings = [
  107. * 'Articles' => [
  108. * 'finder' => 'custom',
  109. * 'sortWhitelist' => ['title', 'author_id', 'comment_count'],
  110. * ]
  111. * ];
  112. * ```
  113. *
  114. * Passing an empty array as whitelist disallows sorting altogether.
  115. *
  116. * ### Paginating with custom finders
  117. *
  118. * You can paginate with any find type defined on your table using the `finder` option.
  119. *
  120. * ```
  121. * $settings = [
  122. * 'Articles' => [
  123. * 'finder' => 'popular'
  124. * ]
  125. * ];
  126. * $results = $paginator->paginate($table, $settings);
  127. * ```
  128. *
  129. * Would paginate using the `find('popular')` method.
  130. *
  131. * You can also pass an already created instance of a query to this method:
  132. *
  133. * ```
  134. * $query = $this->Articles->find('popular')->matching('Tags', function ($q) {
  135. * return $q->where(['name' => 'CakePHP'])
  136. * });
  137. * $results = $paginator->paginate($query);
  138. * ```
  139. *
  140. * ### Scoping Request parameters
  141. *
  142. * By using request parameter scopes you can paginate multiple queries in the same controller action:
  143. *
  144. * ```
  145. * $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']);
  146. * $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']);
  147. * ```
  148. *
  149. * Each of the above queries will use different query string parameter sets
  150. * for pagination data. An example URL paginating both results would be:
  151. *
  152. * ```
  153. * /dashboard?articles[page]=1&tags[page]=2
  154. * ```
  155. *
  156. * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The table or query to paginate.
  157. * @param array $settings The settings/configuration used for pagination.
  158. * @return \Cake\Datasource\ResultSetInterface Query results
  159. * @throws \Cake\Network\Exception\NotFoundException
  160. */
  161. public function paginate($object, array $settings = [])
  162. {
  163. $query = null;
  164. if ($object instanceof QueryInterface) {
  165. $query = $object;
  166. $object = $query->repository();
  167. }
  168. $alias = $object->alias();
  169. $options = $this->mergeOptions($alias, $settings);
  170. $options = $this->validateSort($object, $options);
  171. $options = $this->checkLimit($options);
  172. $options += ['page' => 1, 'scope' => null];
  173. $options['page'] = (int)$options['page'] < 1 ? 1 : (int)$options['page'];
  174. list($finder, $options) = $this->_extractFinder($options);
  175. /* @var \Cake\Datasource\RepositoryInterface $object */
  176. if (empty($query)) {
  177. $query = $object->find($finder, $options);
  178. } else {
  179. $query->applyOptions($options);
  180. }
  181. $results = $query->all();
  182. $numResults = count($results);
  183. $count = $numResults ? $query->count() : 0;
  184. $defaults = $this->getDefaults($alias, $settings);
  185. unset($defaults[0]);
  186. $page = $options['page'];
  187. $limit = $options['limit'];
  188. $pageCount = (int)ceil($count / $limit);
  189. $requestedPage = $page;
  190. $page = max(min($page, $pageCount), 1);
  191. $request = $this->_registry->getController()->request;
  192. $order = (array)$options['order'];
  193. $sortDefault = $directionDefault = false;
  194. if (!empty($defaults['order']) && count($defaults['order']) == 1) {
  195. $sortDefault = key($defaults['order']);
  196. $directionDefault = current($defaults['order']);
  197. }
  198. $paging = [
  199. 'finder' => $finder,
  200. 'page' => $page,
  201. 'current' => $numResults,
  202. 'count' => $count,
  203. 'perPage' => $limit,
  204. 'prevPage' => $page > 1,
  205. 'nextPage' => $count > ($page * $limit),
  206. 'pageCount' => $pageCount,
  207. 'sort' => key($order),
  208. 'direction' => current($order),
  209. 'limit' => $defaults['limit'] != $limit ? $limit : null,
  210. 'sortDefault' => $sortDefault,
  211. 'directionDefault' => $directionDefault,
  212. 'scope' => $options['scope'],
  213. ];
  214. if (!$request->getParam('paging')) {
  215. $request->params['paging'] = [];
  216. }
  217. $request->params['paging'] = [$alias => $paging] + (array)$request->getParam('paging');
  218. if ($requestedPage > $page) {
  219. throw new NotFoundException();
  220. }
  221. return $results;
  222. }
  223. /**
  224. * Extracts the finder name and options out of the provided pagination options
  225. *
  226. * @param array $options the pagination options
  227. * @return array An array containing in the first position the finder name and
  228. * in the second the options to be passed to it
  229. */
  230. protected function _extractFinder($options)
  231. {
  232. $type = !empty($options['finder']) ? $options['finder'] : 'all';
  233. unset($options['finder'], $options['maxLimit']);
  234. if (is_array($type)) {
  235. $options = (array)current($type) + $options;
  236. $type = key($type);
  237. }
  238. return [$type, $options];
  239. }
  240. /**
  241. * Merges the various options that Pagination uses.
  242. * Pulls settings together from the following places:
  243. *
  244. * - General pagination settings
  245. * - Model specific settings.
  246. * - Request parameters
  247. *
  248. * The result of this method is the aggregate of all the option sets combined together. You can change
  249. * config value `whitelist` to modify which options/values can be set using request parameters.
  250. *
  251. * @param string $alias Model alias being paginated, if the general settings has a key with this value
  252. * that key's settings will be used for pagination instead of the general ones.
  253. * @param array $settings The settings to merge with the request data.
  254. * @return array Array of merged options.
  255. */
  256. public function mergeOptions($alias, $settings)
  257. {
  258. $defaults = $this->getDefaults($alias, $settings);
  259. $request = $this->_registry->getController()->request;
  260. $scope = Hash::get($settings, 'scope', null);
  261. $query = $request->getQueryParams();
  262. if ($scope) {
  263. $query = Hash::get($request->getQueryParams(), $scope, []);
  264. }
  265. $request = array_intersect_key($query, array_flip($this->_config['whitelist']));
  266. return array_merge($defaults, $request);
  267. }
  268. /**
  269. * Get the settings for a $model. If there are no settings for a specific model, the general settings
  270. * will be used.
  271. *
  272. * @param string $alias Model name to get settings for.
  273. * @param array $settings The settings which is used for combining.
  274. * @return array An array of pagination settings for a model, or the general settings.
  275. */
  276. public function getDefaults($alias, $settings)
  277. {
  278. if (isset($settings[$alias])) {
  279. $settings = $settings[$alias];
  280. }
  281. $defaults = $this->getConfig();
  282. $maxLimit = isset($settings['maxLimit']) ? $settings['maxLimit'] : $defaults['maxLimit'];
  283. $limit = isset($settings['limit']) ? $settings['limit'] : $defaults['limit'];
  284. if ($limit > $maxLimit) {
  285. $limit = $maxLimit;
  286. }
  287. $settings['maxLimit'] = $maxLimit;
  288. $settings['limit'] = $limit;
  289. return $settings + $defaults;
  290. }
  291. /**
  292. * Validate that the desired sorting can be performed on the $object. Only fields or
  293. * virtualFields can be sorted on. The direction param will also be sanitized. Lastly
  294. * sort + direction keys will be converted into the model friendly order key.
  295. *
  296. * You can use the whitelist parameter to control which columns/fields are available for sorting.
  297. * This helps prevent users from ordering large result sets on un-indexed values.
  298. *
  299. * If you need to sort on associated columns or synthetic properties you will need to use a whitelist.
  300. *
  301. * Any columns listed in the sort whitelist will be implicitly trusted. You can use this to sort
  302. * on synthetic columns, or columns added in custom find operations that may not exist in the schema.
  303. *
  304. * @param \Cake\Datasource\RepositoryInterface $object Repository object.
  305. * @param array $options The pagination options being used for this request.
  306. * @return array An array of options with sort + direction removed and replaced with order if possible.
  307. */
  308. public function validateSort(RepositoryInterface $object, array $options)
  309. {
  310. if (isset($options['sort'])) {
  311. $direction = null;
  312. if (isset($options['direction'])) {
  313. $direction = strtolower($options['direction']);
  314. }
  315. if (!in_array($direction, ['asc', 'desc'])) {
  316. $direction = 'asc';
  317. }
  318. $options['order'] = [$options['sort'] => $direction];
  319. }
  320. unset($options['sort'], $options['direction']);
  321. if (empty($options['order'])) {
  322. $options['order'] = [];
  323. }
  324. if (!is_array($options['order'])) {
  325. return $options;
  326. }
  327. $inWhitelist = false;
  328. if (isset($options['sortWhitelist'])) {
  329. $field = key($options['order']);
  330. $inWhitelist = in_array($field, $options['sortWhitelist'], true);
  331. if (!$inWhitelist) {
  332. $options['order'] = [];
  333. return $options;
  334. }
  335. }
  336. $options['order'] = $this->_prefix($object, $options['order'], $inWhitelist);
  337. return $options;
  338. }
  339. /**
  340. * Prefixes the field with the table alias if possible.
  341. *
  342. * @param \Cake\Datasource\RepositoryInterface $object Repository object.
  343. * @param array $order Order array.
  344. * @param bool $whitelisted Whether or not the field was whitelisted
  345. * @return array Final order array.
  346. */
  347. protected function _prefix(RepositoryInterface $object, $order, $whitelisted = false)
  348. {
  349. $tableAlias = $object->alias();
  350. $tableOrder = [];
  351. foreach ($order as $key => $value) {
  352. if (is_numeric($key)) {
  353. $tableOrder[] = $value;
  354. continue;
  355. }
  356. $field = $key;
  357. $alias = $tableAlias;
  358. if (strpos($key, '.') !== false) {
  359. list($alias, $field) = explode('.', $key);
  360. }
  361. $correctAlias = ($tableAlias === $alias);
  362. if ($correctAlias && $whitelisted) {
  363. // Disambiguate fields in schema. As id is quite common.
  364. if ($object->hasField($field)) {
  365. $field = $alias . '.' . $field;
  366. }
  367. $tableOrder[$field] = $value;
  368. } elseif ($correctAlias && $object->hasField($field)) {
  369. $tableOrder[$tableAlias . '.' . $field] = $value;
  370. } elseif (!$correctAlias && $whitelisted) {
  371. $tableOrder[$alias . '.' . $field] = $value;
  372. }
  373. }
  374. return $tableOrder;
  375. }
  376. /**
  377. * Check the limit parameter and ensure it's within the maxLimit bounds.
  378. *
  379. * @param array $options An array of options with a limit key to be checked.
  380. * @return array An array of options for pagination
  381. */
  382. public function checkLimit(array $options)
  383. {
  384. $options['limit'] = (int)$options['limit'];
  385. if (empty($options['limit']) || $options['limit'] < 1) {
  386. $options['limit'] = 1;
  387. }
  388. $options['limit'] = max(min($options['limit'], $options['maxLimit']), 1);
  389. return $options;
  390. }
  391. }