Completed
Push — master ( c34430...1ff666 )
by Gaetano
07:53
created

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