MigrationService::executeMigrationInner()   F
last analyzed

Complexity

Conditions 38
Paths > 20000

Size

Total Lines 264
Code Lines 129

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 90
CRAP Score 60.5625

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 38
eloc 129
nc 79373953
nop 6
dl 0
loc 264
ccs 90
cts 120
cp 0.75
crap 60.5625
rs 0
c 2
b 1
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core;
4
5
use eZ\Publish\API\Repository\Repository;
6
use Kaliop\eZMigrationBundle\API\ReferenceBagInterface;
7
use Kaliop\eZMigrationBundle\API\Value\MigrationStep;
8
use Kaliop\eZMigrationBundle\API\Collection\MigrationDefinitionCollection;
9
use Kaliop\eZMigrationBundle\API\StorageHandlerInterface;
10
use Kaliop\eZMigrationBundle\API\LoaderInterface;
11
use Kaliop\eZMigrationBundle\API\DefinitionParserInterface;
12
use Kaliop\eZMigrationBundle\API\ExecutorInterface;
13
use Kaliop\eZMigrationBundle\API\ContextProviderInterface;
14
use Kaliop\eZMigrationBundle\API\Value\Migration;
15
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
16
use Kaliop\eZMigrationBundle\API\Exception\MigrationStepExecutionException;
17
use Kaliop\eZMigrationBundle\API\Exception\MigrationAbortedException;
18
use Kaliop\eZMigrationBundle\API\Exception\MigrationBundleException;
19
use Kaliop\eZMigrationBundle\API\Exception\MigrationSuspendedException;
20
use Kaliop\eZMigrationBundle\API\Exception\MigrationStepSkippedException;
21
use Kaliop\eZMigrationBundle\API\Exception\AfterMigrationExecutionException;
22
use Kaliop\eZMigrationBundle\API\Event\BeforeStepExecutionEvent;
23
use Kaliop\eZMigrationBundle\API\Event\StepExecutedEvent;
24
use Kaliop\eZMigrationBundle\API\Event\MigrationAbortedEvent;
25
use Kaliop\eZMigrationBundle\API\Event\MigrationSuspendedEvent;
26
use Symfony\Component\Console\Output\OutputInterface;
27
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
28
29
/// @todo replace transaction-manager trait with a transaction-manager service
30
class MigrationService implements ContextProviderInterface
31
{
32
    use RepositoryUserSetterTrait;
0 ignored issues
show
introduced by
The trait Kaliop\eZMigrationBundle...positoryUserSetterTrait requires some properties which are not provided by Kaliop\eZMigrationBundle\Core\MigrationService: $id, $login
Loading history...
33
    use TransactionManagerTrait;
34
35
    /**
36
     * The default Admin user Id, used when no Admin user is specified
37
     */
38
    const ADMIN_USER_ID = 14;
39
40
    /**
41
     * @var LoaderInterface $loader
42
     */
43
    protected $loader;
44
45
    /**
46
     * @var StorageHandlerInterface $storageHandler
47
     */
48
    protected $storageHandler;
49
50
    /** @var DefinitionParserInterface[] $DefinitionParsers */
51
    protected $DefinitionParsers = array();
52
53
    /** @var ExecutorInterface[] $executors */
54
    protected $executors = array();
55
56
    protected $dispatcher;
57
58
    /**
59
     * @var ContextHandler $contextHandler
60
     */
61
    protected $contextHandler;
62
63
    protected $eventPrefix = 'ez_migration.';
64
65
    protected $eventEntity = 'migration';
66
67
    protected $migrationContext = array();
68
69
    /** @var  OutputInterface $output */
70
    protected $output;
71
72
    /** @var ReferenceBagInterface */
73
    protected $referenceResolver;
74 149
75
    public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository,
76
        EventDispatcherInterface $eventDispatcher, $contextHandler, $referenceResolver)
77 149
    {
78 149
        $this->loader = $loader;
79 149
        $this->storageHandler = $storageHandler;
80 149
        $this->repository = $repository;
81 149
        $this->dispatcher = $eventDispatcher;
82 149
        $this->contextHandler = $contextHandler;
83 149
        $this->referenceResolver = $referenceResolver;
84
    }
85 149
86
    public function addDefinitionParser(DefinitionParserInterface $DefinitionParser)
87 149
    {
88 149
        $this->DefinitionParsers[] = $DefinitionParser;
89
    }
90 149
91
    public function addExecutor(ExecutorInterface $executor)
92 149
    {
93 149
        foreach ($executor->supportedTypes() as $type) {
94
            $this->executors[$type] = $executor;
95 149
        }
96
    }
97
98
    /**
99
     * @param string $type
100
     * @return ExecutorInterface
101
     * @throws \InvalidArgumentException If executor doesn't exist
102 38
     */
103
    public function getExecutor($type)
