Completed
Push — master ( ac3b8b...3c857d )
by Gaetano
18:21
created

MigrationService   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 533
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 20

Test Coverage

Coverage 73.12%

Importance

Changes 0
Metric Value
dl 0
loc 533
c 0
b 0
f 0
wmc 66
lcom 1
cbo 20
rs 2.1568
ccs 98
cts 134
cp 0.7312

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A addDefinitionParser() 0 4 1
A addExecutor() 0 6 2
A getExecutor() 0 8 2
A listExecutors() 0 4 1
B getMigrationsDefinitions() 0 19 6
A getMigrations() 0 4 1
A getMigrationsByStatus() 0 4 1
A getMigration() 0 4 1
A addMigration() 0 4 1
A deleteMigration() 0 4 1
A skipMigration() 0 4 1
A endMigration() 0 4 1
B parseMigrationDefinition() 0 27 5
A executeMigration() 0 19 3
F executeMigrationInner() 0 125 12
B resumeMigration() 0 36 4
A migrationContextFromParameters() 0 13 3
A injectContextIntoStep() 0 8 1
A getAdminUserIdentifier() 0 8 2
C getFullExceptionMessage() 0 47 13
A getCurrentContext() 0 4 2
A restoreContext() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like MigrationService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MigrationService, and based on these observations, apply Extract Interface, too.

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