NamedScopeBehavior.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <?php
  2. App::uses('ModelBehavior', 'Model');
  3. /**
  4. * A behavior to keep scopes and conditions DRY across multiple methods and models.
  5. * Those scopes can be added to any find() options array.
  6. * Additionally, custom scopedFinds can help to reduce the amount of methods needed
  7. * through a global config and one scopedFind() method.
  8. *
  9. * Basic idea taken and modified/fixed from https://github.com/netguru/namedscopebehavior
  10. * and https://github.com/josegonzalez/cakephp-simple-scope
  11. *
  12. * - it's now "scope" instead of "scopes" (singular and now analogous to "contain" etc)
  13. * - corrected syntax, indentation
  14. * - reads the model's 'scopes' attribute if applicable
  15. * - allows 'scopes' in scopedFind()
  16. *
  17. * If used across models, it is advisable to load this globally via $actAs in the AppModel
  18. * (just as with Containable).
  19. *
  20. * In case you need to dynamically set the Model->scopes attribute, use the constructor:
  21. *
  22. * public function __construct($id = false, $table = null, $ds = null) {
  23. * $this->scopes = ...
  24. * parent::__construct($id, $table, $ds);
  25. * }
  26. *
  27. * The order is important since behaviors are loaded in the parent constructor.
  28. *
  29. * Note that it can be vital to use the model prefixes in the conditions and in the scopes
  30. * to avoid SQL errors or naming conflicts.
  31. *
  32. * See the test cases for more complex examples.
  33. *
  34. * @license MIT
  35. * @author Mark Scherer
  36. * @link https://github.com/dereuromark/tools/wiki/Model-Behavior-NamedScope
  37. */
  38. class NamedScopeBehavior extends ModelBehavior {
  39. protected $_defaults = array(
  40. 'scope' => array(), // Container to hold all scopes
  41. 'attribute' => 'scopes', // Model attribute to hold the custom scopes
  42. 'findAttribute' => 'scopedFinds' // Model attribute to hold the custom finds
  43. );
  44. /**
  45. * Sets up the behavior including settings (i.e. scope).
  46. *
  47. * @param Model $Model
  48. * @param array $settings
  49. * @return void
  50. */
  51. public function setup(Model $Model, $settings = array()) {
  52. $attribute = !empty($settings['attribute']) ? $settings['attribute'] : $this->_defaults['attribute'];
  53. if (!empty($Model->$attribute)) {
  54. $settings['scope'] = !empty($settings['scope']) ? array_merge($Model->$attribute, $settings['scope']) : $Model->$attribute;
  55. }
  56. $this->settings[$Model->alias] = $settings + $this->_defaults;
  57. }
  58. /**
  59. * Triggered before the actual find.
  60. *
  61. * @param Model $Model
  62. * @param array $queryData
  63. * @return mixed
  64. */
  65. public function beforeFind(Model $Model, $queryData) {
  66. $scopes = array();
  67. // Passed as scopes (preferred)
  68. if (!empty($queryData['scope'])) {
  69. $scope = !is_array($queryData['scope']) ? array($queryData['scope']) : $queryData['scope'];
  70. $scopes = array_merge($scopes, $scope);
  71. }
  72. // Passed as conditions['scope']
  73. if (is_array($queryData['conditions']) && !empty($queryData['conditions']['scope'])) {
  74. $scope = !is_array($queryData['conditions']['scope']) ? array($queryData['conditions']['scope']) : $queryData['conditions']['scope'];
  75. unset($queryData['conditions']['scope']);
  76. $scopes = array_merge($scopes, $scope);
  77. }
  78. // If there are scopes defined, we need to get rid of possible condition set earlier by find() method if model->id was set
  79. if (!empty($scopes) && !empty($Model->id) && !empty($queryData['conditions']["`{$Model->alias}`.`{$Model->primaryKey}`"]) &&
  80. $queryData['conditions']["`{$Model->alias}`.`{$Model->primaryKey}`"] == $Model->id) {
  81. unset($queryData['conditions']["`{$Model->alias}`.`{$Model->primaryKey}`"]);
  82. }
  83. $queryData['conditions'][] = $this->_conditions($scopes, $Model->alias);
  84. return $queryData;
  85. }
  86. /**
  87. * Set/get scopes.
  88. *
  89. * @param Model $Model
  90. * @param string $name
  91. * @param mixed $value
  92. * @return mixed
  93. */
  94. public function scope(Model $Model, $name = null, $value = null) {
  95. if ($name === null) {
  96. return $this->settings[$Model->alias]['scope'];
  97. }
  98. if (in_array($name, $this->settings[$Model->alias]['scope'])) {
  99. continue;
  100. }
  101. $this->settings[$Model->alias]['scope'][$name] = $value;
  102. }
  103. /**
  104. * Scoped find() with a specific key.
  105. *
  106. * If you need to switch the type, use the customConfig:
  107. * array('type' => 'count')
  108. * All active find methods are supported.
  109. *
  110. * @param mixed $Model
  111. * @param mixed $key
  112. * @param array $customConfig
  113. * @return mixed
  114. * @throws RuntimeException On invalid configs.
  115. */
  116. public function scopedFind(Model $Model, $key, array $customConfig = array()) {
  117. $attribute = $this->settings[$Model->alias]['findAttribute'];
  118. if (empty($Model->$attribute)) {
  119. throw new RuntimeException('No scopedFinds configs in ' . $Model->alias);
  120. }
  121. $finds = $Model->$attribute;
  122. if (empty($finds[$key])) {
  123. throw new RuntimeException('No scopedFinds configs in ' . $Model->alias . ' for the key ' . $key);
  124. }
  125. $config = $finds[$key];
  126. $config['find'] = array_merge_recursive($config['find'], $customConfig);
  127. if (!isset($config['find']['type'])) {
  128. $config['find']['type'] = 'all';
  129. }
  130. if (!empty($config['find']['virtualFields'])) {
  131. $Model->virtualFields = $config['find']['virtualFields'] + $Model->virtualFields;
  132. }
  133. if (!empty($config['find']['options']['contain']) && !$Model->Behaviors->loaded('Containable')) {
  134. $Model->Behaviors->load('Containable');
  135. }
  136. return $Model->find($config['find']['type'], $config['find']['options']);
  137. }
  138. /**
  139. * List all scoped find groups available.
  140. *
  141. * @param Model $Model
  142. * @return array
  143. */
  144. public function scopedFinds(Model $Model) {
  145. $attribute = $this->settings[$Model->alias]['findAttribute'];
  146. if (empty($Model->$attribute)) {
  147. return array();
  148. }
  149. $data = array();
  150. foreach ($Model->$attribute as $group => $config) {
  151. $data[$group] = $config['name'];
  152. }
  153. return $data;
  154. }
  155. /**
  156. * Resolves the scope names into their conditions.
  157. *
  158. * @param array $scopes
  159. * @param string $modelName
  160. * @return array
  161. */
  162. protected function _conditions(array $scopes, $modelName) {
  163. $conditions = array();
  164. foreach ($scopes as $scope) {
  165. if (strpos($scope, '.')) {
  166. list($scopeModel, $scope) = explode('.', $scope);
  167. } else {
  168. $scopeModel = $modelName;
  169. }
  170. if (!empty($this->settings[$scopeModel]['scope'][$scope])) {
  171. $conditions[] = array($this->settings[$scopeModel]['scope'][$scope]);
  172. }
  173. }
  174. return $conditions;
  175. }
  176. }