104 38
    {
105
        if (!isset($this->executors[$type])) {
106
            throw new \InvalidArgumentException("Executor with type '$type' doesn't exist");
107
        }
108 38
109
        return $this->executors[$type];
110
    }
111
112
    /**
113
     * @return string[]
114 37
     */
115
    public function listExecutors()
116 37
    {
117
        return array_keys($this->executors);
118
    }
119
120
    public function setLoader(LoaderInterface $loader)
121
    {
122
        $this->loader = $loader;
123
    }
124
125
    /**
126
     * @todo we could get rid of this by getting $output passed as argument to self::executeMigration. We are not doing
127
     *       that for BC for the moment (self::executeMigration api should be redone, but it is used in WorkfloBundle too)
128 98
     */
129
    public function setOutput(OutputInterface $output)
130 98
    {
131 98
        $this->output = $output;
132
    }
133
134
    /**
135
     * NB: returns UNPARSED definitions
136
     *
137
     * @param string[] $paths
138
     * @return MigrationDefinitionCollection key: migration name, value: migration definition as binary string
139
     * @throws \Exception
140 145
     */
141
    public function getMigrationsDefinitions(array $paths = array())
142
    {
143 145
        // we try to be flexible in file types we support, and the same time avoid loading all files in a directory
144 145
        $handledDefinitions = array();
145 145
        foreach ($this->loader->listAvailableDefinitions($paths) as $migrationName => $definitionPath) {
146 145
            foreach ($this->DefinitionParsers as $definitionParser) {
147 145
                if ($definitionParser->supports($migrationName)) {
148
                    $handledDefinitions[] = $definitionPath;
149
                }
150
            }
151
        }
152
153 145
        // we can not call loadDefinitions with an empty array using the Filesystem loader, or it will start looking in bundles...
154
        if (empty($handledDefinitions) && !empty($paths)) {
155
            return new MigrationDefinitionCollection();
156
        }
157 145
158
        return $this->loader->loadDefinitions($handledDefinitions);
159
    }
160
161
    /**
162
     * Returns the list of all the migrations which where executed or attempted so far
163
     *
164
     * @param int $limit 0 or below will be treated as 'no limit'
165
     * @param int $offset
166
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
167 99
     */
168
    public function getMigrations($limit = null, $offset = null)
169 99
    {
170
        return $this->storageHandler->loadMigrations($limit, $offset);
171
    }
172
173
    /**
174
     * Returns the list of all the migrations in a given status which where executed or attempted so far
175
     *
176
     * @param int $status
177
     * @param int $limit 0 or below will be treated as 'no limit'
178
     * @param int $offset
179
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
180 2
     */
181
    public function getMigrationsByStatus($status, $limit = null, $offset = null)
182 2
    {
183
        return $this->storageHandler->loadMigrationsByStatus($status, $limit, $offset);
184
    }
185 47
186
    public function getMigrationsByPaths(array $paths, $limit = null, $offset = null)
187 47
    {
188
        return $this->storageHandler->loadMigrationsByPaths($paths, $limit, $offset);
0 ignored issues
show
Bug introduced by
The method loadMigrationsByPaths() does not exist on Kaliop\eZMigrationBundle...StorageHandlerInterface. Did you maybe mean loadMigration()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

188
        return $this->storageHandler->/** @scrutinizer ignore-call */ loadMigrationsByPaths($paths, $limit, $offset);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
189
    }
190
191
    /**
192
     * @param string $migrationName
193
     * @return Migration|null
194 108
     */
195
    public function getMigration($migrationName)
196 108
    {
197
        return $this->storageHandler->loadMigration($migrationName);
198
    }
199
200
    /**
201
     * @param MigrationDefinition $migrationDefinition
202
     * @return Migration
203 85
     */
204
    public function addMigration(MigrationDefinition $migrationDefinition)
205 85
    {
206
        return $this->storageHandler->addMigration($migrationDefinition);
207
    }
208
209
    /**
210
     * @param Migration $migration
211 108
     */
212
    public function deleteMigration(Migration $migration)
213 108
    {
214
        return $this->storageHandler->deleteMigration($migration);
215
    }
216
217
    /**
218
     * @param MigrationDefinition $migrationDefinition
219
     * @return Migration
220 1
     */
221
    public function skipMigration(MigrationDefinition $migrationDefinition)
222 1
    {
223
        return $this->storageHandler->skipMigration($migrationDefinition);
224
    }
225
226
    /**
227
     * Not to be called by external users for normal use cases, you should use executeMigration() instead
228
     *
229
     * @param Migration $migration
230
     */
231
    public function endMigration(Migration $migration)
232
    {
233
        return $this->storageHandler->endMigration($migration);
234
    }
235
236
    /**
237
     * Not to be called by external users for normal use cases, you should use executeMigration() instead.
238
     * NB: will act regardless of current migration status.
239
     *
240
     * @param Migration $migration
241
     */
