NamedScopeBehavior.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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 adviced 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. */
  37. class NamedScopeBehavior extends ModelBehavior {
  38. protected $_defaults = array(
  39. 'scope' => array(), // Container to hold all scopes
  40. 'attribute' => 'scopes', // Model attribute to hold the custom scopes
  41. 'findAttribute' => 'scopedFinds' // Model attribute to hold the custom finds
  42. );
  43. /**
  44. * Sets up the behavior including settings (i.e. scope).
  45. *
  46. * @param Model $Model
  47. * @param array $settings
  48. * @return void
  49. */
  50. public function setup(Model $Model, $settings = array()) {
  51. $attribute = !empty($settings['attribute']) ? $settings['attribute'] : $this->_defaults['attribute'];
  52. if (!empty($Model->$attribute)) {
  53. $settings['scope'] = !empty($settings['scope']) ? array_merge($Model->$attribute, $settings['scope']) : $Model->$attribute;
  54. }
  55. $this->settings[$Model->alias] = $settings + $this->_defaults;
  56. }
  57. /**
  58. * Triggered before the actual find.
  59. *
  60. * @param Model $Model
  61. * @param array $queryData
  62. * @return mixed
  63. */
  64. public function beforeFind(Model $Model, $queryData) {
  65. $scopes = array();
  66. // passed as scopes
  67. if (!empty($queryData['scope'])) {
  68. $scope = !is_array($queryData['scope']) ? array($queryData['scope']) : $queryData['scope'];
  69. $scopes = array_merge($scopes, $scope);
  70. }
  71. // passed as conditions['scope']
  72. if (is_array($queryData['conditions']) && !empty($queryData['conditions']['scope'])) {
  73. $scope = !is_array($queryData['conditions']['scope']) ? array($queryData['conditions']['scope']) : $queryData['conditions']['scope'];
  74. unset($queryData['conditions']['scope']);
  75. $scopes = array_merge($scopes, $scope);
  76. }
  77. // if there are scopes defined, we need to get rid of possible condition set earlier by find() method if model->id was set
  78. if (!empty($scopes) && !empty($Model->id) && !empty($queryData['conditions']["`{$Model->alias}`.`{$Model->primaryKey}`"]) && $queryData['conditions']["`{$Model->alias}`.`{$Model->primaryKey}`"] ==
  79. $Model->id) {
  80. unset($queryData['conditions']["`{$Model->alias}`.`{$Model->primaryKey}`"]);
  81. }
  82. $queryData['conditions'][] = $this->_conditions($scopes, $Model->alias);
  83. return $queryData;
  84. }
  85. /**
  86. * Set/get scopes.
  87. *
  88. * @param Model $Model
  89. * @param string $name
  90. * @param mixed $value
  91. * @return mixed
  92. */
  93. public function scope(Model $Model, $name = null, $value = null) {
  94. if ($name === null) {
  95. return $this->settings[$Model->alias]['scope'];
  96. }
  97. if (in_array($name, $this->settings[$Model->alias]['scope'])) {
  98. continue;
  99. }
  100. $this->settings[$Model->alias]['scope'][$name] = $value;
  101. }
  102. /**
  103. * Scoped find() with a specific key.
  104. *
  105. * If you need to switch the type, use the customConfig:
  106. * array('type' => 'count')
  107. * All active find methods are supported.
  108. *
  109. * @param mixed $Model
  110. * @param mixed $key
  111. * @param array $customConfig
  112. * @return mixed
  113. * @throws RuntimeException On invalid configs.
  114. */
  115. public function scopedFind(Model $Model, $key, array $customConfig = array()) {
  116. $attribute = $this->settings[$Model->alias]['findAttribute'];
  117. if (empty($Model->$attribute)) {
  118. throw new RuntimeException('No scopedFinds configs in ' . $Model->alias);
  119. }
  120. $finds = $Model->$attribute;
  121. if (empty($finds[$key])) {
  122. throw new RuntimeException('No scopedFinds configs in ' . $Model->alias . ' for the key ' . $key);
  123. }
  124. $config = $finds[$key];
  125. $config['find'] = array_merge_recursive($config['find'], $customConfig);
  126. if (!isset($config['find']['type'])) {
  127. $config['find']['type'] = 'all';
  128. }
  129. if (!empty($config['find']['virtualFields'])) {
  130. $Model->virtualFields = $config['find']['virtualFields'] + $Model->virtualFields;
  131. }
  132. if (!empty($config['find']['options']['contain']) && !$Model->Behaviors->loaded('Containable')) {
  133. $Model->Behaviors->load('Containable');
  134. }
  135. return $Model->find($config['find']['type'], $config['find']['options']);
  136. }
  137. /**
  138. * List all scoped find groups available.
  139. *
  140. * @param Model $Model
  141. * @return array
  142. */
  143. public function scopedFinds(Model $Model) {
  144. $attribute = $this->settings[$Model->alias]['findAttribute'];
  145. if (empty($Model->$attribute)) {
  146. return array();
  147. }
  148. $data = array();
  149. foreach ($Model->$attribute as $group => $config) {
  150. $data[$group] = $config['name'];
  151. }
  152. return $data;
  153. }
  154. /**
  155. * Resolves the scope names into their conditions.
  156. *
  157. * @param array $scopes
  158. * @param string $modelName
  159. * @return array
  160. */
  161. protected function _conditions(array $scopes, $modelName) {
  162. $conditions = array();
  163. foreach ($scopes as $scope) {
  164. if (strpos($scope, '.')) {
  165. list($scopeModel, $scope) = explode('.', $scope);
  166. } else {
  167. $scopeModel = $modelName;
  168. }
  169. if (!empty($this->settings[$scopeModel]['scope'][$scope])) {
  170. $conditions[] = array($this->settings[$scopeModel]['scope'][$scope]);
  171. }
  172. }
  173. return $conditions;
  174. }
  175. }