Completed
Push — master ( bc9a97...2c9e04 )
by Gaetano
12:29 queued 07:39
created

MigrationService::setLoader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
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\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 80
    public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository,
66
        EventDispatcherInterface $eventDispatcher, $contextHandler)
67
    {
68 80
        $this->loader = $loader;
69 80
        $this->storageHandler = $storageHandler;
70 80
        $this->repository = $repository;
71 80
        $this->dispatcher = $eventDispatcher;
72 80
        $this->contextHandler = $contextHandler;
73 80
    }
74
75 80
    public function addDefinitionParser(DefinitionParserInterface $DefinitionParser)
76
    {
77 80
        $this->DefinitionParsers[] = $DefinitionParser;
78 80
    }
79
80 80
    public function addExecutor(ExecutorInterface $executor)
81
    {
82 80
        foreach ($executor->supportedTypes() as $type) {
83 80
            $this->executors[$type] = $executor;
84
        }
85 80
    }
86
87
    /**
88
     * @param string $type
89
     * @return ExecutorInterface
90
     * @throws \InvalidArgumentException If executor doesn't exist
91
     */
92 26
    public function getExecutor($type)
93
    {
94 26
        if (!isset($this->executors[$type])) {
95
            throw new \InvalidArgumentException("Executor with type '$type' doesn't exist");
96
        }
97
98 26
        return $this->executors[$type];
99
    }
100
101
    /**
102
     * @return string[]
103
     */
104 25
    public function listExecutors()