242
    public function failMigration(Migration $migration, $errorMessage)
243
    {
244
        return $this->storageHandler->endMigration(
245
            new Migration(
246
                $migration->name,
247
                $migration->md5,
248
                $migration->path,
249
                $migration->executionDate,
250
                Migration::STATUS_FAILED,
251
                $errorMessage
252
            ),
253
            true
254
        );
255
    }
256
257
    /**
258
     * Parses a migration definition, return a parsed definition.
259
     * If there is a parsing error, the definition status will be updated accordingly
260
     *
261
     * @param MigrationDefinition $migrationDefinition
262
     * @return MigrationDefinition
263
     * @throws \Exception if the migrationDefinition has no suitable parser for its source format
264 148
     */
265
    public function parseMigrationDefinition(MigrationDefinition $migrationDefinition)
266 148
    {
267 148
        foreach ($this->DefinitionParsers as $definitionParser) {
268
            if ($definitionParser->supports($migrationDefinition->name)) {
269 148
                // parse the source file
270
                $migrationDefinition = $definitionParser->parseMigrationDefinition($migrationDefinition);
271
272 148
                // and make sure we know how to handle all steps
273 141
                foreach ($migrationDefinition->steps as $step) {
274 2
                    if (!isset($this->executors[$step->type])) {
275 2
                        return new MigrationDefinition(
276 2
                            $migrationDefinition->name,
277 2
                            $migrationDefinition->path,
278 2
                            $migrationDefinition->rawDefinition,
279 2
                            MigrationDefinition::STATUS_INVALID,
280 2
                            array(),
281
                            "Can not handle migration step of type '{$step->type}'"
282
                        );
283
                    }
284
                }
285 148
286
                return $migrationDefinition;
287
            }
288
        }
289
290
        throw new MigrationBundleException("No parser available to parse migration definition '{$migrationDefinition->name}'");
291
    }
292
293
    /**
294
     * Note: previous API is kept for BC (subclasses reimplementing this method).
295
     * @param MigrationDefinition $migrationDefinition
296
     * @param array $migrationContext Supported array keys are: adminUserLogin, defaultLanguageCode,
297
     *                                forcedReferences, forceExecution, userContentType, userGroupContentType,
298
     *                                useTransaction.
299
     *                                Bool usage is deprecated. It was: when set to false, no repo transaction will be used to wrap the migration
300
     * @param string $defaultLanguageCode Deprecated - use $migrationContext['defaultLanguageCode']
301
     * @param string|int|false|null $adminLogin Deprecated - use $migrationContext['adminLogin']; when false, current user is used; when null, hardcoded admin account
302
     * @param bool $force Deprecated - use $migrationContext['forceExecution']; when true, execute a migration if it was already in status DONE or SKIPPED (would throw by default)
303
     * @param bool|null $forceSigchildEnabled Deprecated
304
     * @throws \Exception
305 89
     *
306
     * @todo treating a null and false $adminLogin values differently is prone to hard-to-track errors.
307
     *       Shall we use instead -1 to indicate the desire to not-login-as-admin-user-at-all ?
308 89
     */
309 4
    public function executeMigration(MigrationDefinition $migrationDefinition, $migrationContext = true,
310
        $defaultLanguageCode = null, $adminLogin = null, $force = false, $forceSigchildEnabled = null)
311
    {
312 89
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
313
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
314
        }
315
316
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
317 89
            throw new MigrationBundleException("Can not execute " . $this->getEntityName($migrationDefinition). " '{$migrationDefinition->name}': {$migrationDefinition->parsingError}");
318
        }
319
320 89
        // BC: handling of legacy method call signature
321
        if (!is_array($migrationContext)) {
322 89
            $useTransaction = $migrationContext;
323 60
            $migrationContext = $this->migrationContextFromParameters($defaultLanguageCode, $adminLogin, $forceSigchildEnabled);
0 ignored issues
show
Deprecated Code introduced by
The function Kaliop\eZMigrationBundle...ContextFromParameters() has been deprecated: kept for BC ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

323
            $migrationContext = /** @scrutinizer ignore-deprecated */ $this->migrationContextFromParameters($defaultLanguageCode, $adminLogin, $forceSigchildEnabled);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
324
            $migrationContext['useTransaction'] = $useTransaction;
325
            $migrationContext['forceExecution'] = $force;
326
        } else {
327
            if ($defaultLanguageCode !== null || $adminLogin !== null || $force !== false || $forceSigchildEnabled !== null) {
328
                throw new MigrationBundleException("Invalid call to executeMigration: argument types mismatch");
329
            }
330
        }
331
        if ($this->output) {
332
            $migrationContext['output'] = $this->output;
333
        }
334 89
        $forceExecution = array_key_exists('forceExecution', $migrationContext) ? $migrationContext['forceExecution'] : false;
