NamedScopeBehavior.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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 http://opensource.org/licenses/mit-license.php MIT
  35. * @author Mark Scherer
  36. * @link https://github.com/dereuromark/cakephp-tools/wiki/Model-Behavior-NamedScope
  37. */
  38. class NamedScopeBehavior extends ModelBehavior {
  39. protected $_defaultConfig = [
  40. 'scope' => [], // 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 $config
  49. * @return void
  50. */
  51. public function setup(Model $Model, $config = []) {
  52. $attribute = !empty($config['attribute']) ? $config['attribute'] : $this->_defaultConfig['attribute'];
  53. if (!empty($Model->$attribute)) {
  54. $config['scope'] = !empty($config['scope']) ? array_merge($Model->$attribute, $config['scope']) : $Model->$attribute;
  55. }
  56. $this->settings[$Model->alias] = $config + $this->_defaultConfig;
  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 = [];
  67. // Passed as scopes (preferred)
  68. if (!empty($queryData['scope'])) {
  69. $scope = !is_array($queryData['scope']) ? [$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']) ? [$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 ($value === null) {
  99. return isset($this->settings[$Model->alias]['scope'][$name]) ? $this->settings[$Model->alias]['scope'][$name] : null;
  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 = []) {
  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. foreach ($customConfig as $keyCustom => $custom) {
  127. if ($keyCustom === 'options') {
  128. foreach ($custom as $keyOptions => $option) {
  129. $config['find']['options'][$keyOptions] = $option;
  130. }
  131. } else {
  132. $config['find'][$keyCustom] = $custom;
  133. }
  134. }
  135. if (!isset($config['find']['type'])) {
  136. $config['find']['type'] = 'all';
  137. }
  138. if (!empty($config['find']['virtualFields'])) {
  139. $Model->virtualFields = $config['find']['virtualFields'] + $Model->virtualFields;
  140. }
  141. if (!empty($config['find']['options']['contain']) && !$Model->Behaviors->loaded('Containable')) {
  142. $Model->Behaviors->load('Containable');
  143. }
  144. return $Model->find($config['find']['type'], $config['find']['options']);
  145. }
  146. /**
  147. * List all scoped find groups available.
  148. *
  149. * @param Model $Model
  150. * @return array
  151. */
  152. public function scopedFinds(Model $Model) {
  153. $attribute = $this->settings[$Model->alias]['findAttribute'];
  154. if (empty($Model->$attribute)) {
  155. return [];
  156. }
  157. $data = [];
  158. foreach ($Model->$attribute as $group => $config) {
  159. $data[$group] = $config['name'];
  160. }
  161. return $data;
  162. }
  163. /**
  164. * Resolves the scope names into their conditions.
  165. *
  166. * @param array $scopes
  167. * @param string $modelName
  168. * @return array
  169. */
  170. protected function _conditions(array $scopes, $modelName) {
  171. $conditions = [];
  172. foreach ($scopes as $scope) {
  173. if (strpos($scope, '.')) {
  174. list($scopeModel, $scope) = explode('.', $scope);
  175. } else {
  176. $scopeModel = $modelName;
  177. }
  178. if (!empty($this->settings[$scopeModel]['scope'][$scope])) {
  179. $conditions[] = [$this->settings[$scopeModel]['scope'][$scope]];
  180. }
  181. }
  182. return $conditions;
  183. }
  184. }