Completed
Push — master ( 5a501b...3982a6 )
by Gaetano
08:04
created

MigrationService::executeMigrationInner()   F

Complexity

Conditions 14
Paths 2806

Size

Total Lines 137

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 0
Metric Value
dl 0
loc 137
ccs 0
cts 86
cp 0
rs 1.68
c 0
b 0
f 0
cc 14
nc 2806
nop 6
crap 210

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 Kaliop\eZMigrationBundle\API\Value\MigrationStep;
6
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
7
use eZ\Publish\API\Repository\Repository;
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\MigrationSuspendedException;
19
use Kaliop\eZMigrationBundle\API\Exception\MigrationStepSkippedException;
20
use Kaliop\eZMigrationBundle\API\Exception\AfterMigrationExecutionException;
21
use Kaliop\eZMigrationBundle\API\Event\BeforeStepExecutionEvent;
22
use Kaliop\eZMigrationBundle\API\Event\StepExecutedEvent;
23
use Kaliop\eZMigrationBundle\API\Event\MigrationAbortedEvent;
24
use Kaliop\eZMigrationBundle\API\Event\MigrationSuspendedEvent;
25
26
class MigrationService implements ContextProviderInterface
27
{
28
    use RepositoryUserSetterTrait;
29
30
    /**
31
     * The default Admin user Id, used when no Admin user is specified
32
     */
33
    const ADMIN_USER_ID = 14;
34
35
    /**
36
     * @var LoaderInterface $loader
37
     */
38
    protected $loader;
39
    /**
40
     * @var StorageHandlerInterface $storageHandler
41
     */
42
    protected $storageHandler;
43
44
    /** @var DefinitionParserInterface[] $DefinitionParsers */
45
    protected $DefinitionParsers = array();
46
47
    /** @var ExecutorInterface[] $executors */
48
    protected $executors = array();
49
50
    protected $repository;
51
52
    protected $dispatcher;
53
54
    /**
55
     * @var ContextHandler $contextHandler
56
     */
57
    protected $contextHandler;
58
59
    protected $eventPrefix = 'ez_migration.';
60
61
    protected $eventEntity = 'migration';
62
63
    protected $migrationContext = array();
64
65
    public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository,
66
        EventDispatcherInterface $eventDispatcher, $contextHandler)
67
    {
68
        $this->loader = $loader;
69
        $this->storageHandler = $storageHandler;
70
        $this->repository = $repository;
71
        $this->dispatcher = $eventDispatcher;
72
        $this->contextHandler = $contextHandler;
73
    }
74
75
    public function addDefinitionParser(DefinitionParserInterface $DefinitionParser)
76
    {
77
        $this->DefinitionParsers[] = $DefinitionParser;
78
    }
79
80
    public function addExecutor(ExecutorInterface $executor)
81
    {
82
        foreach ($executor->supportedTypes() as $type) {
83
            $this->executors[$type] = $executor;
84
        }
85
    }
86
87
    /**
88
     * @param string $type
89
     * @return ExecutorInterface
90
     * @throws \InvalidArgumentException If executor doesn't exist
91
     */
92
    public function getExecutor($type)
93
    {
94
        if (!isset($this->executors[$type])) {
95
            throw new \InvalidArgumentException("Executor with type '$type' doesn't exist");
96
        }
97
98
        return $this->executors[$type];
99
    }
100
101
    /**
102
     * @return string[]
103
     */
104
    public function listExecutors()
105
    {
106
        return array_keys($this->executors);
107
    }
108
109
    public function setLoader(LoaderInterface $loader)
110
    {
111
        $this->loader = $loader;
112
    }
113
114
    /**
115
     * NB: returns UNPARSED definitions
116
     *
117
     * @param string[] $paths
118
     * @return MigrationDefinitionCollection key: migration name, value: migration definition as binary string
119
     */
120
    public function getMigrationsDefinitions(array $paths = array())
121
    {
122
        // we try to be flexible in file types we support, and the same time avoid loading all files in a directory
123
        $handledDefinitions = array();
124
        foreach ($this->loader->listAvailableDefinitions($paths) as $migrationName => $definitionPath) {
125
            foreach ($this->DefinitionParsers as $definitionParser) {
126
                if ($definitionParser->supports($migrationName)) {
127
                    $handledDefinitions[] = $definitionPath;
128
                }
129
            }
130
        }
131
132
        // we can not call loadDefinitions with an empty array using the Filesystem loader, or it will start looking in bundles...
133
        if (empty($handledDefinitions) && !empty($paths)) {
134
            return new MigrationDefinitionCollection();
135
        }
136
137
        return $this->loader->loadDefinitions($handledDefinitions);
138
    }
