ResetBehavior.php 4.9 KB

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