1
|
|
|
<?php namespace Tequilarapido\Cli\Commands; |
2
|
|
|
|
3
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
4
|
|
|
use Symfony\Component\Console\Input\InputOption; |
5
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
6
|
|
|
use Tequilarapido\Cli\Commands\Base\AbstractDatabaseCommand; |
7
|
|
|
use Tequilarapido\Database\Column; |
8
|
|
|
use Tequilarapido\Helpers\ShellHelper; |
9
|
|
|
use Tequilarapido\PHPSerialized\SearchReplace; |
10
|
|
|
|
11
|
|
|
class DatabaseReplace extends AbstractDatabaseCommand |
12
|
|
|
{ |
13
|
|
|
const OPTION_USE_TRANSACTIONS = 'use-transactions'; |
14
|
|
|
|
15
|
|
|
protected $useTransactions = false; |
16
|
|
|
|
17
|
|
View Code Duplication |
protected function configure() |
|
|
|
|
18
|
|
|
{ |
19
|
|
|
parent::configure(); |
20
|
|
|
|
21
|
|
|
$description = ''; |
22
|
|
|
$description .= 'Search and replace string in database (even in serialized objects). ' . PHP_EOL; |
23
|
|
|
$description .= 'Can be used to switch domain for a WordPress application, when moving to different environement. ' . PHP_EOL; |
24
|
|
|
$description .= ' '; |
25
|
|
|
|
26
|
|
|
$this |
27
|
|
|
->setName('db:replace') |
28
|
|
|
->setDescription($description) |
29
|
|
|
->addOption( |
30
|
|
|
static::OPTION_USE_TRANSACTIONS, |
31
|
|
|
null, |
32
|
|
|
InputOption::VALUE_NONE, |
33
|
|
|
'If specified, SQL update operations will be grouped into transactions.' |
34
|
|
|
); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
38
|
|
|
{ |
39
|
|
|
parent::execute($input, $output); |
40
|
|
|
$this->useTransactions = $input->getOption(static::OPTION_USE_TRANSACTIONS); |
41
|
|
|
$replacements = $this->config->getReplacements(); |
42
|
|
|
$excludeTables = $this->config->getExcludeTables(); |
43
|
|
|
|
44
|
|
|
// Operations config |
45
|
|
|
if (is_null($replacements)) { |
46
|
|
|
$this->output->warn('There is nothing to replace according to configuration.'); |
|
|
|
|
47
|
|
|
return; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
// Setup connection |
51
|
|
|
$this->setup(); |
52
|
|
|
|
53
|
|
|
// Process |
54
|
|
|
$textColumns = $this->analyseDatabase($excludeTables); |
55
|
|
|
$progress = $this->startProgress(count($textColumns)); |
56
|
|
|
$queriesCount = $this->processDatabase($textColumns, $progress, $replacements); |
57
|
|
|
$progress->finish(); |
58
|
|
|
|
59
|
|
|
if ($this->useTransactions) { |
60
|
|
|
$this->output->info("Queries were executed using transactions.\n"); |
|
|
|
|
61
|
|
|
} |
62
|
|
|
$this->output->info("Total executed queries : $queriesCount"); |
|
|
|
|
63
|
|
|
|
64
|
|
|
// Mail |
65
|
|
|
$this->sendNotification(); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
protected function startProgress($total) |
69
|
|
|
{ |
70
|
|
|
$progress = $this->getHelperSet()->get('progress'); |
71
|
|
|
$progress->start($this->output, $total); |
72
|
|
|
return $progress; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
protected function sendNotification() |
76
|
|
|
{ |
77
|
|
|
$mail = array( |
78
|
|
|
'subject' => 'Done.', |
79
|
|
|
'body' => '' |
80
|
|
|
); |
81
|
|
|
|
82
|
|
|
$this->notify($mail); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @param $excludeTables |
87
|
|
|
* @return array |
88
|
|
|
*/ |
89
|
|
|
protected function analyseDatabase($excludeTables) |
90
|
|
|
{ |
91
|
|
|
$this->output->info('Analysing database : looking for text columns ...'); |
|
|
|
|
92
|
|
|
$column = new Column(); |
93
|
|
|
$textColumns = $column->scanTextColumns($this->databaseName, $excludeTables); |
94
|
|
|
return $textColumns; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
protected function setup() |
98
|
|
|
{ |
99
|
|
|
parent::setup(); |
100
|
|
|
|
101
|
|
|
// Need memory on big databases |
102
|
|
|
if ($this->useTransactions) { |
103
|
|
|
$this->iAmHungry(); |
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @param $textColumns |
109
|
|
|
* @param $progress |
110
|
|
|
* @param $replacements |
111
|
|
|
* @return int |
112
|
|
|
*/ |
113
|
|
|
protected function processDatabase($textColumns, $progress, $replacements) |
114
|
|
|
{ |
115
|
|
|
$queriesCount = 0; |
116
|
|
|
foreach ($textColumns as $tableName => $tableInfos) { |
117
|
|
|
$progress->advance(); |
118
|
|
|
$this->output->writeln(" : Processing $tableName "); |
119
|
|
|
$tableCount = $this->processTable($tableName, $tableInfos, $replacements); |
120
|
|
|
$queriesCount += $tableCount; |
121
|
|
|
$this->output->writeln(" -> $tableCount queries "); |
122
|
|
|
} |
123
|
|
|
return $queriesCount; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
protected function processTable($tableName, $tableInfos, $replacements) |
127
|
|
|
{ |
128
|
|
|
$queriesCount = 0; |
129
|
|
|
|
130
|
|
|
// Is there columns ? |
131
|
|
|
if (empty($tableInfos['columns'])) { |
132
|
|
|
return; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
// Iterates in each column and looks for the old string |
136
|
|
|
foreach ($tableInfos['columns'] as $field_name) { |
137
|
|
|
|
138
|
|
|
$this->beginTransaction(); |
139
|
|
|
|
140
|
|
|
foreach ($replacements as $replacement) { |
141
|
|
|
|
142
|
|
|
// Search |
143
|
|
|
$searchQuery = $this->searchQuery($tableName, $tableInfos, $field_name, $replacement); |
144
|
|
|
$search_results = $this->db->select($searchQuery); |
145
|
|
|
if (empty($search_results)) { |
146
|
|
|
continue; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
// Loop through result and search/replace |
150
|
|
|
foreach ($search_results as $found_data) { |
151
|
|
|
|
152
|
|
|
// Pk check |
153
|
|
|
if (isset($found_data['_id'])) { |
154
|
|
|
$id = $found_data['_id']; |
155
|
|
|
unset($found_data['_id']); |
156
|
|
|
} |
157
|
|
|
$found_data = current($found_data); |
158
|
|
|
|
159
|
|
|
// Try to replace string |
160
|
|
|
try { |
161
|
|
|
$sr = new SearchReplace; |
162
|
|
|
$edited_data = $sr->run($replacement->from, $replacement->to, $found_data); |
163
|
|
|
} catch |
164
|
|
|
(\Exception $e) { |
165
|
|
|
continue; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
// Update entry / value |
169
|
|
|
if (isset($id)) { |
170
|
|
|
$this->db |
171
|
|
|
->table($tableName) |
172
|
|
|
->where($tableInfos['pk'], '=', $id) |
173
|
|
|
->update(array($field_name => $edited_data)); |
174
|
|
|
|
175
|
|
|
} else { |
176
|
|
|
$this->db |
177
|
|
|
->table($tableName) |
178
|
|
|
->where($field_name, '=', $found_data) |
179
|
|
|
->update(array($field_name => $edited_data)); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
$queriesCount++; |
183
|
|
|
|
184
|
|
|
// display progress |
185
|
|
|
ShellHelper::progress($this->output); |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
// Commit updates if using transactions |
190
|
|
|
$this->endTransaction(); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// End shell progress |
194
|
|
|
ShellHelper::progressEnd($this->output); |
195
|
|
|
return $queriesCount; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
View Code Duplication |
protected function searchQuery($tableName, $tableInfos, $field_name, $replacement) |
|
|
|
|
199
|
|
|
{ |
200
|
|
|
// table with primary key ? |
201
|
|
|
if (empty($tableInfos['pk'])) { |
202
|
|
|
return sprintf('SELECT `%s` FROM %s WHERE `%s` LIKE "%%%s%%"', $field_name, $tableName, $field_name, $replacement->from); |
203
|
|
|
} else { |
204
|
|
|
return sprintf('SELECT `%s`, ' . $tableInfos['pk'] . ' AS _id FROM %s WHERE `%s` LIKE "%%%s%%"', $field_name, $tableName, $field_name, $replacement->from); |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
private function beginTransaction() |
209
|
|
|
{ |
210
|
|
|
if ($this->useTransactions) { |
211
|
|
|
$this->db->beginTransaction(); |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
private function endTransaction() |
216
|
|
|
{ |
217
|
|
|
if ($this->useTransactions) { |
218
|
|
|
$this->db->commit(); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
} |
223
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.