139
140
    /**
141
     * Returns the list of all the migrations which where executed or attempted so far
142
     *
143
     * @param int $limit 0 or below will be treated as 'no limit'
144
     * @param int $offset
145
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
146
     */
147
    public function getMigrations($limit = null, $offset = null)
148
    {
149
        return $this->storageHandler->loadMigrations($limit, $offset);
150
    }
151
152
    /**
153
     * Returns the list of all the migrations in a given status which where executed or attempted so far
154
     *
155
     * @param int $status
156
     * @param int $limit 0 or below will be treated as 'no limit'
157
     * @param int $offset
158
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
159
     */
160
    public function getMigrationsByStatus($status, $limit = null, $offset = null)
161
    {
162
        return $this->storageHandler->loadMigrationsByStatus($status, $limit, $offset);
163
    }
164
165
    /**
166
     * @param string $migrationName
167
     * @return Migration|null
168
     */
169
    public function getMigration($migrationName)
170
    {
171
        return $this->storageHandler->loadMigration($migrationName);
172
    }
173
174
    /**
175
     * @param MigrationDefinition $migrationDefinition
176
     * @return Migration
177
     */
178
    public function addMigration(MigrationDefinition $migrationDefinition)
179
    {
180
        return $this->storageHandler->addMigration($migrationDefinition);
181
    }
182
183
    /**
184
     * @param Migration $migration
185
     */
186
    public function deleteMigration(Migration $migration)
187
    {
188
        return $this->storageHandler->deleteMigration($migration);
189
    }
190
191
    /**
192
     * @param MigrationDefinition $migrationDefinition
193
     * @return Migration
194
     */
195
    public function skipMigration(MigrationDefinition $migrationDefinition)
196
    {
197
        return $this->storageHandler->skipMigration($migrationDefinition);
198
    }
199
200
    /**
201
     * Not to be called by external users for normal use cases, you should use executeMigration() instead
202
     *
203
     * @param Migration $migration
204
     */
205
    public function endMigration(Migration $migration)
206
    {
207
        return $this->storageHandler->endMigration($migration);
208
    }
209
210
    /**
211
     * Parses a migration definition, return a parsed definition.
212
     * If there is a parsing error, the definition status will be updated accordingly
213
     *
214
     * @param MigrationDefinition $migrationDefinition
215
     * @return MigrationDefinition
216
     * @throws \Exception if the migrationDefinition has no suitable parser for its source format
217
     */
218
    public function parseMigrationDefinition(MigrationDefinition $migrationDefinition)
219
    {
220
        foreach ($this->DefinitionParsers as $definitionParser) {
221
            if ($definitionParser->supports($migrationDefinition->name)) {
222
                // parse the source file
223
                $migrationDefinition = $definitionParser->parseMigrationDefinition($migrationDefinition);
224
225
                // and make sure we know how to handle all steps
226
                foreach ($migrationDefinition->steps as $step) {
227
                    if (!isset($this->executors[$step->type])) {
228
                        return new MigrationDefinition(
229
                            $migrationDefinition->name,
230
                            $migrationDefinition->path,
231
                            $migrationDefinition->rawDefinition,
232
                            MigrationDefinition::STATUS_INVALID,
233
                            array(),
234
                            "Can not handle migration step of type '{$step->type}'"
235
                        );
236
                    }
237
                }
238
239
                return $migrationDefinition;
240
            }
241
        }
242
243
        throw new \Exception("No parser available to parse migration definition '{$migrationDefinition->name}'");
244
    }
245
246
    /**
247
     * @param MigrationDefinition $migrationDefinition
248
     * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration
249
     * @param string $defaultLanguageCode
250
     * @param string|int|false|null $adminLogin when false, current user is used; when null, hardcoded admin account
251
     * @throws \Exception
252
     *
253
     * @todo treating a null and false $adminLogin values differently is prone to hard-to-track errors.
254
     *       Shall we use instead -1 to indicate the desire to not-login-as-admin-user-at-all ?
255
     */
256
    public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true,
257
        $defaultLanguageCode = null, $adminLogin = null)
258
    {
259
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
260
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
261
        }