335
336
        /// @todo log a warning if there are already db transactions active (an active pdo-only transaction will result in an
337 89
        ///       exception, but a dbal transaction will result in not committing immediately transaction status update)
338 15
339
        // set migration as begun - has to be in own db transaction
340
        $migration = $this->storageHandler->startMigration($migrationDefinition, $forceExecution);
341 89
342 89
        $this->executeMigrationInner($migration, $migrationDefinition, $migrationContext);
343 89
    }
344
345
    /**
346
     * Note: previous API is kept for BC (subclasses reimplementing this method).
347 89
     * @param Migration $migration
348 89
     * @param MigrationDefinition $migrationDefinition
349 89
     * @param array $migrationContext
350
     * @param int $stepOffset
351
     * @param bool $useTransaction Deprecated - replaced by $migrationContext['useTransaction']. When set to false, no repo transaction will be used to wrap the migration
352
     * @param string|int|false|null $adminLogin Deprecated - $migrationContext['adminUserLogin']. Used only for committing db transaction if needed. If false or null, hardcoded admin is used
353 89
     * @throws \Exception
354
     */
355 89
    protected function executeMigrationInner(Migration $migration, MigrationDefinition $migrationDefinition,
356
        $migrationContext, $stepOffset = 0, $useTransaction = true, $adminLogin = null)
