ResetBehavior.php 4.8 KB

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