262
263 View Code Duplication
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
264
            throw new \Exception("Can not execute " . $this->getEntityName($migrationDefinition). " '{$migrationDefinition->name}': {$migrationDefinition->parsingError}");
265
        }
266
267
        /// @todo add support for setting in $migrationContext a userContentType ?
268
        $migrationContext = $this->migrationContextFromParameters($defaultLanguageCode, $adminLogin);
269
270
        // set migration as begun - has to be in own db transaction
271
        $migration = $this->storageHandler->startMigration($migrationDefinition);
272
273
        $this->executeMigrationInner($migration, $migrationDefinition, $migrationContext, 0, $useTransaction, $adminLogin);
274
    }
275
276
    /**
277
     * @param Migration $migration
278
     * @param MigrationDefinition $migrationDefinition
279
     * @param array $migrationContext
280
     * @param int $stepOffset
281
     * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration
282
     * @param string|int|false|null $adminLogin used only for committing db transaction if needed. If false or null, hardcoded admin is used
283
     * @throws \Exception
284
     */
285
    protected function executeMigrationInner(Migration $migration, MigrationDefinition $migrationDefinition,
286
        $migrationContext, $stepOffset = 0, $useTransaction = true, $adminLogin = null)
287
    {
288
        if ($useTransaction) {
289
            $this->repository->beginTransaction();
290
        }
291
292
        $this->migrationContext[$migration->name] = array('context' => $migrationContext);
293
        $previousUserId = null;
294
        $steps = array_slice($migrationDefinition->steps->getArrayCopy(), $stepOffset);
295
296
        try {
297
298
            $i = $stepOffset+1;
299
            $finalStatus = Migration::STATUS_DONE;
300
            $finalMessage = null;
301
302
            try {
303
304
                foreach ($steps as $step) {
305
                    // save enough data in the context to be able to successfully suspend/resume
306
                    $this->migrationContext[$migration->name]['step'] = $i;
307
308
                    $step = $this->injectContextIntoStep($step, $migrationContext);
309
310
                    // we validated the fact that we have a good executor at parsing time
311
                    $executor = $this->executors[$step->type];
312
313
                    $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor);
314
                    $this->dispatcher->dispatch($this->eventPrefix . 'before_execution', $beforeStepExecutionEvent);
315
                    // allow some sneaky trickery here: event listeners can manipulate 'live' the step definition and the executor
316
                    $executor = $beforeStepExecutionEvent->getExecutor();
317
                    $step = $beforeStepExecutionEvent->getStep();
318
319
                    try {
320
                        $result = $executor->execute($step);
321
322
                        $this->dispatcher->dispatch($this->eventPrefix . 'step_executed', new StepExecutedEvent($step, $result));
323
                    } catch (MigrationStepSkippedException $e) {
324
                        continue;
325
                    }
326
327
                    $i++;
328
                }
329
330
            } catch (MigrationAbortedException $e) {
331
                // allow a migration step (or events) to abort the migration via a specific exception
332
333
                $this->dispatcher->dispatch($this->eventPrefix . $this->eventEntity . '_aborted', new MigrationAbortedEvent($step, $e));
334
335
                $finalStatus = $e->getCode();
336
                $finalMessage = "Abort in execution of step $i: " . $e->getMessage();
337
            } catch (MigrationSuspendedException $e) {
338
                // allow a migration step (or events) to suspend the migration via a specific exception
339
340
                $this->dispatcher->dispatch($this->eventPrefix . $this->eventEntity . '_suspended', new MigrationSuspendedEvent($step, $e));
341
342
                // let the context handler store our context, along with context data from any other (tagged) service which has some
343
                $this->contextHandler->storeCurrentContext($migration->name);
344
345
                $finalStatus = Migration::STATUS_SUSPENDED;
346
                $finalMessage = "Suspended in execution of step $i: " . $e->getMessage();
347
            }
348
349
            // in case we have an exception thrown in the commit phase after the last step, make sure we report the correct step
350
            $i--;
351
352
            // set migration as done
353
            $this->storageHandler->endMigration(new Migration(
354
                $migration->name,
355
                $migration->md5,
356
                $migration->path,
357
                $migration->executionDate,
358
                $finalStatus,
359
                $finalMessage
360
            ));
361
362
            if ($useTransaction) {
363
                // there might be workflows or other actions happening at commit time that fail if we are not admin
364
                $previousUserId = $this->loginUser($this->getAdminUserIdentifier($adminLogin));
365
366
                $this->repository->commit();
367
                $this->loginUser($previousUserId);
368
            }
369
370
        } catch (\Exception $e) {
371
372
            $errorMessage = $this->getFullExceptionMessage($e) . ' in file ' . $e->getFile() . ' line ' . $e->getLine();
373
            $finalStatus = Migration::STATUS_FAILED;
374
            $exception = null;
375
376
            if ($useTransaction) {
377
                try {
378
                    // cater to the case where the $this->repository->commit() call above throws an exception
379
                    if ($previousUserId) {
380
                        $this->loginUser($previousUserId);
381
                    }
382
383
                    // there is no need to become admin here, at least in theory
384
                    $this->repository->rollBack();
385
386
                } catch (\Exception $e2) {
387
                    // This check is not rock-solid, but at the moment is all we can do to tell apart 2 cases of
388
                    // exceptions originating above: the case where the commit was successful but handling of a commit-queue
389
                    // signal failed, from the case where something failed beforehand.
390
                    // Known cases for signals failing at commit time include fe. https://jira.ez.no/browse/EZP-29333
391
                    if ($previousUserId && $e2->getMessage() == 'There is no active transaction.') {
392
                        // since the migration succeeded and it was committed, no use to mark it as failed...
393
                        $finalStatus = Migration::STATUS_DONE;
394
                        $errorMessage = 'An exception was thrown after committing, in file ' .
395
                            $e->getFile() . ' line ' . $e->getLine() . ': ' . $this->getFullExceptionMessage($e);
396
                        $exception = new AfterMigrationExecutionException($errorMessage, $i, $e);
397
                    } else {
398
                        $errorMessage .= '. In addition, an exception was thrown while rolling back, in file ' .
399
                            $e2->getFile() . ' line ' . $e2->getLine() . ': ' . $this->getFullExceptionMessage($e2);
400
                    }
401
                }
402
            }
403
404
            // set migration as failed
405
            // NB: we use the 'force' flag here because we might be catching an exception happened during the call to
406
            // $this->repository->commit() above, in which case the Migration might already be in the DB with a status 'done'
407
            $this->storageHandler->endMigration(
408
                new Migration(
409
                    $migration->name,
410
                    $migration->md5,
411
                    $migration->path,
412
                    $migration->executionDate,
413
                    $finalStatus,
414
                    $errorMessage
415
                ),
416
                true
417
            );
418
419
            throw $exception ? $exception : new MigrationStepExecutionException($errorMessage, $i, $e);
420
        }