357 89
    {
358
        /// @todo can we make this validation smarter / move it somewhere else?
359
        if (array_key_exists('path', $migrationContext) || array_key_exists('contentTypeIdentifier', $migrationContext) ||
360 89
            array_key_exists('fieldIdentifier', $migrationContext)) {
361
            throw new MigrationBundleException("Invalid call to executeMigrationInner: forbidden elements in migrationContext");
362 89
        }
363 89
364
        // BC: handling of legacy method call signature
365 89
        $useTransaction = array_key_exists('useTransaction', $migrationContext) ? $migrationContext['useTransaction'] : $useTransaction;
366 89
        $adminLogin = array_key_exists('adminUserLogin', $migrationContext) ? $migrationContext['adminUserLogin'] : $adminLogin;
367
368
        $messageSuffix = '';
369 89
        if (isset($migrationContext['forcedReferences']) && count($migrationContext['forcedReferences'])) {
370
            $messageSuffix = array();
371 63
            foreach ($migrationContext['forcedReferences'] as $name => $value) {
372 36
                $this->referenceResolver->addReference($name, $value, true);
373 2
                $messageSuffix[] = "$name: $value";
374
            }
375
            $messageSuffix = 'Injected references: ' . implode(', ', $messageSuffix);
376 63
        }
377
378
        $this->migrationContext[$migration->name] = array('context' => $migrationContext);
379 34
380
        $steps = array_slice($migrationDefinition->steps->getArrayCopy(), $stepOffset);
381
        $i = $stepOffset+1;
382 4
        $finalStatus = Migration::STATUS_DONE;
383
        $finalMessage = '';
384 4
        $error = null;
385 4
        $isCommitting = false;
386 30
        $requires = null;
387
388
        try {
389 1
390
            if ($useTransaction) {
391
                /// @todo in case there is already a db transaction running, we should throw - or at least
392 1
                ///       give a warning
393
                try {
394 1
                    $this->beginTransaction();
395 1
                    $requires = 'commit';
396
                } catch (\Exception $e) {
397
                    $finalStatus = Migration::STATUS_FAILED;
398
                    $finalMessage = 'An exception was thrown while starting the transaction before the migration: ' . $this->getFullExceptionMessage($e, true);
0 ignored issues
show
Unused Code introduced by
The call to Kaliop\eZMigrationBundle...tFullExceptionMessage() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

398
                    $finalMessage = 'An exception was thrown while starting the transaction before the migration: ' . $this->/** @scrutinizer ignore-call */ getFullExceptionMessage($e, true);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
399 60
                    /// @todo use a more specific exception?
400
                    $error = new MigrationBundleException($finalMessage, 0, $e);
401
                    $steps = array();
402 60
                }
403 60
            }
404 60
405 60
            if ($steps) {
406 60
                /// @todo catch errors thrown here
407
                $startTransactionLevel = $this->getDBTransactionNestingLevel();
408
            }
409
410
            try {
411 60
                foreach ($steps as $step) {
412
                    // save enough data in the context to be able to successfully suspend/resume
413 14
                    $this->migrationContext[$migration->name]['step'] = $i;
414
415 14
                    $step = $this->injectContextIntoStep($step, array_merge($migrationContext, array('step' => $i)));
416 60
417
                    // we validated the fact that we have a good executor at parsing time
418
                    $executor = $this->executors[$step->type];
419 29
420
                    $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor);
421
                    $this->dispatcher->dispatch($this->eventPrefix . 'before_execution', $beforeStepExecutionEvent);
422 29
                    // allow some sneaky trickery here: event listeners can manipulate 'live' the step definition and the executor
423 29
                    $executor = $beforeStepExecutionEvent->getExecutor();
424 29
                    $step = $beforeStepExecutionEvent->getStep();
425
426 29
                    try {
427
                        $result = $executor->execute($step);
428
429 1
                        $this->dispatcher->dispatch($this->eventPrefix . 'step_executed', new StepExecutedEvent($step, $result));
430
                    } catch (MigrationStepSkippedException $e) {
431
                        continue;
432
                    }
433
434 1
                    $i++;
435
                }
436
437
            } catch (MigrationAbortedException $e) {
438
                // allow a migration step (or events) to abort the migration via a specific exception
439
440
                $this->dispatcher->dispatch($this->eventPrefix . $this->eventEntity . '_aborted', new MigrationAbortedEvent($step, $e));
441
442
                $finalStatus = $e->getCode();
443
                $finalMessage = "Abort in execution of step $i: " . $e->getMessage();
444
445
                /// @todo we should allow another type of migration aborting: one which forces rollback of changes, and
446
                ///       possibly exits execution with a non-zero code.
447
                ///       Atm it can be achieved by throwing any exception, which is easy to do in php but hard in yml...
448
                ///       Eg. distinguish between STATUS_PARTIALLY_DONE and STATUS_FAILED
449
                /*if ($e->getCode() == Migration::STATUS_FAILED) {
450
                    $error = $e;
451
                    if ($requires == 'commit') {
452
                        $requires = 'rollback';
453
                    }
454
                }*/
455
456
            } catch (MigrationSuspendedException $e) {
457 29
                // allow a migration step (or events) to suspend the migration via a specific exception
458 29
459 29
                $this->dispatcher->dispatch($this->eventPrefix . $this->eventEntity . '_suspended', new MigrationSuspendedEvent($step, $e));
460 29
461 29
                // let the context handler store our context, along with context data from any other (tagged) service which has some
462 29
                $this->contextHandler->storeCurrentContext($migration->name);
463
464
                $finalStatus = Migration::STATUS_SUSPENDED;
465
                $finalMessage = "Suspended in execution of step $i: " . $e->getMessage();
466 29
467
            } catch (\Exception $e) {
468
                /// @todo shall we emit a signal as well?
469 29
470
                if ($requires == 'commit') {
471 60
                    $requires = 'rollback';
472
                }
473
474
                $finalStatus = Migration::STATUS_FAILED;
475
                $finalMessage = $this->getFullExceptionMessage($e, true);
476
                $error = new MigrationStepExecutionException($finalMessage, $i, $e);
477
                $finalMessage = $error->getMessage();
478
            }
479
480
            /// @todo this test only works if the transaction left pending was opened using the PDO connection.
481 1
            ///       If it was opened by a stray/unterminated sql `begin`, it will not be detected, and most likely
482
            ///       throw an error later when we try to save the migration status (unless the migration is run
483 1
            ///       with $useTransaction, in which case the pending transaction just gets committed).
484
            ///       We could try to use PDO::inTransaction to check for pending transactions, but we'd
485
            ///       have to first check if it behaves the same way across all db/php-version combinations...
486
            if ($steps && ($pendingTransactionLevel = $this->getDBTransactionNestingLevel() - $startTransactionLevel) > 0) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $startTransactionLevel does not seem to be defined for all execution paths leading up to this point.
Loading history...
487 1
                // a migration step has opened a transaction and forgotten to close it! For safety, we have to roll back
488 1
489
                // remove extra transaction levels
490
                for ($i = 0; $i < $pendingTransactionLevel; $i++) {
491
                    /// @todo catch errors thrown here
492 1
                    $this->rollbackDBTransaction();
493 1
                }
494
                // if there was a transaction added by ourselves, let it be rolled back
495 1
                if ($requires == 'commit') {
496 1
                    $requires = 'rollback';
497
                }
498
                // mark the migration as failed
499
                $finalStatus = Migration::STATUS_FAILED;
500
                if ($error) {
501 1
                    /// @todo re-inject the new message into $error
502
                    $finalMessage .= '. In addition, the migration had left a database transaction pending';
503 1
                } else {
504 1
                    // it would be nice to tell the user which step actually has opened the transaction left dangling,
505 1
                    // but that could be complicated, taking into account the case of migrations starting a transaction
506
                    // in step X and closing it in step Y, as well as for the possibility of exceptions being thrown
507
                    // halfway through a step
508
                    $finalMessage = 'The migration was rolled back because it had left a database transaction pending';
509 1
                    $error = new MigrationBundleException($finalMessage);
510
                }
511
            }
512 1
513 1
            /// @todo in the same way that we check for migration steps having left a pending transaction in the code
514
            ///       block above, we could check if any transaction has changed the currently logged-in user, and roll
515
            ///       that back, too
