PageRenderTime 48ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/models/behaviors/bindable.php

http://skygames.googlecode.com/
PHP | 504 lines | 301 code | 66 blank | 137 comment | 113 complexity | 6cf96b09d4365ff8af49aef6d2893897 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, CC-BY-SA-3.0
  1. <?php
  2. /* SVN FILE: $Id: bindable.php 46 2008-05-09 13:39:22Z mgiglesias $ */
  3. /**
  4. * Bindable Behavior class file.
  5. *
  6. * Go to the Bindable Behavior page at Cake Syrup to learn more about it:
  7. *
  8. * http://cake-syrup.sourceforge.net/ingredients/bindable-behavior/
  9. *
  10. * @filesource
  11. * @author Mariano Iglesias
  12. * @link http://cake-syrup.sourceforge.net/ingredients/bindable-behavior/
  13. * @version $Revision: 46 $
  14. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  15. * @package app
  16. * @subpackage app.models.behaviors
  17. */
  18. /**
  19. * Model behavior to support unbinding of models.
  20. *
  21. * @package app
  22. * @subpackage app.models.behaviors
  23. */
  24. class BindableBehavior extends ModelBehavior {
  25. /**
  26. * Types of relationships available for models
  27. *
  28. * @var array
  29. * @access private
  30. */
  31. var $__bindings = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
  32. /**
  33. * Initiate behavior for the model using specified settings. Available settings:
  34. *
  35. * - recursive: (boolean, optional) set to true to allow bindable to automatically
  36. * determine the recursiveness level needed to fetch specified models,
  37. * and set the model recursiveness to this level. setting it to false
  38. * disables this feature. DEFAULTS TO: true
  39. *
  40. * - notices: (boolean, optional) issues E_NOTICES for bindings referenced in a
  41. * bindable call that are not valid. DEFAULTS TO: false
  42. *
  43. * - autoFields: (boolean, optional) auto-add needed fields to fetch requested
  44. * bindings. DEFAULTS TO: true
  45. *
  46. * @param object $Model Model using the behavior
  47. * @param array $settings Settings to override for model.
  48. * @access public
  49. */
  50. function setup(&$Model, $settings = array()) {
  51. $default = array('recursive' => true, 'notices' => false, 'autoFields' => true);
  52. if (!isset($this->settings[$Model->alias])) {
  53. $this->settings[$Model->alias] = $default;
  54. }
  55. $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], ife(is_array($settings), $settings, array()));
  56. }
  57. /**
  58. * Unbinds all relations from a model except the specified ones. Calling this function without
  59. * parameters unbinds all related models.
  60. *
  61. * @param object $Model Model on which binding restriction is being applied
  62. * @return mixed If direct call, integer with recommended value for recursive
  63. * @access public
  64. */
  65. function restrict(&$Model) {
  66. $innerCall = false;
  67. $reset = true;
  68. $recursive = null;
  69. $arguments = func_get_args();
  70. $totalArguments = count($arguments);
  71. // Get the model, and find out if we're being called directly
  72. $shift = 1;
  73. if ($totalArguments > 1 && is_bool($arguments[1])) {
  74. $reset = $arguments[1];
  75. $shift++;
  76. if ($totalArguments > 2 && is_bool($arguments[2])) {
  77. $innerCall = $arguments[2];
  78. $shift++;
  79. }
  80. }
  81. // Process arguments into a set of models to include
  82. $arguments = array_slice($arguments, $shift);
  83. foreach($arguments as $index => $argument) {
  84. if (is_array($argument)) {
  85. if (!empty($argument)) {
  86. $arguments = array_merge($arguments, $argument);
  87. }
  88. unset($arguments[$index]);
  89. }
  90. }
  91. $models = array();
  92. if (!$innerCall) {
  93. $models = $this->__models($Model, $arguments, $this->settings[$Model->alias]['notices'], $this->settings[$Model->alias]['autoFields']);
  94. $recursive = -1;
  95. if (!empty($models)) {
  96. $recursive = $this->__recursivity($models);
  97. }
  98. } else if (!empty($arguments)) {
  99. $models = $arguments;
  100. }
  101. // Go through all models and run bindable on inner models
  102. foreach($models as $name => $children) {
  103. if (isset($Model->$name)) {
  104. if (isset($children['__settings__'])) {
  105. foreach($this->__bindings as $relation) {
  106. if (isset($Model->{$relation}[$name])) {
  107. if (!$reset) {
  108. $this->__backupAssociations($Model);
  109. }
  110. $Model->bindModel(array($relation => array(
  111. $name => array_merge($Model->{$relation}[$name], $children['__settings__'])
  112. )), $reset);
  113. }
  114. }
  115. unset($children['__settings__']);
  116. }
  117. // Run bindable on inner model
  118. if (!isset($Model->__backInnerAssociation)) {
  119. $Model->__backInnerAssociation = array();
  120. }
  121. $Model->__backInnerAssociation[] = $name;
  122. $this->restrict($Model->$name, $reset, true, $children);
  123. }
  124. }
  125. // Setup mandatory fields
  126. if (!$innerCall && $this->settings[$Model->alias]['autoFields'] && isset($models['__settings__'])) {
  127. if (!empty($models['__settings__']['fields'])) {
  128. if (empty($this->__fields[$Model->alias])) {
  129. $this->__fields[$Model->alias] = array();
  130. }
  131. $this->__fields[$Model->alias] = $models['__settings__']['fields'];
  132. }
  133. unset($models['__settings__']);
  134. }
  135. // Unbind unneeded models
  136. $unbind = array();
  137. $models = array_keys($models);
  138. $bindings = $Model->getAssociated();
  139. foreach($bindings as $bindingName => $relation) {
  140. if (!in_array($bindingName, $models)) {
  141. $unbind[$relation][] = $bindingName;
  142. }
  143. }
  144. if (!empty($unbind)) {
  145. if (!$reset) {
  146. $this->__backupAssociations($Model);
  147. }
  148. $Model->unbindModel($unbind, $reset);
  149. }
  150. // Keep a reference that this model is the originator of a chain-bindable call
  151. if (!$innerCall && $reset) {
  152. $this->__runResetBindable[$Model->alias] = true;
  153. }
  154. // If specified, set this model's recursiveness level
  155. if (!$innerCall && $this->settings[$Model->alias]['recursive'] === true && $recursive !== null) {
  156. $Model->__backRecursive = $Model->recursive;
  157. $Model->recursive = $recursive;
  158. }
  159. return $recursive;
  160. }
  161. /**
  162. * Resets all relations and inner model relations after calling restrict()
  163. *
  164. * @param object $Model Model using the behavior
  165. * @param boolean $resetOriginal Force resetting original associations that may have been set to not reset
  166. * @access public
  167. */
  168. function resetBindable(&$Model, $resetOriginal = false) {
  169. $innerAssociations = array();
  170. if (isset($Model->__backInnerAssociation)) {
  171. $innerAssociations = $Model->__backInnerAssociation;
  172. unset($Model->__backInnerAssociation);
  173. }
  174. if ($resetOriginal && !empty($Model->__backOriginalAssociation)) {
  175. $Model->__backAssociation = array_pop($Model->__backOriginalAssociation);
  176. if (empty($Model->__backOriginalAssociation)) {
  177. unset($Model->__backOriginalAssociation);
  178. }
  179. }
  180. if (empty($innerAssociations) && !empty($Model->__backAssociation)) {
  181. $innerAssociations = array();
  182. foreach($this->__bindings as $relation) {
  183. if (!empty($Model->__backAssociation[$relation])) {
  184. $innerAssociations = array_merge($innerAssociations, array_keys($Model->__backAssociation[$relation]));
  185. }
  186. }
  187. }
  188. if (isset($Model->__backAssociation)) {
  189. //$Model->__resetAssociations();
  190. }
  191. if (isset($Model->__backRecursive)) {
  192. $Model->recursive = $Model->__backRecursive;
  193. unset($Model->__backRecursive);
  194. }
  195. if (isset($this->__fields[$Model->alias])) {
  196. unset($this->__fields[$Model->alias]);
  197. }
  198. foreach($innerAssociations as $currentModel) {
  199. $this->resetBindable($Model->$currentModel, $resetOriginal);
  200. }
  201. }
  202. /**
  203. * Runs before a find() operation. Used to allow 'restrict' setting
  204. * as part of the find call, like this:
  205. *
  206. * Model->find('all', array('restrict' => array('Model1', 'Model2')));
  207. *
  208. * Model->find('all', array('restrict' => array(
  209. * 'Model1' => array('Model11', 'Model12'),
  210. * 'Model2',
  211. * 'Model3' => array(
  212. * 'Model31' => 'Model311',
  213. * 'Model32',
  214. * 'Model33' => array('Model331', 'Model332')
  215. * )));
  216. *
  217. * @param object $Model Model using the behavior
  218. * @param array $query Query parameters as set by cake
  219. * @access public
  220. */
  221. function beforeFind(&$Model, $query) {
  222. if (isset($query['restrict'])) {
  223. $reset = true;
  224. if (is_bool(end($query['restrict']))) {
  225. $reset = array_pop($query['restrict']);
  226. }
  227. $query = array_merge(compact('reset'), $query);
  228. $this->restrict($Model, $query['reset'], false, $query['restrict']);
  229. }
  230. if ($Model->findQueryType != 'list' && is_array($query['fields']) && !empty($this->__fields[$Model->alias]) && !empty($query['fields'])) {
  231. $query['fields'] = array_unique(array_merge($query['fields'], $this->__fields[$Model->alias]));
  232. }
  233. return $query;
  234. }
  235. /**
  236. * Runs after a find() operation.
  237. *
  238. * @param object $Model Model using the behavior
  239. * @param array $results Results of the find operation.
  240. * @access public
  241. */
  242. function afterFind(&$Model, $results) {
  243. if (isset($this->__runResetBindable[$Model->alias]) && $this->__runResetBindable[$Model->alias]) {
  244. $this->resetBindable($Model);
  245. unset($this->__runResetBindable[$Model->alias]);
  246. }
  247. }
  248. /**
  249. * Backup associations for a model right before a non-resettable binding
  250. * operation.
  251. *
  252. * @param object $Model Model being processed
  253. * @access private
  254. */
  255. function __backupAssociations(&$Model) {
  256. $bindings = array();
  257. foreach($this->__bindings as $relation) {
  258. $bindings[$relation] = $Model->{$relation};
  259. }
  260. if (empty($Model->__backOriginalAssociation)) {
  261. $Model->__backOriginalAssociation = array();
  262. }
  263. $Model->__backOriginalAssociation[] = $bindings;
  264. }
  265. /**
  266. * Get a list of models in the form: Model1 => array(Model2, ...), converting
  267. * dot-notation arguments (i.e: Model1.Model2.Model3) to their depth-notation
  268. * equivalent (i.e: Model1 => Model2 => Model3). The convertion is used for
  269. * backwards compatibility with previous versions of bindable (expects).
  270. *
  271. * @param object $Model Model being processed
  272. * @param array $arguments Set of arguments to convert
  273. * @param boolean $notices Set to true to throw a notice when a binding does not exist
  274. * @param boolean $autoFields Discover and add fields needed to fetch requested bindings
  275. * @param boolean $inner Set to true to indicate inner call, false otherwise
  276. * @return array Converted arguments
  277. * @access private
  278. */
  279. function __models(&$Model, $arguments, $notices = false, $autoFields = true, $inner = false) {
  280. $models = array();
  281. $bindings = $Model->getAssociated();
  282. $settings = array('conditions', 'fields', 'limit', 'offset', 'order');
  283. foreach($arguments as $key => $children) {
  284. $name = null;
  285. $setting = null;
  286. $settingValue = array();
  287. if (is_numeric($key) && !is_array($children)) {
  288. $name = $children;
  289. $children = array();
  290. } else if (!is_numeric($key)) {
  291. $name = $key;
  292. }
  293. if (!empty($name) && is_string($name) && !in_array($name, $settings) && $Model->hasField($name) && (!isset($Model->$name) || !is_object($Model->$name)) && (!isset($children['fields']) || !in_array($name, $children['fields']))) {
  294. $setting = 'fields';
  295. $settingValue = array($name);
  296. } else if (!empty($name) && in_array($name, $settings)) {
  297. $setting = $name;
  298. $settingValue = $children;
  299. $children = array();
  300. }
  301. if (!empty($setting)) {
  302. if ($setting == 'fields') {
  303. if (!is_array($children)) {
  304. $children = array($setting => array());
  305. } else if (!isset($children[$setting])) {
  306. $children[$setting] = array();
  307. }
  308. $settingValue = array_merge($children[$setting], ife(!is_array($settingValue), array($settingValue), $settingValue));
  309. }
  310. $models = Set::merge($models, array('__settings__' => array($setting => $settingValue)));
  311. } else if (!empty($name)) {
  312. if (!is_array($children) && $children != $key) {
  313. $children = array($children => array());
  314. }
  315. // Handle dot notation and in place list of fields
  316. if (strpos($name, '.') !== false) {
  317. $chain = explode('.', $name);
  318. $name = array_shift($chain);
  319. $children = array(join('.', $chain) => $children);
  320. if (isset($models[$name])) {
  321. $children = array_merge($children, $models[$name]);
  322. }
  323. }
  324. $fields = null;
  325. if (preg_match('/^(\w+)\(([^\)]+)\)$/i', $name, $matches)) {
  326. $name = $matches[1];
  327. $fields = preg_split('/,\s*/', $matches[2]);
  328. }
  329. if ($name != '*' && !isset($models[$name])) {
  330. $models[$name] = array();
  331. }
  332. // Do a processing of children and assign
  333. if ($name == '*') {
  334. $children = array_flip(array_keys($bindings));
  335. array_walk($children, create_function('&$item', '$item = array();'));
  336. $models = Set::merge($models, $children);
  337. } else if (isset($Model->$name) && is_object($Model->$name)) {
  338. if (!empty($fields)) {
  339. if (!isset($children['fields'])) {
  340. $children['fields'] = array();
  341. }
  342. $children['fields'] = array_merge($children['fields'], $fields);
  343. }
  344. $models[$name] = array_merge($models[$name], $this->__models($Model->$name, $children, $notices, $autoFields, true));
  345. } else if ($notices) {
  346. trigger_error(sprintf(__('%s.%s is not a valid binding', true), $Model->alias, $name), E_USER_NOTICE);
  347. }
  348. }
  349. }
  350. if (!$inner && $autoFields) {
  351. $models = $this->__fields($Model, $models);
  352. }
  353. return $models;
  354. }
  355. /**
  356. * Compute mandatory fields for fetching required bindings.
  357. *
  358. * @param object $Model Model to start from
  359. * @param array $models Bindings for this model
  360. * @param array $mandatory Include these mandatory fields
  361. * @param bool $inner Set to true if on inner call, false otherwise
  362. * @return array Modified bindings with mandatory fields included
  363. * @access private
  364. */
  365. function __fields(&$Model, $models, $mandatory = array(), $inner = false) {
  366. $bindings = $Model->getAssociated();
  367. $fields = (!empty($mandatory) ? $mandatory : array());
  368. foreach($models as $name => $data) {
  369. if ($name == '__settings__' || empty($bindings[$name])) {
  370. continue;
  371. }
  372. $mandatory = array();
  373. $relation = $bindings[$name];
  374. switch($relation) {
  375. case 'belongsTo':
  376. $mandatory[] = $Model->$name->alias . '.' . $Model->$name->primaryKey;
  377. $fields[] = $Model->alias . '.' . $Model->{$relation}[$name]['foreignKey'];
  378. break;
  379. case 'hasOne':
  380. case 'hasMany':
  381. $mandatory[] = $Model->$name->alias . '.' . $Model->{$relation}[$name]['foreignKey'];
  382. $fields[] = $Model->alias . '.' . $Model->primaryKey;
  383. break;
  384. case 'hasAndBelongsToMany':
  385. $mandatory[] = $Model->$name->alias . '.' . $Model->$name->primaryKey;
  386. $fields[] = $Model->alias . '.' . $Model->primaryKey;
  387. break;
  388. }
  389. if (is_object($Model->$name)) {
  390. $models[$name] = $this->__fields($Model->$name, $models[$name], $mandatory, true);
  391. }
  392. }
  393. if ((!$inner && !empty($fields)) || (!empty($fields) && isset($models['__settings__']) && isset($models['__settings__']['fields']))) {
  394. if (!$inner) {
  395. foreach($models as $name => $data) {
  396. if ($name != '__settings__' && $bindings[$name] == 'belongsTo' && !empty($data['__settings__']) && !empty($data['__settings__']['fields'])) {
  397. $innerFields = $data['__settings__']['fields'];
  398. foreach($innerFields as $index => $field) {
  399. if (strpos($field, '.') === false) {
  400. $innerFields[$index] = $Model->$name->alias . '.' . $field;
  401. }
  402. }
  403. $fields = array_merge($fields, $innerFields);
  404. }
  405. }
  406. }
  407. if (!isset($models['__settings__'])) {
  408. $models['__settings__'] = array();
  409. }
  410. if (!isset($models['__settings__']['fields'])) {
  411. $models['__settings__']['fields'] = array();
  412. }
  413. $models['__settings__']['fields'] = array_unique(array_merge($models['__settings__']['fields'], $fields));
  414. }
  415. return $models;
  416. }
  417. /**
  418. * Calculate the minimum recursivity required for fetching the bindings
  419. *
  420. * @param array $models Bindings
  421. * @param bool $inner Set to true if on inner call, false otherwise
  422. * @return int Recursivity level
  423. * @access private
  424. */
  425. function __recursivity($models, $inner = false) {
  426. if ($inner) {
  427. if (isset($models['__settings__'])) {
  428. unset($models['__settings__']);
  429. }
  430. foreach($models as $key => $value) {
  431. $models[$key] = $this->__recursivity($models[$key], true);
  432. }
  433. return $models;
  434. }
  435. return Set::countDim($this->__recursivity($models, true), true);
  436. }
  437. }
  438. ?>