421
    }
422
423
    /**
424
     * @param Migration $migration
425
     * @param bool $useTransaction
426
     * @throws \Exception
427
     *
428
     * @todo add support for adminLogin ?
429
     */
430
    public function resumeMigration(Migration $migration, $useTransaction = true)
431
    {
432
        if ($migration->status != Migration::STATUS_SUSPENDED) {
433
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': it is not in suspended status");
434
        }
435
436
        $migrationDefinitions = $this->getMigrationsDefinitions(array($migration->path));
437
        if (!count($migrationDefinitions)) {
438
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': its definition is missing");
439
        }
440
441
        $defs = $migrationDefinitions->getArrayCopy();
442
        $migrationDefinition = reset($defs);
443
444
        $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
445 View Code Duplication
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
446
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': {$migrationDefinition->parsingError}");
447
        }
448
449
        // restore context
450
        $this->contextHandler->restoreCurrentContext($migration->name);
451
452
        if (!isset($this->migrationContext[$migration->name])) {
453
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': the stored context is missing");
454
        }
455
        $restoredContext = $this->migrationContext[$migration->name];
456
        if (!is_array($restoredContext) || !isset($restoredContext['context']) || !isset($restoredContext['step'] )) {
457
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': the stored context is invalid");
458
        }
459
460
        // update migration status
461
        $migration = $this->storageHandler->resumeMigration($migration);
462
463
        // clean up restored context - ideally it should be in the same db transaction as the line above
464
        $this->contextHandler->deleteContext($migration->name);
465
466
        // and go
467
        // note: we store the current step counting starting at 1, but use offset starting at 0, hence the -1 here
468
        $this->executeMigrationInner($migration, $migrationDefinition, $restoredContext['context'],
469
            $restoredContext['step'] - 1, $useTransaction);
470
    }
471
472
    /**
473
     * @param string $defaultLanguageCode
474
     * @param string|int|false $adminLogin
475
     * @return array
476
     */