516
517
            // in case we are wrapping the migration in a transaction, either commit or roll back
518 1
            if ($requires == 'commit') {
519
                try {
520
                    // there might be workflows or other actions happening at commit time that fail if we are not admin
521 1
                    // when committing
522
                    $previousUserId = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $previousUserId is dead and can be removed.
Loading history...
523
                    $previousUserId = $this->loginUser($this->getAdminUserIdentifier($adminLogin));
0 ignored issues
show
Bug introduced by
It seems like $adminLogin can also be of type false; however, parameter $adminLogin of Kaliop\eZMigrationBundle...etAdminUserIdentifier() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

523
                    $previousUserId = $this->loginUser($this->getAdminUserIdentifier(/** @scrutinizer ignore-type */ $adminLogin));
Loading history...
524
525 1
                    // Note that the repository by design does first execute a db commit, then carries out some other stuff,
526 1
                    // such as f.e. sending data to Solr. It is useful to differentiate between errors thrown during the
527 1
                    // two phases, as the end results are very different in the db, but it is hard to do so by looking
528
                    // into the exception that gets thrown here. So we just mark the transaction for rollback in case an
529
                    // error occurs, and check what happens during rollback later on.
530
                    $isCommitting = true;
531
                    $this->commit();
532
                    $isCommitting = false;
533
                } catch (\Exception $e) {
534
                    // When running some DDL queries, some databases (eg. mysql, oracle) do commit any pending transaction.
535 89
                    // Since php 8.0, the PDO driver does properly take that into account, and throws an exception
536
                    // when we try to commit here. Short of analyzing any executed migration step checking for execution
537 89
                    // of DDL, all we can do is swallow the error.
538
                    // NB: what we get is a chain: RuntimeException/RuntimeException/PDOException. Should we validate it fully?
539 89
                    if ($e instanceof \RuntimeException && $e->getMessage() == 'There is no active transaction') {
540 1
                        $isCommitting = false;
541
                        $this->resetDBTransaction();
542
                        // save a warning in the migration status
543 89
                        $finalMessage = 'Some migration step committed the transaction halfway';
544 1
                    } else {
545
                        $finalStatus = Migration::STATUS_FAILED;
546 89
                        $finalMessage = 'An exception was thrown while committing: ' . $this->getFullExceptionMessage($e, true);
547
                        $requires = 'rollback';
548
                        /// @todo use a more specific exception?
549
                        $error = new MigrationBundleException($finalMessage, 0, $e);
550
                    }
551 89
                } finally {
552 85
                    /// @todo wrap any (unlikely) exception from this call in an AfterMigrationExecutionException
553
                    if ($previousUserId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $previousUserId of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
554
                        $this->loginUser($previousUserId);
555 89
                    }
556
                }
557
            }
558 89
559
            if ($requires == 'rollback') {
560 89
                try {
561 89
                    // there is no need to be admin here, at least in theory
562 89
                    $this->rollBack();
563 89
                } catch (\Exception $e) {
564
                    // This check is not rock-solid, but at the moment is the best we can do to tell apart 2 cases of
565
                    // exceptions originating above during commit: the case where the commit was successful but handling
566
                    // of a commit-queue signal failed, from the case where something failed beforehand.
567
                    // Known cases for signals failing at commit time include fe. https://jira.ez.no/browse/EZP-29333
568
                    if ($isCommitting && $e instanceof \RuntimeException && $e->getMessage() == 'There is no active transaction') {
569
                        // since all the migration steps succeeded and it was committed (because there was nothing to roll back),
570
                        // no use to mark it as failed...
571 14
                        $finalStatus = Migration::STATUS_DONE;
572
                        $finalMessage = 'An exception was thrown after committing, in file: ' .
573 14
                            $this->getFullExceptionMessage($error, true);
0 ignored issues
show
Bug introduced by
It seems like $error can also be of type null; however, parameter $e of Kaliop\eZMigrationBundle...tFullExceptionMessage() does only seem to accept Exception, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

573
                            $this->getFullExceptionMessage(/** @scrutinizer ignore-type */ $error, true);
Loading history...
574
                        $error = new AfterMigrationExecutionException($finalMessage, 0, $e);
575
                    } else {
576
                        $finalMessage .= '. In addition, an exception was thrown while rolling back, in file ' .
577 14
                            $e->getFile() . ' line ' . $e->getLine() . ': ' . $e->getMessage();
578
                        $errorClass = get_class($error);
0 ignored issues
show
Bug introduced by
It seems like $error can also be of type null; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

578
                        $errorClass = get_class(/** @scrutinizer ignore-type */ $error);
Loading history...
579
                        // we trust the constructor to be fine as error should only be a subclass of MigrationBundleException
580
                        $error = new $errorClass($finalMessage, $error->getCode(), $error->getPrevious());
581
                    }
582
                }
583
            }
