Completed
Push — master ( 938ba4...ee3177 )
by Gaetano
10:05
created

MigrationService::getEntityName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 2
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|int|false|null $adminLogin when false, current user is used; when null, hardcoded admin account
242 3
     * @throws \Exception
243 3
     *
244 3
     * @todo treating a null and false $adminLogin values differently is prone to hard-to-track errors.
245 3
     *       Shall we use instead -1 to indicate the desire to not-login-as-admin-user-at-all ?
246 3
     */
247 3
    public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true,
248 3
                                     $defaultLanguageCode = null, $adminLogin = null)
249 3
    {
250
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
251 12
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
252 12
        }
253 12
254
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
255
            /// @todo !important name of entity should be gotten dynamically (migration vs. workflow)
256
            throw new \Exception("Can not execute migration '{$migrationDefinition->name}': {$migrationDefinition->parsingError}");
257
        }
258
259
        /// @todo add support for setting in $migrationContext a userContentType ?
260
        $migrationContext = $this->migrationContextFromParameters($defaultLanguageCode, $adminLogin);
261
262 19
        // set migration as begun - has to be in own db transaction
263
        $migration = $this->storageHandler->startMigration($migrationDefinition);
264 3
265 3
        $this->executeMigrationInner($migration, $migrationDefinition, $migrationContext, 0, $useTransaction, $adminLogin);
266
    }
267 3
268 3
    /**
269 19
     * @param Migration $migration
270
     * @param MigrationDefinition $migrationDefinition
271
     * @param array $migrationContext
272
     * @param int $stepOffset
273
     * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration
274
     * @param string|int|false|null $adminLogin used only for committing db transaction if needed. If false or null, hardcoded admin is used
275
     * @throws \Exception
276
     */
277
    protected function executeMigrationInner(Migration $migration, MigrationDefinition $migrationDefinition,
278
        $migrationContext, $stepOffset = 0, $useTransaction = true, $adminLogin = null)
