LinkableBehavior.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <?php
  2. App::uses('ModelBehavior', 'Model');
  3. /**
  4. * LinkableBehavior
  5. * Light-weight approach for data mining on deep relations between models.
  6. * Join tables based on model relations to easily enable right to left find operations.
  7. * Original behavior by rafaelbandeira3 on GitHub.
  8. * Includes modifications from Terr, n8man, and Chad Jablonski
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * GiulianoB ( https://github.com/giulianob/linkable )
  14. *
  15. * @version 1.0;
  16. *
  17. * @version 1.1:
  18. * -Brought in improvements and test cases from Terr. However, THIS VERSION OF LINKABLE IS NOT DROP IN COMPATIBLE WITH Terr's VERSION!
  19. * -If fields aren't specified, will now return all columns of that model
  20. * -No need to specify the foreign key condition if a custom condition is given. Linkable will automatically include the foreign key relationship.
  21. * -Ability to specify the exact condition Linkable should use (e.g. $this->Post->find('first', array('link' => array('User' => array('conditions' => array('exactly' => 'User.last_post_id = Post.id'))))) )
  22. * This is usually required when doing on-the-fly joins since Linkable generally assumes a belongsTo relationship when no specific relationship is found and may produce invalid foreign key conditions.
  23. * -Linkable will no longer break queries that use SQL COUNTs
  24. *
  25. * @version 1.2:
  26. * @modified Mark Scherer
  27. * - works with cakephp2.x (89.84 test coverage)
  28. */
  29. class LinkableBehavior extends ModelBehavior {
  30. protected $_key = 'link';
  31. protected $_options = array(
  32. 'type' => true, 'table' => true, 'alias' => true,
  33. 'conditions' => true, 'fields' => true, 'reference' => true,
  34. 'class' => true, 'defaults' => true
  35. );
  36. protected $_defaultConfig = array('type' => 'LEFT');
  37. public function beforeFind(Model $Model, $query) {
  38. if (isset($query[$this->_key])) {
  39. $optionsDefaults = $this->_defaultConfig + array('reference' => $Model->alias, $this->_key => array());
  40. $optionsKeys = $this->_options + array($this->_key => true);
  41. // If containable is being used, then let it set the recursive!
  42. if (empty($query['contain'])) {
  43. $query = array_merge(array('joins' => array()), $query, array('recursive' => -1));
  44. } else {
  45. $query = array_merge(array('joins' => array()), $query);
  46. }
  47. $iterators[] = $query[$this->_key];
  48. $cont = 0;
  49. do {
  50. $iterator = $iterators[$cont];
  51. $defaults = $optionsDefaults;
  52. if (isset($iterator['defaults'])) {
  53. $defaults = array_merge($defaults, $iterator['defaults']);
  54. unset($iterator['defaults']);
  55. }
  56. $iterations = Set::normalize($iterator);
  57. foreach ($iterations as $alias => $options) {
  58. if ($options === null) {
  59. $options = array();
  60. }
  61. $options = array_merge($defaults, compact('alias'), $options);
  62. if (empty($options['alias'])) {
  63. throw new InvalidArgumentException(sprintf('%s::%s must receive aliased links', get_class($this), __FUNCTION__));
  64. }
  65. // try to find the class name - important for aliased relations
  66. foreach ($Model->belongsTo as $relationAlias => $relation) {
  67. if (empty($relation['className']) || $relationAlias !== $options['alias']) {
  68. continue;
  69. }
  70. $options['class'] = $relation['className'];
  71. break;
  72. }
  73. foreach ($Model->hasOne as $relationAlias => $relation) {
  74. if (empty($relation['className']) || $relationAlias !== $options['alias']) {
  75. continue;
  76. }
  77. $options['class'] = $relation['className'];
  78. break;
  79. }
  80. foreach ($Model->hasMany as $relationAlias => $relation) {
  81. if (empty($relation['className']) || $relationAlias !== $options['alias']) {
  82. continue;
  83. }
  84. $options['class'] = $relation['className'];
  85. break;
  86. }
  87. // guess it then
  88. if (empty($options['table']) && empty($options['class'])) {
  89. $options['class'] = $options['alias'];
  90. } elseif (!empty($options['table']) && empty($options['class'])) {
  91. $options['class'] = Inflector::classify($options['table']);
  92. }
  93. // the incoming model to be linked in query using class and alias
  94. $_Model = ClassRegistry::init(array('class' => $options['class'], 'alias' => $options['alias']));
  95. // the already in query model that links to $_Model
  96. $Reference = ClassRegistry::init($options['reference']);
  97. $db = $_Model->getDataSource();
  98. $associations = $_Model->getAssociated();
  99. if (isset($Reference->belongsTo[$_Model->alias])) {
  100. $type = 'hasOne';
  101. $association = $Reference->belongsTo[$_Model->alias];
  102. } elseif (isset($Reference->hasOne[$_Model->alias])) {
  103. $type = 'belongsTo';
  104. $association = $Reference->hasOne[$_Model->alias];
  105. } elseif (isset($Reference->hasMany[$_Model->alias])) {
  106. $type = 'belongsTo';
  107. $association = $Reference->hasMany[$_Model->alias];
  108. } elseif (isset($associations[$Reference->alias])) {
  109. $type = $associations[$Reference->alias];
  110. $association = $_Model->{$type}[$Reference->alias];
  111. } else {
  112. $_Model->bindModel(array('belongsTo' => array($Reference->alias)));
  113. $type = 'belongsTo';
  114. $association = $_Model->{$type}[$Reference->alias];
  115. $_Model->unbindModel(array('belongsTo' => array($Reference->alias)));
  116. }
  117. if (!isset($options['conditions'])) {
  118. $options['conditions'] = array();
  119. } elseif (!is_array($options['conditions'])) {
  120. // Support for string conditions
  121. $options['conditions'] = array($options['conditions']);
  122. }
  123. if (isset($options['conditions']['exactly'])) {
  124. if (is_array($options['conditions']['exactly']))
  125. $options['conditions'] = reset($options['conditions']['exactly']);
  126. else
  127. $options['conditions'] = array($options['conditions']['exactly']);
  128. } else {
  129. if ($type === 'belongsTo') {
  130. $modelKey = $_Model->escapeField($association['foreignKey']);
  131. $modelKey = str_replace($_Model->alias, $options['alias'], $modelKey);
  132. $referenceKey = $Reference->escapeField($Reference->primaryKey);
  133. $options['conditions'][] = "{$referenceKey} = {$modelKey}";
  134. } elseif ($type === 'hasAndBelongsToMany') {
  135. // try to determine fields by HABTM model
  136. if (isset($association['with'])) {
  137. $Link = $_Model->{$association['with']};
  138. if (isset($Link->belongsTo[$_Model->alias])) {
  139. $modelLink = $Link->escapeField($Link->belongsTo[$_Model->alias]['foreignKey']);
  140. }
  141. if (isset($Link->belongsTo[$Reference->alias])) {
  142. $referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey']);
  143. }
  144. } else {
  145. $Link = $_Model->{Inflector::classify($association['joinTable'])};
  146. }
  147. // try to determine fields by current model relation settings
  148. if (empty($modelLink) && !empty($_Model->{$type}[$Reference->alias]['foreignKey'])) {
  149. $modelLink = $Link->escapeField($_Model->{$type}[$Reference->alias]['foreignKey']);
  150. }
  151. if (empty($referenceLink) && !empty($_Model->{$type}[$Reference->alias]['associationForeignKey'])) {
  152. $referenceLink = $Link->escapeField($_Model->{$type}[$Reference->alias]['associationForeignKey']);
  153. }
  154. // fallback to defaults otherwise
  155. if (empty($modelLink)) {
  156. $modelLink = $Link->escapeField($association['foreignKey']);
  157. }
  158. if (empty($referenceLink)) {
  159. $referenceLink = $Link->escapeField($association['associationForeignKey']);
  160. }
  161. $referenceKey = $Reference->escapeField();
  162. $query['joins'][] = array(
  163. 'alias' => $Link->alias,
  164. 'table' => $Link->table, //$Link->getDataSource()->fullTableName($Link),
  165. 'conditions' => "{$referenceLink} = {$referenceKey}",
  166. 'type' => 'LEFT',
  167. );
  168. $modelKey = $_Model->escapeField();
  169. $modelKey = str_replace($_Model->alias, $options['alias'], $modelKey);
  170. $options['conditions'][] = "{$modelLink} = {$modelKey}";
  171. } else {
  172. $referenceKey = $Reference->escapeField($association['foreignKey']);
  173. $modelKey = $_Model->escapeField($_Model->primaryKey);
  174. $modelKey = str_replace($_Model->alias, $options['alias'], $modelKey);
  175. $options['conditions'][] = "{$modelKey} = {$referenceKey}";
  176. }
  177. }
  178. if (empty($options['table'])) {
  179. $options['table'] = $_Model->table;
  180. }
  181. // Decide whether we should mess with the fields or not
  182. // If this query is a COUNT query then we just leave it alone
  183. if (!isset($query['fields']) || is_array($query['fields']) || strpos($query['fields'], 'COUNT(*)') === false) {
  184. if (!empty($options['fields'])) {
  185. if ($options['fields'] === true && !empty($association['fields'])) {
  186. $options['fields'] = $db->fields($_Model, null, $association['fields']);
  187. } elseif ($options['fields'] === true) {
  188. $options['fields'] = $db->fields($_Model);
  189. } else {
  190. $options['fields'] = $db->fields($_Model, null, $options['fields']);
  191. }
  192. } elseif (!isset($options['fields']) || (isset($options['fields']) && !is_array($options['fields']))) {
  193. if (!empty($association['fields'])) {
  194. $options['fields'] = $db->fields($_Model, null, $association['fields']);
  195. } else {
  196. $options['fields'] = $db->fields($_Model);
  197. }
  198. }
  199. if (!empty($options['class']) && $options['class'] !== $alias) {
  200. //$options['fields'] = str_replace($options['class'], $alias, $options['fields']);
  201. }
  202. if (is_array($query['fields'])) {
  203. $query['fields'] = array_merge($query['fields'], $options['fields']);
  204. } else {
  205. // If user didn't specify any fields then select all fields by default (just as find would)
  206. $query['fields'] = array_merge($db->fields($Model), $options['fields']);
  207. }
  208. }
  209. $options[$this->_key] = array_merge($options[$this->_key], array_diff_key($options, $optionsKeys));
  210. $options = array_intersect_key($options, $optionsKeys);
  211. if (!empty($options[$this->_key])) {
  212. $iterators[] = $options[$this->_key] + array('defaults' => array_merge($defaults, array('reference' => $options['class'])));
  213. }
  214. $query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'conditions' => true));
  215. }
  216. $cont++;
  217. $notDone = isset($iterators[$cont]);
  218. } while ($notDone);
  219. }
  220. unset($query['link']);
  221. return $query;
  222. }
  223. }