1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Arrilot\BitrixMigrations; |
4
|
|
|
|
5
|
|
|
use Arrilot\BitrixIblockHelper\HLBlock; |
6
|
|
|
use Arrilot\BitrixIblockHelper\IblockId; |
7
|
|
|
use Arrilot\BitrixMigrations\Constructors\FieldConstructor; |
8
|
|
|
use Arrilot\BitrixMigrations\Interfaces\DatabaseStorageInterface; |
9
|
|
|
use Arrilot\BitrixMigrations\Interfaces\FileStorageInterface; |
10
|
|
|
use Arrilot\BitrixMigrations\Interfaces\MigrationInterface; |
11
|
|
|
use Arrilot\BitrixMigrations\Storages\BitrixDatabaseStorage; |
12
|
|
|
use Arrilot\BitrixMigrations\Storages\FileStorage; |
13
|
|
|
use Bitrix\Main\Application; |
14
|
|
|
use Exception; |
15
|
|
|
|
16
|
|
|
class Migrator |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* Migrator configuration array. |
20
|
|
|
* |
21
|
|
|
* @var array |
22
|
|
|
*/ |
23
|
|
|
protected $config; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Directory to store m. |
27
|
|
|
* |
28
|
|
|
* @var string |
29
|
|
|
*/ |
30
|
|
|
protected $dir; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Directory to store archive m. |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
protected $dir_archive; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* User transaction default. |
41
|
|
|
* |
42
|
|
|
* @var bool |
43
|
|
|
*/ |
44
|
|
|
protected $use_transaction; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Files interactions. |
48
|
|
|
* |
49
|
|
|
* @var FileStorageInterface |
50
|
|
|
*/ |
51
|
|
|
protected $files; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Interface that gives us access to the database. |
55
|
|
|
* |
56
|
|
|
* @var DatabaseStorageInterface |
57
|
|
|
*/ |
58
|
|
|
protected $database; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* TemplatesCollection instance. |
62
|
|
|
* |
63
|
|
|
* @var TemplatesCollection |
64
|
|
|
*/ |
65
|
|
|
protected $templates; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Constructor. |
69
|
|
|
* |
70
|
|
|
* @param array $config |
71
|
|
|
* @param TemplatesCollection $templates |
72
|
|
|
* @param DatabaseStorageInterface $database |
73
|
|
|
* @param FileStorageInterface $files |
74
|
|
|
*/ |
75
|
|
|
public function __construct($config, TemplatesCollection $templates, DatabaseStorageInterface $database = null, FileStorageInterface $files = null) |
76
|
|
|
{ |
77
|
|
|
$this->config = $config; |
78
|
|
|
$this->dir = $config['dir']; |
79
|
|
|
$this->dir_archive = isset($config['dir_archive']) ? $config['dir_archive'] : 'archive'; |
80
|
|
|
$this->use_transaction = isset($config['use_transaction']) ? $config['use_transaction'] : false; |
81
|
|
|
|
82
|
|
|
if (isset($config['default_fields']) && is_array($config['default_fields'])) { |
83
|
|
|
foreach ($config['default_fields'] as $class => $default_fields) { |
84
|
|
|
FieldConstructor::$defaultFields[$class] = $default_fields; |
85
|
|
|
} |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
$this->templates = $templates; |
89
|
|
|
$this->database = $database ?: new BitrixDatabaseStorage($config['table']); |
90
|
|
|
$this->files = $files ?: new FileStorage(); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Create migration file. |
95
|
|
|
* |
96
|
|
|
* @param string $name - migration name |
97
|
|
|
* @param string $templateName |
98
|
|
|
* @param array $replace - array of placeholders that should be replaced with a given values. |
99
|
|
|
* @param string $subDir |
100
|
|
|
* |
101
|
|
|
* @return string |
102
|
|
|
*/ |
103
|
|
|
public function createMigration($name, $templateName, array $replace = [], $subDir = '') |
104
|
|
|
{ |
105
|
|
|
$targetDir = $this->dir; |
106
|
|
|
$subDir = trim(str_replace('\\', '/', $subDir), '/'); |
107
|
|
|
if ($subDir) { |
108
|
|
|
$targetDir .= '/' . $subDir; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
$this->files->createDirIfItDoesNotExist($targetDir); |
112
|
|
|
|
113
|
|
|
$fileName = $this->constructFileName($name); |
114
|
|
|
$className = $this->getMigrationClassNameByFileName($fileName); |
115
|
|
|
$templateName = $this->templates->selectTemplate($templateName); |
116
|
|
|
|
117
|
|
|
$template = $this->files->getContent($this->templates->getTemplatePath($templateName)); |
118
|
|
|
$template = $this->replacePlaceholdersInTemplate($template, array_merge($replace, ['className' => $className])); |
119
|
|
|
|
120
|
|
|
$this->files->putContent($targetDir.'/'.$fileName.'.php', $template); |
121
|
|
|
|
122
|
|
|
return $fileName; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Run all migrations that were not run before. |
127
|
|
|
*/ |
128
|
|
|
public function runMigrations() |
129
|
|
|
{ |
130
|
|
|
$migrations = $this->getMigrationsToRun(); |
131
|
|
|
$ran = []; |
132
|
|
|
|
133
|
|
|
if (empty($migrations)) { |
134
|
|
|
return $ran; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
foreach ($migrations as $migration) { |
138
|
|
|
$this->runMigration($migration); |
139
|
|
|
$ran[] = $migration; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
return $ran; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Run a given migration. |
147
|
|
|
* |
148
|
|
|
* @param string $file |
149
|
|
|
* |
150
|
|
|
* @throws Exception |
151
|
|
|
* |
152
|
|
|
* @return string |
153
|
|
|
*/ |
154
|
|
View Code Duplication |
public function runMigration($file) |
|
|
|
|
155
|
|
|
{ |
156
|
|
|
$migration = $this->getMigrationObjectByFileName($file); |
157
|
|
|
|
158
|
|
|
$this->disableBitrixIblockHelperCache(); |
159
|
|
|
|
160
|
|
|
$this->checkTransactionAndRun($migration, function () use ($migration, $file) { |
161
|
|
|
if ($migration->up() === false) { |
162
|
|
|
throw new Exception("Migration up from {$file}.php returned false"); |
163
|
|
|
} |
164
|
|
|
}); |
165
|
|
|
|
166
|
|
|
$this->logSuccessfulMigration($file); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Log successful migration. |
171
|
|
|
* |
172
|
|
|
* @param string $migration |
173
|
|
|
* |
174
|
|
|
* @return void |
175
|
|
|
*/ |
176
|
|
|
public function logSuccessfulMigration($migration) |
177
|
|
|
{ |
178
|
|
|
$this->database->logSuccessfulMigration($migration); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Get ran migrations. |
183
|
|
|
* |
184
|
|
|
* @return array |
185
|
|
|
*/ |
186
|
|
|
public function getRanMigrations() |
187
|
|
|
{ |
188
|
|
|
return $this->database->getRanMigrations(); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* Get all migrations. |
193
|
|
|
* |
194
|
|
|
* @return array |
195
|
|
|
*/ |
196
|
|
|
public function getAllMigrations() |
197
|
|
|
{ |
198
|
|
|
return $this->files->getMigrationFiles($this->dir); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Determine whether migration file for migration exists. |
203
|
|
|
* |
204
|
|
|
* @param string $migration |
205
|
|
|
* |
206
|
|
|
* @return bool |
207
|
|
|
*/ |
208
|
|
|
public function doesMigrationFileExist($migration) |
209
|
|
|
{ |
210
|
|
|
return $this->files->exists($this->getMigrationFilePath($migration)); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Rollback a given migration. |
215
|
|
|
* |
216
|
|
|
* @param string $file |
217
|
|
|
* |
218
|
|
|
* @throws Exception |
219
|
|
|
* |
220
|
|
|
* @return mixed |
221
|
|
|
*/ |
222
|
|
View Code Duplication |
public function rollbackMigration($file) |
|
|
|
|
223
|
|
|
{ |
224
|
|
|
$migration = $this->getMigrationObjectByFileName($file); |
225
|
|
|
|
226
|
|
|
$this->checkTransactionAndRun($migration, function () use ($migration, $file) { |
227
|
|
|
if ($migration->down() === false) { |
228
|
|
|
throw new Exception("<error>Can't rollback migration:</error> {$file}.php"); |
229
|
|
|
} |
230
|
|
|
}); |
231
|
|
|
|
232
|
|
|
$this->removeSuccessfulMigrationFromLog($file); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Remove a migration name from the database so it can be run again. |
237
|
|
|
* |
238
|
|
|
* @param string $file |
239
|
|
|
* |
240
|
|
|
* @return void |
241
|
|
|
*/ |
242
|
|
|
public function removeSuccessfulMigrationFromLog($file) |
243
|
|
|
{ |
244
|
|
|
$this->database->removeSuccessfulMigrationFromLog($file); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Delete migration file. |
249
|
|
|
* |
250
|
|
|
* @param string $migration |
251
|
|
|
* |
252
|
|
|
* @return bool |
253
|
|
|
*/ |
254
|
|
|
public function deleteMigrationFile($migration) |
255
|
|
|
{ |
256
|
|
|
return $this->files->delete($this->getMigrationFilePath($migration)); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Get array of migrations that should be ran. |
261
|
|
|
* |
262
|
|
|
* @return array |
263
|
|
|
*/ |
264
|
|
|
public function getMigrationsToRun() |
265
|
|
|
{ |
266
|
|
|
$allMigrations = $this->getAllMigrations(); |
267
|
|
|
|
268
|
|
|
$ranMigrations = $this->getRanMigrations(); |
269
|
|
|
|
270
|
|
|
return array_diff($allMigrations, $ranMigrations); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Move migration files. |
275
|
|
|
* |
276
|
|
|
* @param array $files |
277
|
|
|
* @param string $toDir |
278
|
|
|
* |
279
|
|
|
* @return int |
280
|
|
|
*/ |
281
|
|
|
public function moveMigrationFiles($files = [], $toDir = '') |
282
|
|
|
{ |
283
|
|
|
$toDir = trim($toDir ?: $this->dir_archive, '/'); |
284
|
|
|
$files = $files ?: $this->getAllMigrations(); |
285
|
|
|
$this->files->createDirIfItDoesNotExist("$this->dir/$toDir"); |
286
|
|
|
|
287
|
|
|
$count = 0; |
288
|
|
|
foreach ($files as $migration) { |
289
|
|
|
$from = $this->getMigrationFilePath($migration); |
290
|
|
|
$to = "$this->dir/$toDir/$migration.php"; |
291
|
|
|
|
292
|
|
|
if ($from == $to) { |
293
|
|
|
continue; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
$flag = $this->files->move($from, $to); |
297
|
|
|
|
298
|
|
|
if ($flag) { |
299
|
|
|
$count++; |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
return $count; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Construct migration file name from migration name and current time. |
308
|
|
|
* |
309
|
|
|
* @param string $name |
310
|
|
|
* |
311
|
|
|
* @return string |
312
|
|
|
*/ |
313
|
|
|
protected function constructFileName($name) |
314
|
|
|
{ |
315
|
|
|
list($usec, $sec) = explode(' ', microtime()); |
316
|
|
|
|
317
|
|
|
$usec = substr($usec, 2, 6); |
318
|
|
|
|
319
|
|
|
return date('Y_m_d_His', $sec).'_'.$usec.'_'.$name; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Get a migration class name by a migration file name. |
324
|
|
|
* |
325
|
|
|
* @param string $file |
326
|
|
|
* |
327
|
|
|
* @return string |
328
|
|
|
*/ |
329
|
|
|
protected function getMigrationClassNameByFileName($file) |
330
|
|
|
{ |
331
|
|
|
$fileExploded = explode('_', $file); |
332
|
|
|
|
333
|
|
|
$datePart = implode('_', array_slice($fileExploded, 0, 5)); |
334
|
|
|
$namePart = implode('_', array_slice($fileExploded, 5)); |
335
|
|
|
|
336
|
|
|
return Helpers::studly($namePart.'_'.$datePart); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Replace all placeholders in the stub. |
341
|
|
|
* |
342
|
|
|
* @param string $template |
343
|
|
|
* @param array $replace |
344
|
|
|
* |
345
|
|
|
* @return string |
346
|
|
|
*/ |
347
|
|
|
protected function replacePlaceholdersInTemplate($template, array $replace) |
348
|
|
|
{ |
349
|
|
|
foreach ($replace as $placeholder => $value) { |
350
|
|
|
$template = str_replace("__{$placeholder}__", $value, $template); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
return $template; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Resolve a migration instance from a file. |
358
|
|
|
* |
359
|
|
|
* @param string $file |
360
|
|
|
* |
361
|
|
|
* @throws Exception |
362
|
|
|
* |
363
|
|
|
* @return MigrationInterface |
364
|
|
|
*/ |
365
|
|
|
protected function getMigrationObjectByFileName($file) |
366
|
|
|
{ |
367
|
|
|
$class = $this->getMigrationClassNameByFileName($file); |
368
|
|
|
|
369
|
|
|
$this->requireMigrationFile($file); |
370
|
|
|
|
371
|
|
|
$object = new $class(); |
372
|
|
|
|
373
|
|
|
if (!$object instanceof MigrationInterface) { |
374
|
|
|
throw new Exception("Migration class {$class} must implement Arrilot\\BitrixMigrations\\Interfaces\\MigrationInterface"); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
return $object; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* Require migration file. |
382
|
|
|
* |
383
|
|
|
* @param string $file |
384
|
|
|
* |
385
|
|
|
* @return void |
386
|
|
|
*/ |
387
|
|
|
protected function requireMigrationFile($file) |
388
|
|
|
{ |
389
|
|
|
$this->files->requireFile($this->getMigrationFilePath($file)); |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Get path to a migration file. |
394
|
|
|
* |
395
|
|
|
* @param string $migration |
396
|
|
|
* |
397
|
|
|
* @return string |
398
|
|
|
*/ |
399
|
|
|
protected function getMigrationFilePath($migration) |
400
|
|
|
{ |
401
|
|
|
$files = Helpers::rGlob("$this->dir/$migration.php"); |
402
|
|
|
if (count($files) != 1) { |
403
|
|
|
throw new \Exception("Not found migration file"); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
return $files[0]; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* If package arrilot/bitrix-iblock-helper is loaded then we should disable its caching to avoid problems. |
411
|
|
|
*/ |
412
|
|
|
private function disableBitrixIblockHelperCache() |
413
|
|
|
{ |
414
|
|
|
if (class_exists('\\Arrilot\\BitrixIblockHelper\\IblockId')) { |
415
|
|
|
IblockId::setCacheTime(0); |
416
|
|
|
if (method_exists('\\Arrilot\\BitrixIblockHelper\\IblockId', 'flushLocalCache')) { |
417
|
|
|
IblockId::flushLocalCache(); |
418
|
|
|
} |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
if (class_exists('\\Arrilot\\BitrixIblockHelper\\HLBlock')) { |
422
|
|
|
HLBlock::setCacheTime(0); |
423
|
|
|
if (method_exists('\\Arrilot\\BitrixIblockHelper\\HLBlock', 'flushLocalCache')) { |
424
|
|
|
HLBlock::flushLocalCache(); |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* @param MigrationInterface $migration |
431
|
|
|
* @param callable $callback |
432
|
|
|
* @throws Exception |
433
|
|
|
*/ |
434
|
|
|
protected function checkTransactionAndRun($migration, $callback) |
435
|
|
|
{ |
436
|
|
|
if ($migration->useTransaction($this->use_transaction)) { |
437
|
|
|
$this->database->startTransaction(); |
438
|
|
|
Logger::log("Начало транзакции", Logger::COLOR_LIGHT_BLUE); |
439
|
|
|
try { |
440
|
|
|
$callback(); |
441
|
|
|
} catch (\Exception $e) { |
442
|
|
|
$this->database->rollbackTransaction(); |
443
|
|
|
Logger::log("Откат транзакции из-за ошибки '{$e->getMessage()}'", Logger::COLOR_LIGHT_RED); |
444
|
|
|
throw $e; |
445
|
|
|
} |
446
|
|
|
$this->database->commitTransaction(); |
447
|
|
|
Logger::log("Конец транзакции", Logger::COLOR_LIGHT_BLUE); |
448
|
|
|
} else { |
449
|
|
|
$callback(); |
450
|
|
|
} |
451
|
|
|
} |
452
|
|
|
} |
453
|
|
|
|
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.