PageRenderTime 43ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/Croogo/Model/Behavior/CopyableBehavior.php

https://github.com/kareypowell/croogo
PHP | 415 lines | 224 code | 47 blank | 144 comment | 31 complexity | b87100693ecc70ae38ed1c4e826d522b MD5 | raw file
  1. <?php
  2. App::uses('Croogo', 'Croogo.Lib');
  3. App::uses('ModelBehavior', 'Model');
  4. /**
  5. * Copyable Behavior class file.
  6. *
  7. * Adds ability to copy a model record, including all hasMany and
  8. * hasAndBelongsToMany associations. Relies on Containable behavior, which
  9. * this behavior will attach on the fly as needed.
  10. *
  11. * HABTM relationships are just duplicated in the join table, while hasMany
  12. * and hasOne records are recursively copied as well.
  13. *
  14. * Usage is straightforward:
  15. * From model: $this->copy($id); // id = the id of the record to be copied
  16. * From container: $this->MyModel->copy($id);
  17. *
  18. * @category Behavior
  19. * @package Croogo.Croogo.Model.Behavior
  20. * @author Jamie Nay
  21. * @copyright Jamie Nay
  22. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  23. * @link http://github.com/jamienay/copyable_behavior
  24. * @link http://www.croogo.org
  25. */
  26. class CopyableBehavior extends ModelBehavior {
  27. /**
  28. * Behavior settings
  29. */
  30. public $settings = array();
  31. /**
  32. * Array of contained models.
  33. */
  34. public $contain = array();
  35. /**
  36. * The full results of Model::find() that are modified and saved
  37. * as a new copy.
  38. */
  39. public $record = array();
  40. /**
  41. * Default values for settings.
  42. *
  43. * - recursive: whether to copy hasMany and hasOne records
  44. * - habtm: whether to copy hasAndBelongsToMany associations
  45. * - stripFields: fields to strip during copy process
  46. * - ignore: aliases of any associations that should be ignored, using dot (.) notation.
  47. * will look in the $this->contain array.
  48. */
  49. protected $_defaults = array(
  50. 'recursive' => false,
  51. 'habtm' => false,
  52. 'autoFields' => array(
  53. 'title',
  54. 'slug',
  55. 'alias',
  56. ),
  57. 'stripFields' => array(
  58. 'id',
  59. 'created',
  60. 'modified',
  61. 'updated',
  62. 'lft',
  63. 'status',
  64. 'rght'
  65. ),
  66. 'ignore' => array(
  67. ),
  68. 'masterKey' => null
  69. );
  70. /**
  71. * Configuration method.
  72. *
  73. * @param object $Model Model object
  74. * @param array $settings Config array
  75. * @return boolean
  76. */
  77. public function setup(Model $Model, $settings = array()) {
  78. $this->settings[$Model->alias] = array_merge($this->_defaults, $settings);
  79. return true;
  80. }
  81. /**
  82. * Copy method.
  83. *
  84. * @param object $Model model object
  85. * @param mixed $id String or integer model ID
  86. * @return boolean
  87. */
  88. public function copy(Model $Model, $id) {
  89. $this->generateContain($Model);
  90. $this->record = $Model->find('first', array(
  91. 'conditions' => array(
  92. $Model->escapeField() => $id
  93. ),
  94. 'contain' => $this->contain
  95. ));
  96. if (empty($this->record)) {
  97. return false;
  98. }
  99. if (!$this->_convertData($Model)) {
  100. return false;
  101. }
  102. $result = false;
  103. try {
  104. $result = $this->_copyRecord($Model);
  105. } catch (PDOException $e) {
  106. $this->log('Error executing _copyRecord: ' . $e->getMessage());
  107. }
  108. return $result;
  109. }
  110. /**
  111. * Wrapper method that combines the results of _recursiveChildContain()
  112. * with the models' HABTM associations.
  113. *
  114. * @param object $Model Model object
  115. * @return array
  116. */
  117. public function generateContain(Model $Model) {
  118. if (!$this->_verifyContainable($Model)) {
  119. return false;
  120. }
  121. $this->contain = array_merge($this->_recursiveChildContain($Model), array_keys($Model->hasAndBelongsToMany));
  122. $this->_removeIgnored($Model);
  123. return $this->contain;
  124. }
  125. /**
  126. * Removes any ignored associations, as defined in the model settings, from
  127. * the $this->contain array.
  128. *
  129. * @param object $Model Model object
  130. * @return boolean
  131. */
  132. protected function _removeIgnored(Model $Model) {
  133. if (!$this->settings[$Model->alias]['ignore']) {
  134. return true;
  135. }
  136. $ignore = array_unique($this->settings[$Model->alias]['ignore']);
  137. foreach ($ignore as $path) {
  138. if (Hash::check($this->contain, $path)) {
  139. $this->contain = Hash::remove($this->contain, $path);
  140. }
  141. }
  142. return true;
  143. }
  144. /**
  145. * Strips primary keys and other unwanted fields
  146. * from hasOne and hasMany records.
  147. *
  148. * @param object $Model model object
  149. * @param array $record
  150. * @return array $record
  151. */
  152. protected function _convertChildren(Model $Model, $record) {
  153. $children = array_merge($Model->hasMany, $Model->hasOne);
  154. foreach ($children as $key => $val) {
  155. if (!isset($record[$key])) {
  156. continue;
  157. }
  158. if (empty($record[$key])) {
  159. unset($record[$key]);
  160. continue;
  161. }
  162. if (isset($record[$key][0])) {
  163. foreach ($record[$key] as $innerKey => $innerVal) {
  164. $record[$key][$innerKey] = $this->_stripFields($Model, $innerVal);
  165. if (array_key_exists($val['foreignKey'], $innerVal)) {
  166. unset($record[$key][$innerKey][$val['foreignKey']]);
  167. }
  168. $record[$key][$innerKey] = $this->_convertChildren($Model->{$key}, $record[$key][$innerKey]);
  169. }
  170. } else {
  171. $record[$key] = $this->_stripFields($Model, $record[$key]);
  172. if (isset($record[$key][$val['foreignKey']])) {
  173. unset($record[$key][$val['foreignKey']]);
  174. }
  175. $record[$key] = $this->_convertChildren($Model->{$key}, $record[$key]);
  176. }
  177. }
  178. return $record;
  179. }
  180. /**
  181. * Strips primary and parent foreign keys (where applicable)
  182. * from $this->record in preparation for saving.
  183. *
  184. * When `autoFields` is set, it will iterate listed fields and append
  185. * ' (copy)' for titles or '-copy' for slug/alias fields.
  186. *
  187. * Plugins can also perform custom/additional data conversion by listening
  188. * on `Behavior.Copyable.convertData`
  189. *
  190. * @param object $Model Model object
  191. * @return array $this->record
  192. */
  193. protected function _convertData(Model $Model) {
  194. $this->record[$Model->alias] = $this->_stripFields($Model, $this->record[$Model->alias]);
  195. $this->record = $this->_convertHabtm($Model, $this->record);
  196. $this->record = $this->_convertChildren($Model, $this->record);
  197. if (!empty($this->settings[$Model->alias]['autoFields'])) {
  198. $autoFields = (array)$this->settings[$Model->alias]['autoFields'];
  199. $slugFields = array('slug', 'alias');
  200. foreach ($autoFields as $field) {
  201. if (!$Model->hasField($field)) {
  202. continue;
  203. }
  204. if (in_array($field, $slugFields)) {
  205. $this->record[$Model->alias][$field] .= '-copy';
  206. } else {
  207. $this->record[$Model->alias][$field] .= ' (copy)';
  208. }
  209. }
  210. }
  211. $eventName = 'Behavior.Copyable.convertData';
  212. $event = Croogo::dispatchEvent($eventName, $Model, array(
  213. 'record' => $this->record,
  214. ));
  215. $this->record = $event->data['record'];
  216. return $this->record;
  217. }
  218. /**
  219. * Loops through any HABTM results in $this->record and plucks out
  220. * the join table info, stripping out the join table primary
  221. * key and the primary key of $Model. This is done instead of
  222. * a simple collection of IDs of the associated records, since
  223. * HABTM join tables may contain extra information (sorting
  224. * order, etc).
  225. *
  226. * @param Model $Model Model object
  227. * @param array $record
  228. * @return array modified $record
  229. */
  230. protected function _convertHabtm(Model $Model, $record) {
  231. if (!$this->settings[$Model->alias]['habtm']) {
  232. return $record;
  233. }
  234. foreach ($Model->hasAndBelongsToMany as $key => $val) {
  235. $className = pluginSplit($val['className']);
  236. $className = $className[1];
  237. if (!isset($record[$className]) || empty($record[$className])) {
  238. continue;
  239. }
  240. $joinInfo = Hash::extract($record[$className], '{n}.' . $val['with']);
  241. if (empty($joinInfo)) {
  242. continue;
  243. }
  244. foreach ($joinInfo as $joinKey => $joinVal) {
  245. $joinInfo[$joinKey] = $this->_stripFields($Model, $joinVal);
  246. if (array_key_exists($val['foreignKey'], $joinVal)) {
  247. unset($joinInfo[$joinKey][$val['foreignKey']]);
  248. }
  249. }
  250. $record[$className] = $joinInfo;
  251. }
  252. return $record;
  253. }
  254. /**
  255. * Performs the actual creation and save.
  256. *
  257. * @param object $Model Model object
  258. * @return mixed
  259. */
  260. protected function _copyRecord(Model $Model) {
  261. $Model->create();
  262. $saved = $Model->saveAll($this->record, array(
  263. 'validate' => false,
  264. 'deep' => true
  265. ));
  266. if ($this->settings[$Model->alias]['masterKey']) {
  267. $record = $this->_updateMasterKey($Model);
  268. $Model->saveAll($record, array(
  269. 'validate' => false,
  270. 'deep' => true
  271. ));
  272. }
  273. return $saved;
  274. }
  275. /**
  276. * Runs through to update the master key for deep copying.
  277. *
  278. * @param Model $Model
  279. * @return array
  280. */
  281. protected function _updateMasterKey(Model $Model) {
  282. $record = $Model->find('first', array(
  283. 'conditions' => array(
  284. $Model->escapeField() => $Model->id
  285. ),
  286. 'contain' => $this->contain
  287. ));
  288. $record = $this->_masterKeyLoop($Model, $record, $Model->id);
  289. return $record;
  290. }
  291. /**
  292. * Called by _updateMasterKey as part of the copying process for deep recursion.
  293. *
  294. * @param Model $Model
  295. * @param array $record
  296. * @param integer $id
  297. * @return array
  298. */
  299. protected function _masterKeyLoop(Model $Model, $record, $id) {
  300. foreach ($record as $key => $val) {
  301. if (is_array($val)) {
  302. if (empty($val)) {
  303. unset($record[$key]);
  304. }
  305. foreach ($val as $innerKey => $innerVal) {
  306. if (is_array($innerVal)) {
  307. $record[$key][$innerKey] = $this->_masterKeyLoop($Model, $innerVal, $id);
  308. }
  309. }
  310. }
  311. if (!isset($val[$this->settings[$Model->alias]['masterKey']])) {
  312. continue;
  313. }
  314. $record[$this->settings[$Model->alias]['masterKey']] = $id;
  315. }
  316. return $record;
  317. }
  318. /**
  319. * Generates a contain array for Containable behavior by
  320. * recursively looping through $Model->hasMany and
  321. * $Model->hasOne associations.
  322. *
  323. * @param object $Model Model object
  324. * @return array
  325. */
  326. protected function _recursiveChildContain(Model $Model) {
  327. $contain = array();
  328. if (!isset($this->settings[$Model->alias]) || !$this->settings[$Model->alias]['recursive']) {
  329. return $contain;
  330. }
  331. $children = array_merge(array_keys($Model->hasMany), array_keys($Model->hasOne));
  332. foreach ($children as $child) {
  333. if ($Model->alias == $child) {
  334. continue;
  335. }
  336. $contain[$child] = $this->_recursiveChildContain($Model->{$child});
  337. }
  338. return $contain;
  339. }
  340. /**
  341. * Strips unwanted fields from $record, taken from
  342. * the 'stripFields' setting.
  343. *
  344. * @param object $Model Model object
  345. * @param array $record
  346. * @return array
  347. */
  348. protected function _stripFields(Model $Model, $record) {
  349. foreach ($this->settings[$Model->alias]['stripFields'] as $field) {
  350. if (array_key_exists($field, $record)) {
  351. unset($record[$field]);
  352. }
  353. }
  354. return $record;
  355. }
  356. /**
  357. * Attaches Containable if it's not already attached.
  358. *
  359. * @param object $Model Model object
  360. * @return boolean
  361. */
  362. protected function _verifyContainable(Model $Model) {
  363. if (!$Model->Behaviors->attached('Containable')) {
  364. return $Model->Behaviors->attach('Containable');
  365. }
  366. return true;
  367. }
  368. }