SoftDeleteBehavior.php 9.4 KB

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