SoftDeleteBehavior.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. <?php
  2. /**
  3. * Copyright 2007-2010, Cake Development Corporation (http://cakedc.com)
  4. *
  5. * Licensed under The MIT License
  6. * Redistributions of files must retain the above copyright notice.
  7. *
  8. * @copyright Copyright 2007-2010, Cake Development Corporation (http://cakedc.com)
  9. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  10. */
  11. App::uses('ModelBehavior', 'Model');
  12. /**
  13. * Utils Plugin
  14. *
  15. * Utils Soft Delete Behavior
  16. *
  17. * @package utils
  18. * @subpackage utils.models.behaviors
  19. */
  20. class SoftDeleteBehavior extends ModelBehavior {
  21. /**
  22. * Default settings
  23. *
  24. * @var array $default
  25. */
  26. public $default = array('deleted' => 'deleted_date');
  27. /**
  28. * Holds activity flags for models
  29. *
  30. * @var array $runtime
  31. */
  32. public $runtime = array();
  33. /**
  34. * Setup callback
  35. *
  36. * @param object $model
  37. * @param array $settings
  38. */
  39. public function setup(Model $model, $settings = array()) {
  40. if (empty($settings)) {
  41. $settings = $this->default;
  42. } elseif (!is_array($settings)) {
  43. $settings = array($settings);
  44. }
  45. $error = 'SoftDeleteBehavior::setup(): model ' . $model->alias . ' has no field ';
  46. $fields = $this->_normalizeFields($model, $settings);
  47. foreach ($fields as $flag => $date) {
  48. if ($model->hasField($flag)) {
  49. if ($date && !$model->hasField($date)) {
  50. trigger_error($error . $date, E_USER_NOTICE);
  51. return;
  52. }
  53. continue;
  54. }
  55. trigger_error($error . $flag, E_USER_NOTICE);
  56. return;
  57. }
  58. $this->settings[$model->alias] = $fields;
  59. $this->softDelete($model, true);
  60. }
  61. /**
  62. * Before find callback
  63. *
  64. * @param object $model
  65. * @param array $query
  66. * @return array
  67. */
  68. public function beforeFind(Model $model, $query) {
  69. $runtime = $this->runtime[$model->alias];
  70. if ($runtime) {
  71. if (!is_array($query['conditions'])) {
  72. $query['conditions'] = array();
  73. }
  74. $conditions = array_filter(array_keys($query['conditions']));
  75. $fields = $this->_normalizeFields($model);
  76. foreach ($fields as $flag => $date) {
  77. if (true === $runtime || $flag === $runtime) {
  78. if (!in_array($flag, $conditions) && !in_array($model->name . '.' . $flag, $conditions)) {
  79. $query['conditions'][$model->alias . '.' . $flag] = false;
  80. }
  81. if ($flag === $runtime) {
  82. break;
  83. }
  84. }
  85. }
  86. return $query;
  87. }
  88. }
  89. /**
  90. * Before delete callback
  91. *
  92. * @param object $model
  93. * @param array $query
  94. * @return boolean
  95. */
  96. public function beforeDelete(Model $model, $cascade = true) {
  97. $runtime = $this->runtime[$model->alias];
  98. if ($runtime) {
  99. $this->delete($model, $model->id);
  100. return false;
  101. }
  102. return true;
  103. }
  104. /**
  105. * Mark record as deleted
  106. *
  107. * @param object $model
  108. * @param integer $id
  109. * @return boolean
  110. */
  111. public function delete(Model $model, $id) {
  112. $runtime = $this->runtime[$model->alias];
  113. $data = array();
  114. $fields = $this->_normalizeFields($model);
  115. foreach ($fields as $flag => $date) {
  116. if (true === $runtime || $flag === $runtime) {
  117. $data[$flag] = true;
  118. if ($date) {
  119. $data[$date] = date('Y-m-d H:i:s');
  120. }
  121. if ($flag === $runtime) {
  122. break;
  123. }
  124. }
  125. }
  126. $model->create();
  127. $model->set($model->primaryKey, $id);
  128. return $model->save(array($model->alias => $data), false, array_keys($data));
  129. }
  130. /**
  131. * Mark record as not deleted
  132. *
  133. * @param object $model
  134. * @param integer $id
  135. * @return boolean
  136. */
  137. public function undelete(Model $model, $id) {
  138. $runtime = $this->runtime[$model->alias];
  139. $this->softDelete($model, false);
  140. $data = array();
  141. $fields = $this->_normalizeFields($model);
  142. foreach ($fields as $flag => $date) {
  143. if (true === $runtime || $flag === $runtime) {
  144. $data[$flag] = false;
  145. if ($date) {
  146. $data[$date] = null;
  147. }
  148. if ($flag === $runtime) {
  149. break;
  150. }
  151. }
  152. }
  153. $model->create();
  154. $model->set($model->primaryKey, $id);
  155. $result = $model->save(array($model->alias => $data), false, array_keys($data));
  156. $this->softDelete($model, $runtime);
  157. return $result;
  158. }
  159. /**
  160. * Enable/disable SoftDelete functionality
  161. *
  162. * Usage from model:
  163. * $this->softDelete(false); deactivate this behavior for model
  164. * $this->softDelete('field_two'); enabled only for this flag field
  165. * $this->softDelete(true); enable again for all flag fields
  166. * $config = $this->softDelete(null); for obtaining current setting
  167. *
  168. * @param object $model
  169. * @param mixed $active
  170. * @return mixed if $active is null, then current setting/null, or boolean if runtime setting for model was changed
  171. */
  172. public function softDelete(Model $model, $active) {
  173. if (is_null($active)) {
  174. return !empty($this->runtime[$model->alias]) ? $this->runtime[$model->alias] : null;
  175. }
  176. $result = !isset($this->runtime[$model->alias]) || $this->runtime[$model->alias] !== $active;
  177. $this->runtime[$model->alias] = $active;
  178. $this->_softDeleteAssociations($model, $active);
  179. return $result;
  180. }
  181. /**
  182. * Returns number of outdated softdeleted records prepared for purge
  183. *
  184. * @param object $model
  185. * @param mixed $expiration anything parseable by strtotime(), by default '-90 days'
  186. * @return integer
  187. */
  188. public function purgeDeletedCount(Model $model, $expiration = '-90 days') {
  189. $this->softDelete($model, false);
  190. return $model->find('count', array('conditions' => $this->_purgeDeletedConditions($model, $expiration), 'recursive' => -1));
  191. }
  192. /**
  193. * Purge table
  194. *
  195. * @param object $model
  196. * @param mixed $expiration anything parseable by strtotime(), by default '-90 days'
  197. * @return boolean if there were some outdated records
  198. */
  199. public function purgeDeleted(Model $model, $expiration = '-90 days') {
  200. $this->softDelete($model, false);
  201. $records = $model->find('all', array(
  202. 'conditions' => $this->_purgeDeletedConditions($model, $expiration),
  203. 'fields' => array($model->primaryKey),
  204. 'recursive' => -1));
  205. if ($records) {
  206. foreach ($records as $record) {
  207. $model->delete($record[$model->alias][$model->primaryKey]);
  208. }
  209. return true;
  210. }
  211. return false;
  212. }
  213. /**
  214. * Returns conditions for finding outdated records
  215. *
  216. * @param object $model
  217. * @param mixed $expiration anything parseable by strtotime(), by default '-90 days'
  218. * @return array
  219. */
  220. protected function _purgeDeletedConditions(Model $model, $expiration = '-90 days') {
  221. $purgeDate = date('Y-m-d H:i:s', strtotime($expiration));
  222. $conditions = array();
  223. foreach ($this->settings[$model->alias] as $flag => $date) {
  224. $conditions[$model->alias . '.' . $flag] = true;
  225. if ($date) {
  226. $conditions[$model->alias . '.' . $date . ' <'] = $purgeDate;
  227. }
  228. }
  229. return $conditions;
  230. }
  231. /**
  232. * Return normalized field array
  233. *
  234. * @param object $model
  235. * @param array $settings
  236. * @return array
  237. */
  238. protected function _normalizeFields(Model $model, $settings = array()) {
  239. if (empty($settings)) {
  240. $settings = @$this->settings[$model->alias];
  241. }
  242. $result = array();
  243. foreach ($settings as $flag => $date) {
  244. if (is_numeric($flag)) {
  245. $flag = $date;
  246. $date = false;
  247. }
  248. $result[$flag] = $date;
  249. }
  250. return $result;
  251. }
  252. /**
  253. * Modifies conditions of hasOne and hasMany associations
  254. *
  255. * If multiple delete flags are configured for model, then $active=true doesn't
  256. * do anything - you have to alter conditions in association definition
  257. *
  258. * @param object $model
  259. * @param mixed $active
  260. */
  261. protected function _softDeleteAssociations(Model $model, $active) {
  262. if (empty($model->belongsTo)) {
  263. return;
  264. }
  265. $fields = array_keys($this->_normalizeFields($model));
  266. $parentModels = array_keys($model->belongsTo);
  267. foreach ($parentModels as $parentModel) {
  268. foreach (array('hasOne', 'hasMany') as $assocType) {
  269. if (empty($model->{$parentModel}->{$assocType})) {
  270. continue;
  271. }
  272. foreach ($model->{$parentModel}->{$assocType} as $assoc => $assocConfig) {
  273. $modelName = !empty($assocConfig['className']) ? $assocConfig['className'] : $assoc;
  274. if ($model->alias != $modelName) {
  275. continue;
  276. }
  277. $conditions = $model->{$parentModel}->{$assocType}[$assoc]['conditions'];
  278. if (!is_array($conditions)) {
  279. $model->{$parentModel}->{$assocType}[$assoc]['conditions'] = array();
  280. }
  281. $multiFields = 1 < count($fields);
  282. foreach ($fields as $field) {
  283. if ($active) {
  284. if (!isset($conditions[$field]) && !isset($conditions[$assoc . '.' . $field])) {
  285. if (is_string($active)) {
  286. if ($field == $active) {
  287. $conditions[$assoc . '.' . $field] = false;
  288. } elseif (isset($conditions[$assoc . '.' . $field])) {
  289. unset($conditions[$assoc . '.' . $field]);
  290. }
  291. } elseif (!$multiFields) {
  292. $conditions[$assoc . '.' . $field] = false;
  293. }
  294. }
  295. } elseif (isset($conditions[$assoc . '.' . $field])) {
  296. unset($conditions[$assoc . '.' . $field]);
  297. }
  298. }
  299. }
  300. }
  301. }
  302. }
  303. }