Migrator::replacePlaceholdersInTemplate()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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