ResetBehavior.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. <?php
  2. App::uses('ModelBehavior', 'Model');
  3. /**
  4. * Allows the model to reset all records as batch command.
  5. * This way any slugging, geocoding or other beforeValidate, beforeSave, ... callbacks
  6. * can be retriggered for them.
  7. *
  8. * By default it will not update the modified timestamp and will re-save id and displayName.
  9. * If you need more fields, you need to specify them manually.
  10. *
  11. * You can also disable validate callback or provide a conditions scope to match only a subset
  12. * of records.
  13. *
  14. * For performance and memory reasons the records will only be processed in loops (not all at once).
  15. * If you have time-sensitive data, you can modify the limit of records per loop as well as the
  16. * timeout in between each loop.
  17. * Remember to raise set_time_limit() if you do not run this via CLI.
  18. *
  19. * It is recommended to attach this behavior dynamically where needed:
  20. *
  21. * $this->Model->Behaviors->load('Tools.Reset', array(...));
  22. * $this->Model->resetRecords();
  23. *
  24. * If you want to provide a callback function/method, you can either use object methods or
  25. * static functions/methods:
  26. *
  27. * 'callback' => array($this, 'methodName')
  28. *
  29. * and
  30. *
  31. * public function methodName($data, &$fields) {}
  32. *
  33. * For tables with lots of records you might want to use a shell and the CLI to invoke the reset/update process.
  34. *
  35. * @author Mark Scherer
  36. * @license http://opensource.org/licenses/mit-license.php MIT
  37. * @version 1.0
  38. */
  39. class ResetBehavior extends ModelBehavior {
  40. protected $_defaultConfig = [
  41. 'limit' => 100, // batch of records per loop
  42. 'timeout' => null, // in seconds
  43. 'fields' => [], // if not displayField
  44. 'updateFields' => [], // if saved fields should be different from fields
  45. 'validate' => true, // trigger beforeValidate callback
  46. 'updateTimestamp' => false, // update modified/updated timestamp
  47. 'scope' => [], // optional conditions
  48. 'callback' => null,
  49. ];
  50. /**
  51. * Configure the behavior through the Model::actsAs property
  52. *
  53. * @param Model $Model
  54. * @param array $config
  55. */
  56. public function setup(Model $Model, $config = []) {
  57. $this->settings[$Model->alias] = $config + $this->_defaultConfig;
  58. }
  59. /**
  60. * Regenerate all records (including possible beforeValidate/beforeSave callbacks).
  61. *
  62. * @param Model $Model
  63. * @param array $conditions
  64. * @param int $recursive
  65. * @return int Modified records
  66. */
  67. public function resetRecords(Model $Model, $params = []) {
  68. $recursive = -1;
  69. extract($this->settings[$Model->alias]);
  70. $defaults = [
  71. 'page' => 1,
  72. 'limit' => $limit,
  73. 'fields' => [],
  74. 'order' => $Model->alias . '.' . $Model->primaryKey . ' ASC',
  75. 'conditions' => $scope,
  76. 'recursive' => $recursive,
  77. ];
  78. if (!empty($fields)) {
  79. if (!$Model->hasField($fields)) {
  80. throw new CakeException('Model does not have fields ' . print_r($fields, true));
  81. }
  82. $defaults['fields'] = array_merge([$Model->primaryKey], $fields);
  83. } else {
  84. $defaults['fields'] = [$Model->primaryKey];
  85. if ($Model->displayField !== $Model->primaryKey) {
  86. $defaults['fields'][] = $Model->displayField;
  87. }
  88. }
  89. if (!$updateTimestamp) {
  90. $fields = ['modified', 'updated'];
  91. foreach ($fields as $field) {
  92. if ($Model->schema($field)) {
  93. $defaults['fields'][] = $field;
  94. break;
  95. }
  96. }
  97. }
  98. $params += $defaults;
  99. $count = $Model->find('count', compact('conditions'));
  100. $max = ini_get('max_execution_time');
  101. if ($max) {
  102. set_time_limit(max($max, $count / $limit));
  103. }
  104. $modified = 0;
  105. while ($rows = $Model->find('all', $params)) {
  106. foreach ($rows as $row) {
  107. $Model->create();
  108. $fieldList = $params['fields'];
  109. if (!empty($updateFields)) {
  110. $fieldList = $updateFields;
  111. foreach ($defaults['fields'] as $field) {
  112. if (!in_array($field, $fieldList)) {
  113. $fieldList[] = $field;
  114. }
  115. }
  116. }
  117. if ($fieldList && !in_array($Model->primaryKey, $fieldList)) {
  118. $fieldList[] = $Model->primaryKey;
  119. }
  120. if ($callback) {
  121. if (is_callable($callback)) {
  122. $parameters = [&$row, &$fieldList];
  123. $row = call_user_func_array($callback, $parameters);
  124. } else {
  125. $row = $Model->{$callback}($row, $fieldList);
  126. }
  127. if (!$row) {
  128. continue;
  129. }
  130. }
  131. $res = $Model->save($row, compact('validate', 'fieldList'));
  132. if (!$res) {
  133. throw new CakeException(print_r($Model->validationErrors, true));
  134. }
  135. $modified++;
  136. }
  137. $params['page']++;
  138. if ($timeout) {
  139. sleep((int)$timeout);
  140. }
  141. }
  142. return $modified;
  143. }
  144. }