SoftDeleteBehavior.php 9.2 KB

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