584
585
        } finally {
586
587 29
            // save migration status
588
589 29
            $finalMessage = ($finalMessage != '' && $messageSuffix != '') ? $finalMessage . '. '. $messageSuffix : $finalMessage . $messageSuffix;
590 29
591 29
            try {
592 29
                $this->storageHandler->endMigration(new Migration(
593
                    $migration->name,
594
                    $migration->md5,
595
                    $migration->path,
596
                    $migration->executionDate,
597
                    $finalStatus,
598
                    $finalMessage
599
                ));
600
            } catch (\Exception $e) {
601
                // If we get here, the migration will be left in 'executing' state. It might be worth re-trying
602
                // to store its status for a couple of times, in case the error is transient, but that would
603
                // overcomplicate the business logic. So we at least give to the end user a specific error message
604
605
                if ($error) {
606
                    /// @todo use a more specific exception
607
                    $errorMessage = $finalMessage . '. In addition, an exception was thrown while saving migration status after its execution, in file ' .
608
                        $e->getFile() . ' line ' . $e->getLine() . ': ' . $e->getMessage();
609
                    $error = new MigrationBundleException($errorMessage, 0, $e);
610
                } else {
611
                    $errorMessage = 'An exception was thrown while saving migration status after its execution: ' .
612
                        $this->getFullExceptionMessage($e, true);
613
                    $error = new AfterMigrationExecutionException($errorMessage, 0, $e);
614
                }
615
            }
616
617
            if ($error) {
618
                throw $error;
619
            }
620
        }
621
    }
622
623
    /**
624
     * Note: previous API is kept for BC (subclasses reimplementing this method).
625
     * @param Migration $migration
626
     * @param array $migrationContext see executeMigration
627
     * @param array $forcedReferences Deprecated - use $migrationContext['forcedReferences']
628 29
     * @throws \Exception
629 2
     */
630
    public function resumeMigration(Migration $migration, $migrationContext = true, array $forcedReferences = array())
631
    {
632 29
        // BC: handling of legacy method call signature
633
        if (!is_array($migrationContext)) {
634
            $migrationContext = array(
635
                'useTransaction' => $migrationContext,
636
                'forcedReferences' => $forcedReferences,
637
            );
638
        } else {
639 1
            if (!is_array($forcedReferences) || count($forcedReferences)) {
0 ignored issues
show
introduced by
The condition is_array($forcedReferences) is always true.
Loading history...
640
                throw new MigrationBundleException("Invalid call to resumeMigration: argument types mismatch");
641 1
            }
642
        }
643 1
644
        if ($migration->status != Migration::STATUS_SUSPENDED) {
645 1
            throw new MigrationBundleException("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': it is not in suspended status");
646
        }
647
648 1
        $migrationDefinitions = $this->getMigrationsDefinitions(array($migration->path));
649
        if (!count($migrationDefinitions)) {
650
            throw new MigrationBundleException("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': its definition is missing");
651
        }
652
653
        $defs = $migrationDefinitions->getArrayCopy();
654
        $migrationDefinition = reset($defs);
655 1
656
        $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
657 1
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
658 1
            throw new MigrationBundleException("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': {$migrationDefinition->parsingError}");
659 1
        }
660
661 1
        // restore context
662
        $this->contextHandler->restoreCurrentContext($migration->name);
663
        if (!isset($this->migrationContext[$migration->name])) {
664
            throw new MigrationBundleException("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': the stored context is missing");
665
        }
666
        $restoredContext = $this->migrationContext[$migration->name];
667
        if (!is_array($restoredContext) || !isset($restoredContext['context']) || !isset($restoredContext['step'])) {
668
            throw new MigrationBundleException("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': the stored context is invalid");
669
        }
670
671
        // update migration status
672
        $migration = $this->storageHandler->resumeMigration($migration);
673
674
        // clean up restored context - ideally it should be in the same db transaction as the line above
675
        $this->contextHandler->deleteContext($migration->name);
676
677
        // and go
678
        // note: we store the current step counting starting at 1, but use offset starting at 0, hence the -1 here
679
        $this->executeMigrationInner($migration, $migrationDefinition, array_merge($restoredContext['context'], $migrationContext),
680
            $restoredContext['step'] - 1);
681
    }
682
683
    /**
684
     * @param string $defaultLanguageCode
685
     * @param string|int|false $adminLogin
686
     * @param bool|null $forceSigchildEnabled Doubly Deprecated!
687
     * @return array
688
     * @deprecated kept for BC
689
     */
690
    protected function migrationContextFromParameters($defaultLanguageCode = null, $adminLogin = null, $forceSigchildEnabled = null)
0 ignored issues
show
Unused Code introduced by
The parameter $forceSigchildEnabled is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

