Completed
Pull Request — master (#133)
by Damian
20:49 queued 18:16
created

MigrationService::deleteMigration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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