279
    {
280
        if ($useTransaction) {
281
            $this->repository->beginTransaction();
282
        }
283
284
        $previousUserId = null;
285
        $steps = array_slice($migrationDefinition->steps->getArrayCopy(), $stepOffset);
286
287
        try {
288
289
            $i = $stepOffset+1;
290
            $finalStatus = Migration::STATUS_DONE;
291
            $finalMessage = null;
292
293
            try {
294
295
                foreach ($steps as $step) {
296 3
297
                    $step = $this->injectContextIntoStep($step, $migrationContext);
298
299
                    // we validated the fact that we have a good executor at parsing time
300
                    $executor = $this->executors[$step->type];
301
302
                    $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor);
303
                    $this->dispatcher->dispatch($this->eventPrefix . 'before_execution', $beforeStepExecutionEvent);
304
                    // allow some sneaky trickery here: event listeners can manipulate 'live' the step definition and the executor
305
                    $executor = $beforeStepExecutionEvent->getExecutor();
306
                    $step = $beforeStepExecutionEvent->getStep();
307
308
                    $result = $executor->execute($step);
309
310
                    $this->dispatcher->dispatch($this->eventPrefix . 'step_executed', new StepExecutedEvent($step, $result));
311
312
                    $i++;
313
                }
314
315
            } catch (MigrationAbortedException $e) {
316
                // allow a migration step (or events) to abort the migration via a specific exception
317
318
                $this->dispatcher->dispatch($this->eventPrefix . 'migration_aborted', new MigrationAbortedEvent($step, $e));
319
320
                $finalStatus = $e->getCode();
321
                $finalMessage = "Abort in execution of step $i: " . $e->getMessage();
322
            } catch (MigrationSuspendedException $e) {
323
                // allow a migration step (or events) to suspend the migration via a specific exception
324
325
                $this->dispatcher->dispatch($this->eventPrefix . 'migration_suspended', new MigrationSuspendedEvent($step, $e));
326
327
                // prepare data for the context handler
328
                $this->migrationContext[$migration->name] = array('step' => $i, 'context' => $migrationContext);
329
                // let the context handler store our data, along context data from any other (tagged) service which has some
330
                $this->contextHandler->storeCurrentContext($migration->name);
331
332
                $finalStatus = Migration::STATUS_SUSPENDED;
333
                $finalMessage = "Suspended in execution of step $i: " . $e->getMessage();
334
            }
335
336
            // set migration as done
337
            $this->storageHandler->endMigration(new Migration(
338
                $migration->name,
339
                $migration->md5,
340
                $migration->path,
341
                $migration->executionDate,
342
                $finalStatus,
343
                $finalMessage
344
            ));
345
346
            if ($useTransaction) {
347
                // there might be workflows or other actions happening at commit time that fail if we are not admin
348
                $previousUserId = $this->loginUser($this->getAdminUserIdentifier($adminLogin));
349
350
                $this->repository->commit();
351
                $this->loginUser($previousUserId);
352
            }
353
354
        } catch (\Exception $e) {
355
356
            $errorMessage = $this->getFullExceptionMessage($e) . ' in file ' . $e->getFile() . ' line ' . $e->getLine();
357
            $finalStatus = Migration::STATUS_FAILED;
358
359
            if ($useTransaction) {
360
                try {
361
                    // cater to the case where the $this->repository->commit() call above throws an exception
362
                    if ($previousUserId) {
363
                        $this->loginUser($previousUserId);
364
                    }
365
366
                    // there is no need to become admin here, at least in theory
367
                    $this->repository->rollBack();
368
369
                } catch (\Exception $e2) {
370
                    // This check is not rock-solid, but at the moment is all we can do to tell apart 2 cases of
371
                    // exceptions originating above: the case where the commit was successful but a commit-queue event
372
                    // failed, from the case where something failed beforehand
373
                    if ($previousUserId && $e2->getMessage() == 'There is no active transaction.') {
374
                        // since the migration succeeded and it was committed, no use to mark it as failed...
375
                        $finalStatus = Migration::STATUS_DONE;
376
                        $errorMessage = 'Error post '.$this->getEntityName($migration).' execution: ' . $this->getFullExceptionMessage($e2) .
377
                            ' in file ' . $e2->getFile() . ' line ' . $e2->getLine();
378
                    } else {
379
                        $errorMessage .= '. In addition, an exception was thrown while rolling back: ' .
380
                            $this->getFullExceptionMessage($e2) . ' in file ' . $e2->getFile() . ' line ' . $e2->getLine();
381
                    }
382
                }
383
            }
384
385
            // set migration as failed
386
            // NB: we use the 'force' flag here because we might be catching an exception happened during the call to
387
            // $this->repository->commit() above, in which case the Migration might already be in the DB with a status 'done'
388
            $this->storageHandler->endMigration(
389
                new Migration(
390
                    $migration->name,
391
                    $migration->md5,
392
                    $migration->path,
393
                    $migration->executionDate,
394
                    $finalStatus,
395
                    $errorMessage
396
                ),
397
                true
398
            );
399
400
            throw new MigrationStepExecutionException($errorMessage, $i, $e);
401
        }
402
    }
403
404
    /**
405
     * @param Migration $migration
406
     * @param bool $useTransaction
407
     * @throws \Exception
408
     *
409
     * @todo add support for adminLogin ?
410
     */
411
    public function resumeMigration(Migration $migration, $useTransaction = true)
412
    {
413
        if ($migration->status != Migration::STATUS_SUSPENDED) {
414
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': it is not in suspended status");
415
        }
416
417
        $migrationDefinitions = $this->getMigrationsDefinitions(array($migration->path));
418
        if (!count($migrationDefinitions)) {
419
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': its definition is missing");
420
        }
421
422
        $defs = $migrationDefinitions->getArrayCopy();
423
        $migrationDefinition = reset($defs);
424
425
        $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
426
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
427
            throw new \Exception("Can not resume ".$this->getEntityName($migration)." '{$migration->name}': {$migrationDefinition->parsingError}");
428
        }
429
430
        // restore context
431
        $this->contextHandler->restoreCurrentContext($migration->name);
432
        $restoredContext = $this->migrationContext[$migration->name];
433
434
        /// @todo check that restored context is valid
435
436
        // update migration status
437
        $migration = $this->storageHandler->resumeMigration($migration);