690
    protected function migrationContextFromParameters($defaultLanguageCode = null, $adminLogin = null, /** @scrutinizer ignore-unused */ $forceSigchildEnabled = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
691
    {
692
        $properties = array();
693
694
        if ($defaultLanguageCode != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $defaultLanguageCode of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
695
            $properties['defaultLanguageCode'] = $defaultLanguageCode;
696
        }
697
        // nb: other parts of the codebase treat differently a false and null values for $properties['adminUserLogin']
698
        if ($adminLogin !== null) {
699
            $properties['adminUserLogin'] = $adminLogin;
700
        }
701
        //if ($forceSigchildEnabled !== null)
702
        //{
703
        //    $properties['forceSigchildEnabled'] = $forceSigchildEnabled;
704
        //}
705
706
        return $properties;
707
    }
708
709
    protected function injectContextIntoStep(MigrationStep $step, array $context)
710
    {
711
        return new MigrationStep(
712
            $step->type,
713
            $step->dsl,
714
            array_merge($step->context, $context)
715
        );
716
    }
717
718
    /**
719
     * @param string $adminLogin
720
     * @return int|string
721
     */
722
    protected function getAdminUserIdentifier($adminLogin)
723
    {
724
        if ($adminLogin != null) {
725
            return $adminLogin;
726
        }
727
728
        return self::ADMIN_USER_ID;
729
    }
730
731
    /**
732
     * Turns eZPublish cryptic exceptions into something more palatable for random devs
733
     * @todo should this be moved to a lower layer ?
734
     *
735
     * @param \Exception $e
736
     * @return string
737
     */
738
    protected function getFullExceptionMessage(\Exception $e)
739
    {
740
        $message = $e->getMessage();
741
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
742
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException') ||
743
            is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')
744
        ) {
745
            if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException')) {
746
                $errorsArray = $e->getLimitationErrors();
0 ignored issues
show
Bug introduced by
The method getLimitationErrors() does not exist on Exception. It seems like you code against a sub-type of Exception such as eZ\Publish\API\Repositor...tionValidationException. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

746
                /** @scrutinizer ignore-call */ 
747
                $errorsArray = $e->getLimitationErrors();
Loading history...
747
                if ($errorsArray == null) {
748
                    return $message;
749
                }
750
            } else if (is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')) {
751
                $errorsArray = array();
752
                foreach ($e->getFieldErrors() as $limitationError) {
0 ignored issues
show
Bug introduced by
The method getFieldErrors() does not exist on Exception. It seems like you code against a sub-type of Exception such as eZ\Publish\API\Repositor...tionValidationException or eZ\Publish\API\Repositor...ieldValidationException. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

752
                foreach ($e->/** @scrutinizer ignore-call */ getFieldErrors() as $limitationError) {
Loading history...
753
                    // we get the 1st language
754
                    $errorsArray[] = reset($limitationError);
755
                }
756
            } else {
757
                $errorsArray = $e->getFieldErrors();
758
            }
759
760
            foreach ($errorsArray as $errors) {
761
                // sometimes error arrays are 2-level deep, sometimes 1...
762
                if (!is_array($errors)) {
763
                    $errors = array($errors);
764
                }
765
                foreach ($errors as $error) {
766
                    /// @todo find out what is the proper eZ way of getting a translated message for these errors
767
                    $translatableMessage = $error->getTranslatableMessage();
768
                    if (is_a($translatableMessage, '\eZ\Publish\API\Repository\Values\Translation\Plural')) {
769
                        $msgText = $translatableMessage->plural;
770
                    } else {
771
                        $msgText = $translatableMessage->message;
772
                    }
773
774
                    $message .= "\n" . $msgText . " - " . var_export($translatableMessage->values, true);
775
                }
776
            }
777
        }
778
779
        while (($e = $e->getPrevious()) != null) {
780
            $message .= "\n" . $e->getMessage();
781
        }
782
783
        return $message;
784
    }
785
786
    /**
787
     * @param string $migrationName
788
     * @return array
789
     */
790
    public function getCurrentContext($migrationName)
791
    {
792
        if (!isset($this->migrationContext[$migrationName]))
793
            return null;
794
        $context = $this->migrationContext[$migrationName];
795
        // avoid attempting to store the current outputInterface when saving the context
796
        if (isset($context['output'])) {
797
            unset($context['output']);
798
        }
799
        return $context;
800
    }
801
802
    /**
803
     * This gets called when we call $this->contextHandler->restoreCurrentContext().
804
     * @param string $migrationName
805
     * @param array $context
806
     */
807
    public function restoreContext($migrationName, array $context)
808
    {
809
        $this->migrationContext[$migrationName] = $context;
810
        if ($this->output) {
811
            $this->migrationContext['output'] = $this->output;
812
        }
813
    }
814
815
    protected function getEntityName($migration)
816
    {
817
        $array = explode('\\', get_class($migration));
818
        return strtolower(preg_replace('/Definition$/', '', end($array)));
819
    }
820
}
821