105
    {
106 25
        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 78
    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 78
        $handledDefinitions = array();
124 78
        foreach ($this->loader->listAvailableDefinitions($paths) as $migrationName => $definitionPath) {
125 78
            foreach ($this->DefinitionParsers as $definitionParser) {
126 78
                if ($definitionParser->supports($migrationName)) {
127 78
                    $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 78
        if (empty($handledDefinitions) && !empty($paths)) {
134
            return new MigrationDefinitionCollection();
135
        }
136
137 78
        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 79
    public function getMigrations($limit = null, $offset = null)
148
    {
149 79
        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 1
    public function getMigrationsByStatus($status, $limit = null, $offset = null)
161
    {
162 1
        return $this->storageHandler->loadMigrationsByStatus($status, $limit, $offset);
163
    }
164
165
    /**
166
     * @param string $migrationName
167
     * @return Migration|null
168
     */
169 49
    public function getMigration($migrationName)
170
    {
171 49
        return $this->storageHandler->loadMigration($migrationName);
172
    }
173
174
    /**
175
     * @param MigrationDefinition $migrationDefinition
176
     * @return Migration
177
     */
178 48
    public function addMigration(MigrationDefinition $migrationDefinition)
179
    {
180 48
        return $this->storageHandler->addMigration($migrationDefinition);
181
    }
182
183
    /**
184
     * @param Migration $migration
185
     */
186 46
    public function deleteMigration(Migration $migration)
187
    {
188 46
        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 80
    public function parseMigrationDefinition(MigrationDefinition $migrationDefinition)
219
    {
220 80
        foreach ($this->DefinitionParsers as $definitionParser) {
221 80
            if ($definitionParser->supports($migrationDefinition->name)) {
222
                // parse the source file
223 80
                $migrationDefinition = $definitionParser->parseMigrationDefinition($migrationDefinition);
224
225
                // and make sure we know how to handle all steps
226 80
                foreach ($migrationDefinition->steps as $step) {
227 71
                    if (!isset($this->executors[$step->type])) {
228 2
                        return new MigrationDefinition(
229 2
                            $migrationDefinition->name,
230 2
                            $migrationDefinition->path,
231 2
                            $migrationDefinition->rawDefinition,
232 2
                            MigrationDefinition::STATUS_INVALID,
233 2
                            array(),
234 71
                            "Can not handle migration step of type '{$step->type}'"
235
                        );
236
                    }
237
                }
238
239 80
                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 40
    public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true,
257
        $defaultLanguageCode = null, $adminLogin = null)
258
    {
259 40
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
260 2
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
261
        }
262
263 40 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 40
        $migrationContext = $this->migrationContextFromParameters($defaultLanguageCode, $adminLogin);
269
270
        // set migration as begun - has to be in own db transaction
271 40
        $migration = $this->storageHandler->startMigration($migrationDefinition);
272
273 40
        $this->executeMigrationInner($migration, $migrationDefinition, $migrationContext, 0, $useTransaction, $adminLogin);
274 32
    }
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 40
    protected function executeMigrationInner(Migration $migration, MigrationDefinition $migrationDefinition,
286
        $migrationContext, $stepOffset = 0, $useTransaction = true, $adminLogin = null)
287
    {
288 40
        if ($useTransaction) {
289 2
            $this->repository->beginTransaction();
290
        }
291
292 40
        $this->migrationContext[$migration->name] = array('context' => $migrationContext);
293 40
        $previousUserId = null;
294 40
        $steps = array_slice($migrationDefinition->steps->getArrayCopy(), $stepOffset);
295
296
        try {
297
298 40
            $i = $stepOffset+1;
299 40
            $finalStatus = Migration::STATUS_DONE;
300 40
            $finalMessage = null;
301
302
            try {
303
304 40
                foreach ($steps as $step) {
305
                    // save enough data in the context to be able to successfully suspend/resume
306 40
                    $this->migrationContext[$migration->name]['step'] = $i;
307
308 40
                    $step = $this->injectContextIntoStep($step, $migrationContext);
309
310
                    // we validated the fact that we have a good executor at parsing time
311 40
                    $executor = $this->executors[$step->type];
312
313 40
                    $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor);
314 40
                    $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 40
                    $executor = $beforeStepExecutionEvent->getExecutor();
317 40
                    $step = $beforeStepExecutionEvent->getStep();
318
319
                    try {
320 40
                        $result = $executor->execute($step);
321
322 36
                        $this->dispatcher->dispatch($this->eventPrefix . 'step_executed', new StepExecutedEvent($step, $result));
323 13
                    } catch (MigrationStepSkippedException $e) {
324 1
                        continue;
325
                    }
326
327 36
                    $i++;
328
                }
329
330 12
            } catch (MigrationAbortedException $e) {
331
                // allow a migration step (or events) to abort the migration via a specific exception
332
333 3
                $this->dispatcher->dispatch($this->eventPrefix . $this->eventEntity . '_aborted', new MigrationAbortedEvent($step, $e));
334
335 3
                $finalStatus = $e->getCode();
336 3
                $finalMessage = "Abort in execution of step $i: " . $e->getMessage();
337 9
            } catch (MigrationSuspendedException $e) {
338
                // allow a migration step (or events) to suspend the migration via a specific exception
339
340 1
                $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 1
                $this->contextHandler->storeCurrentContext($migration->name);
344
345 1
                $finalStatus = Migration::STATUS_SUSPENDED;
346 1
                $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 32
            $i--;
351
352
            // set migration as done
353 32
            $this->storageHandler->endMigration(new Migration(
354 32
                $migration->name,
355 32
                $migration->md5,
356 32
                $migration->path,
357 32
                $migration->executionDate,
358 32
                $finalStatus,
359 32
                $finalMessage
360
            ));
361
362 32
            if ($useTransaction) {
363
                // there might be workflows or other actions happening at commit time that fail if we are not admin
364 2
                $previousUserId = $this->loginUser($this->getAdminUserIdentifier($adminLogin));
365
366 2
                $this->repository->commit();
367 32
                $this->loginUser($previousUserId);
368
            }
369
370 8
        } catch (\Exception $e) {
371
372 8
            $errorMessage = $this->getFullExceptionMessage($e) . ' in file ' . $e->getFile() . ' line ' . $e->getLine();
373 8
            $finalStatus = Migration::STATUS_FAILED;
374 8
            $exception = null;
375
376 8
            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 8
            $this->storageHandler->endMigration(
408 8
                new Migration(
409 8
                    $migration->name,
410 8
                    $migration->md5,
411 8
                    $migration->path,
412 8
                    $migration->executionDate,
413 8
                    $finalStatus,
414 8
                    $errorMessage
415
                ),
416 8
                true
417
            );
418
419 8
            throw $exception ? $exception : new MigrationStepExecutionException($errorMessage, $i, $e);
420
        }
421 32
    }
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 40
    protected function migrationContextFromParameters($defaultLanguageCode = null, $adminLogin = null)
478
    {
479 40
        $properties = array();
480
481 40
        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 1
            $properties['defaultLanguageCode'] = $defaultLanguageCode;
483
        }
484
        // nb: other parts of the codebase treat differently a false and null values for $properties['adminUserLogin']
485 40
        if ($adminLogin !== null) {
486
            $properties['adminUserLogin'] = $adminLogin;
487
        }
488
489 40
        return $properties;
490
    }
491
492 40
    protected function injectContextIntoStep(MigrationStep $step, array $context)
493
    {
494 40
        return new MigrationStep(
495 40
            $step->type,
496 40
            $step->dsl,
497 40
            array_merge($step->context, $context)
498
        );
499
    }
500
501
    /**
502
     * @param string $adminLogin
503
     * @return int|string
504
     */
505 2
    protected function getAdminUserIdentifier($adminLogin)
506
    {
507 2
        if ($adminLogin != null) {
508
            return $adminLogin;
509
        }
510
511 2
        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 8
    protected function getFullExceptionMessage(\Exception $e)
522
    {
523 8
        $message = $e->getMessage();
524 8
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
525 8
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException') ||
526 8
            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 8
        while (($e = $e->getPrevious()) != null) {
563 1
            $message .= "\n" . $e->getMessage();
564
        }
565
566 8
        return $message;
567
    }
568
569
    /**
570
     * @param string $migrationName
571
     * @return array
572
     */
573 1
    public function getCurrentContext($migrationName)
574
    {
575 1
        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