Completed
Push — master ( 2bd329...4a0c8e )
by Gaetano
18:18 queued 15:28
created

MigrationService::getMigrationsByStatus()   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
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 3
crap 1
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 72
    public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository,
64
        EventDispatcherInterface $eventDispatcher, $contextHandler)
65
    {
66 72
        $this->loader = $loader;
67 72
        $this->storageHandler = $storageHandler;
68 72
        $this->repository = $repository;
69 72
        $this->dispatcher = $eventDispatcher;
70 72
        $this->contextHandler = $contextHandler;
71 72
    }
72
73 72
    public function addDefinitionParser(DefinitionParserInterface $DefinitionParser)
74
    {
75 72
        $this->DefinitionParsers[] = $DefinitionParser;
76 72
    }
77
78 72
    public function addExecutor(ExecutorInterface $executor)
79
    {
80 72
        foreach ($executor->supportedTypes() as $type) {
81 72
            $this->executors[$type] = $executor;
82
        }
83 72
    }
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
    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 70
    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 70
        $handledDefinitions = array();
122 70
        foreach ($this->loader->listAvailableDefinitions($paths) as $migrationName => $definitionPath) {
123 70
            foreach ($this->DefinitionParsers as $definitionParser) {
124 70
                if ($definitionParser->supports($migrationName)) {
125 70
                    $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 70
        if (empty($handledDefinitions) && !empty($paths)) {
132
            return new MigrationDefinitionCollection();
133
        }
134
135 70
        return $this->loader->loadDefinitions($handledDefinitions);
136
    }
137
138
    /**
139
     * Returns the list of all the migrations which where executed or attempted so far
140
     *
141
     * @param int $limit 0 or below will be treated as 'no limit'
142
     * @param int $offset
143
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
144
     */
145 71
    public function getMigrations($limit = null, $offset = null)
146
    {
147 71
        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
     * @param int $status
154
     * @param int $limit 0 or below will be treated as 'no limit'
155
     * @param int $offset
156
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
157
     */
158 1
    public function getMigrationsByStatus($status, $limit = null, $offset = null)
159
    {
160 1
        return $this->storageHandler->loadMigrationsByStatus($status, $limit, $offset);
161
    }
162
163
    /**
164
     * @param string $migrationName
165
     * @return Migration|null
166
     */
167 41
    public function getMigration($migrationName)
168
    {
169 41
        return $this->storageHandler->loadMigration($migrationName);
170
    }
171
172
    /**
173
     * @param MigrationDefinition $migrationDefinition
174
     * @return Migration
175
     */
176 40
    public function addMigration(MigrationDefinition $migrationDefinition)
177
    {
178 40
        return $this->storageHandler->addMigration($migrationDefinition);
179
    }
180
181
    /**
182
     * @param Migration $migration
183
     */
184 38
    public function deleteMigration(Migration $migration)
185
    {
186 38
        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
     *
212
     * @param MigrationDefinition $migrationDefinition
213
     * @return MigrationDefinition
214
     * @throws \Exception if the migrationDefinition has no suitable parser for its source format
215
     */
216 72
    public function parseMigrationDefinition(MigrationDefinition $migrationDefinition)
217
    {
218 72
        foreach ($this->DefinitionParsers as $definitionParser) {
219 72
            if ($definitionParser->supports($migrationDefinition->name)) {
220
                // parse the source file
221 72
                $migrationDefinition = $definitionParser->parseMigrationDefinition($migrationDefinition);
222
223
                // and make sure we know how to handle all steps
224 72
                foreach ($migrationDefinition->steps as $step) {
225 63
                    if (!isset($this->executors[$step->type])) {
226 2
                        return new MigrationDefinition(
227 2
                            $migrationDefinition->name,
228 2
                            $migrationDefinition->path,
229 2
                            $migrationDefinition->rawDefinition,
230 2
                            MigrationDefinition::STATUS_INVALID,
231 2
                            array(),
232 63
                            "Can not handle migration step of type '{$step->type}'"
233
                        );
234
                    }
235
                }
236
237 72
                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
     * @throws \Exception
250
     *
251
     * @todo treating a null and false $adminLogin values differently is prone to hard-to-track errors.
252
     *       Shall we use instead -1 to indicate the desire to not-login-as-admin-user-at-all ?
253
     */
254 32
    public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true,
255
        $defaultLanguageCode = null, $adminLogin = null)
256
    {
257 32
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
258 2
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
259
        }
260
261 32 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
265
        /// @todo add support for setting in $migrationContext a userContentType ?
266 32
        $migrationContext = $this->migrationContextFromParameters($defaultLanguageCode, $adminLogin);
267
268
        // set migration as begun - has to be in own db transaction
269 32
        $migration = $this->storageHandler->startMigration($migrationDefinition);
270
271 32
        $this->executeMigrationInner($migration, $migrationDefinition, $migrationContext, 0, $useTransaction, $adminLogin);
272 28
    }
273
274
    /**
275
     * @param Migration $migration
276
     * @param MigrationDefinition $migrationDefinition
277
     * @param array $migrationContext
278
     * @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
     * @throws \Exception
282
     */
283 32
    protected function executeMigrationInner(Migration $migration, MigrationDefinition $migrationDefinition,
284
        $migrationContext, $stepOffset = 0, $useTransaction = true, $adminLogin = null)
285
    {
286 32
        if ($useTransaction) {
287 2
            $this->repository->beginTransaction();
288
        }
289
290 32
        $this->migrationContext[$migration->name] = array('context' => $migrationContext);
291 32
        $previousUserId = null;
292 32
        $steps = array_slice($migrationDefinition->steps->getArrayCopy(), $stepOffset);
293
294
        try {
295
296 32
            $i = $stepOffset+1;
297 32
            $finalStatus = Migration::STATUS_DONE;
298 32
            $finalMessage = null;
299
300
            try {
301
302 32
                foreach ($steps as $step) {
303
                    // save enough data in the context to be able to successfully suspend/resume
304 32
                    $this->migrationContext[$migration->name]['step'] = $i;
305
306 32
                    $step = $this->injectContextIntoStep($step, $migrationContext);
307
308
                    // we validated the fact that we have a good executor at parsing time
309 32
                    $executor = $this->executors[$step->type];
310
311 32
                    $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor);
312 32
                    $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 32
                    $executor = $beforeStepExecutionEvent->getExecutor();
315 32
                    $step = $beforeStepExecutionEvent->getStep();
316
317 32
                    $result = $executor->execute($step);
318
319 29
                    $this->dispatcher->dispatch($this->eventPrefix . 'step_executed', new StepExecutedEvent($step, $result));
320
321 29
                    $i++;
322
                }
323
324 7
            } catch (MigrationAbortedException $e) {
325
                // allow a migration step (or events) to abort the migration via a specific exception
326
327 2
                $this->dispatcher->dispatch($this->eventPrefix . $this->eventEntity . '_aborted', new MigrationAbortedEvent($step, $e));
328
329 2
                $finalStatus = $e->getCode();
330 2
                $finalMessage = "Abort in execution of step $i: " . $e->getMessage();
331 5
            } catch (MigrationSuspendedException $e) {
332
                // allow a migration step (or events) to suspend the migration via a specific exception
333
334 1
                $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 1
                $this->contextHandler->storeCurrentContext($migration->name);
338
339 1
                $finalStatus = Migration::STATUS_SUSPENDED;
340 1
                $finalMessage = "Suspended in execution of step $i: " . $e->getMessage();
341
            }
342
343
            // set migration as done
344 28
            $this->storageHandler->endMigration(new Migration(
345 28
                $migration->name,
346 28
                $migration->md5,
347 28
                $migration->path,
348 28
                $migration->executionDate,
349 28
                $finalStatus,
350 28
                $finalMessage
351
            ));
352
353 28
            if ($useTransaction) {
354
                // there might be workflows or other actions happening at commit time that fail if we are not admin
355 2
                $previousUserId = $this->loginUser($this->getAdminUserIdentifier($adminLogin));
356
357 2
                $this->repository->commit();
358 28
                $this->loginUser($previousUserId);
359
            }
360
361 4
        } catch (\Exception $e) {
362
363 4
            $errorMessage = $this->getFullExceptionMessage($e) . ' in file ' . $e->getFile() . ' line ' . $e->getLine();
364 4
            $finalStatus = Migration::STATUS_FAILED;
365
366 4
            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 in file ' .
384
                            $e2->getFile() . ' line ' . $e2->getLine() . ': ' . $this->getFullExceptionMessage($e2);
385
                    } else {
386
                        $errorMessage .= '. In addition, an exception was thrown while rolling back, in file ' .
387
                            $e2->getFile() . ' line ' . $e2->getLine() . ': ' . $this->getFullExceptionMessage($e2);
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 4
            $this->storageHandler->endMigration(
396 4
                new Migration(
397 4
                    $migration->name,
398 4
                    $migration->md5,
399 4
                    $migration->path,
400 4
                    $migration->executionDate,
401 4
                    $finalStatus,
402 4
                    $errorMessage
403
                ),
404 4
                true
405
            );
406
407 4
            throw new MigrationStepExecutionException($errorMessage, $i, $e);
408
        }
409 28
    }
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
    /**
461
     * @param string $defaultLanguageCode
462
     * @param string|int|false $adminLogin
463
     * @return array
464
     */
465 32
    protected function migrationContextFromParameters($defaultLanguageCode = null, $adminLogin = null)
466
    {
467 32
        $properties = array();
468
469 32
        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 1
            $properties['defaultLanguageCode'] = $defaultLanguageCode;
471
        }
472
        // nb: other parts of the codebase treat differently a false and null values for $properties['adminUserLogin']
473 32
        if ($adminLogin !== null) {
474
            $properties['adminUserLogin'] = $adminLogin;
475
        }
476
477 32
        return $properties;
478
    }
479
480 32
    protected function injectContextIntoStep(MigrationStep $step, array $context)
481
    {
482 32
        return new MigrationStep(
483 32
            $step->type,
484 32
            $step->dsl,
485 32
            array_merge($step->context, $context)
486
        );
487
    }
488
489
    /**
490
     * @param string $adminLogin
491
     * @return int|string
492
     */
493 2
    protected function getAdminUserIdentifier($adminLogin)
494
    {
495 2
        if ($adminLogin != null) {
496
            return $adminLogin;
497
        }
498
499 2
        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 4
    protected function getFullExceptionMessage(\Exception $e)
510
    {
511 4
        $message = $e->getMessage();
512 4
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
513 4
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException') ||
514 4
            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 4
        while (($e = $e->getPrevious()) != null) {
551 1
            $message .= "\n" . $e->getMessage();
552
        }
553
554 4
        return $message;
555
    }
556
557
    /**
558
     * @param string $migrationName
559
     * @return array
560
     */
561 1
    public function getCurrentContext($migrationName)
562
    {
563 1
        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
        $array = explode('\\', get_class($migration));
578
        return strtolower(preg_replace('/Definition$/', '', end($array)));
579
    }
580
}
581