477
    protected function migrationContextFromParameters($defaultLanguageCode = null, $adminLogin = null)
478
    {
479
        $properties = array();
480
481
        if ($defaultLanguageCode != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $defaultLanguageCode of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
482
            $properties['defaultLanguageCode'] = $defaultLanguageCode;
483
        }
484
        // nb: other parts of the codebase treat differently a false and null values for $properties['adminUserLogin']
485
        if ($adminLogin !== null) {
486
            $properties['adminUserLogin'] = $adminLogin;
487
        }
488
489
        return $properties;
490
    }
491
492
    protected function injectContextIntoStep(MigrationStep $step, array $context)
493
    {
494
        return new MigrationStep(
495
            $step->type,
496
            $step->dsl,
497
            array_merge($step->context, $context)
498
        );
499
    }
500
501
    /**
502
     * @param string $adminLogin
503
     * @return int|string
504
     */
505
    protected function getAdminUserIdentifier($adminLogin)
506
    {
507
        if ($adminLogin != null) {
508
            return $adminLogin;
509
        }
510
511
        return self::ADMIN_USER_ID;
512
    }
513
514
    /**
515
     * Turns eZPublish cryptic exceptions into something more palatable for random devs
516
     * @todo should this be moved to a lower layer ?
517
     *
518
     * @param \Exception $e
519
     * @return string
520
     */
521
    protected function getFullExceptionMessage(\Exception $e)
522
    {
523
        $message = $e->getMessage();
524
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
525
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException') ||
526
            is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')
527
        ) {
528
            if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException')) {
529
                $errorsArray = $e->getLimitationErrors();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getLimitationErrors() does only exist in the following sub-classes of Exception: eZ\Publish\API\Repositor...tionValidationException, eZ\Publish\Core\Base\Exc...tionValidationException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
530
                if ($errorsArray == null) {
531
                    return $message;
532
                }
533
            } else if (is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')) {
534
                $errorsArray = array();
535
                foreach ($e->getFieldErrors() as $limitationError) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getFieldErrors() does only exist in the following sub-classes of Exception: eZ\Publish\API\Repositor...ieldValidationException, eZ\Publish\API\Repositor...tionValidationException, eZ\Publish\Core\Base\Exc...ieldValidationException, eZ\Publish\Core\Base\Exc...tionValidationException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
536
                    // we get the 1st language
537
                    $errorsArray[] = reset($limitationError);
538
                }
539
            } else {
540
                $errorsArray = $e->getFieldErrors();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getFieldErrors() does only exist in the following sub-classes of Exception: eZ\Publish\API\Repositor...ieldValidationException, eZ\Publish\API\Repositor...tionValidationException, eZ\Publish\Core\Base\Exc...ieldValidationException, eZ\Publish\Core\Base\Exc...tionValidationException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
541
            }
542
543
            foreach ($errorsArray as $errors) {
544
                // sometimes error arrays are 2-level deep, sometimes 1...
545
                if (!is_array($errors)) {
546
                    $errors = array($errors);
547
                }
548
                foreach ($errors as $error) {
549
                    /// @todo find out what is the proper eZ way of getting a translated message for these errors
550
                    $translatableMessage = $error->getTranslatableMessage();
551
                    if (is_a($translatableMessage, '\eZ\Publish\API\Repository\Values\Translation\Plural')) {
552
                        $msgText = $translatableMessage->plural;
553
                    } else {
554
                        $msgText = $translatableMessage->message;
555
                    }
556
557
                    $message .= "\n" . $msgText . " - " . var_export($translatableMessage->values, true);
558
                }
559
            }
560
        }
561
562
        while (($e = $e->getPrevious()) != null) {
563
            $message .= "\n" . $e->getMessage();
564
        }
565
566
        return $message;
567
    }
568
569
    /**
570
     * @param string $migrationName
571
     * @return array
572
     */
573
    public function getCurrentContext($migrationName)
574
    {
575
        return isset($this->migrationContext[$migrationName]) ? $this->migrationContext[$migrationName] : null;
576
    }
577
578
    /**
579
     * @param string $migrationName
580
     * @param array $context
581
     */
582
    public function restoreContext($migrationName, array $context)
583
    {
584
        $this->migrationContext[$migrationName] = $context;
585
    }
586
587
    protected function getEntityName($migration)
588
    {
589
        $array = explode('\\', get_class($migration));
590
        return strtolower(preg_replace('/Definition$/', '', end($array)));
591
    }
592
}
593