438
439
        // clean up restored context - ideally it should be in the same db transaction as the line above
440
        $this->contextHandler->deleteContext($migration->name);
441
442
        // and go
443
        // note: we store the current step counting starting at 1, but use offset staring at 0, hence the -1 here
444
        $this->executeMigrationInner($migration, $migrationDefinition, $restoredContext['context'],
445
            $restoredContext['step'] - 1, $useTransaction);
446
    }
447
448
    /**
449
     * @param string $defaultLanguageCode
450
     * @param string|int|false $adminLogin
451
     * @return array
452
     */
453
    protected function migrationContextFromParameters($defaultLanguageCode = null, $adminLogin = null)
454
    {
455
        $properties = array();
456
457
        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...
458
            $properties['defaultLanguageCode'] = $defaultLanguageCode;
459
        }
460
        // nb: other parts of the codebase treat differently a false and null values for $properties['adminUserLogin']
461
        if ($adminLogin !== null) {
462
            $properties['adminUserLogin'] = $adminLogin;
463
        }
464
465
        return $properties;
466
    }
467
468
    protected function injectContextIntoStep(MigrationStep $step, array $context)
469
    {
470
        return new MigrationStep(
471
            $step->type,
472
            $step->dsl,
473
            array_merge($step->context, $context)
474
        );
475
    }
476
477
    /**
478
     * @param string $adminLogin
479
     * @return int|string
480
     */
481
    protected function getAdminUserIdentifier($adminLogin)
482
    {
483
        if ($adminLogin != null) {
484
            return $adminLogin;
485
        }
486
487
        return self::ADMIN_USER_ID;
488
    }
489
490
    /**
491
     * Turns eZPublish cryptic exceptions into something more palatable for random devs
492
     * @todo should this be moved to a lower layer ?
493
     *
494
     * @param \Exception $e
495
     * @return string
496
     */
497
    protected function getFullExceptionMessage(\Exception $e)
498
    {
499
        $message = $e->getMessage();
500
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
501
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException') ||
502
            is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')
503
        ) {
504
            if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException')) {
505
                $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...
506
                if ($errorsArray == null) {
507
                    return $message;
508
                }
509
            } else if (is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')) {
510
                $errorsArray = array();
511
                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...
512
                    // we get the 1st language
513
                    $errorsArray[] = reset($limitationError);
514
                }
515
            } else {
516
                $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...
517
            }
518
519
            foreach ($errorsArray as $errors) {
520
                // sometimes error arrays are 2-level deep, sometimes 1...
521
                if (!is_array($errors)) {
522
                    $errors = array($errors);
523
                }
524
                foreach ($errors as $error) {
525
                    /// @todo find out what is the proper eZ way of getting a translated message for these errors
526
                    $translatableMessage = $error->getTranslatableMessage();
527
                    if (is_a($translatableMessage, '\eZ\Publish\API\Repository\Values\Translation\Plural')) {
528
                        $msgText = $translatableMessage->plural;
529
                    } else {
530
                        $msgText = $translatableMessage->message;
531
                    }
532
533
                    $message .= "\n" . $msgText . " - " . var_export($translatableMessage->values, true);
534
                }
535
            }
536
        }
537
538
        while (($e = $e->getPrevious()) != null) {
539
            $message .= "\n" . $e->getMessage();
540
        }
541
542
        return $message;
543
    }
544
545
    /**
546
     * @param string $migrationName
547
     * @return array
548
     */
549
    public function getCurrentContext($migrationName)
550
    {
551
        return isset($this->migrationContext[$migrationName]) ? $this->migrationContext[$migrationName] : null;
552
    }
553
554
    /**
555
     * @param string $migrationName
556
     * @param array $context
557
     */
558
    public function restoreContext($migrationName, array $context)
559
    {
560
        $this->migrationContext[$migrationName] = $context;
561
    }
562
563
    protected function getEntityName($migration)
564
    {
565
        return strtolower(end(explode('\\', get_class($migration))));
0 ignored issues
show
Bug introduced by
explode('\\', get_class($migration)) cannot be passed to end() as the parameter $array expects a reference.
Loading history...
566
    }
567
}
568