Completed
Pull Request — master (#106)
by
unknown
13:36 queued 03:36
created

MigrationService::getMigrations()   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
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core;
4
5
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
6
use eZ\Publish\API\Repository\Repository;
7
use Kaliop\eZMigrationBundle\API\Collection\MigrationDefinitionCollection;
8
use Kaliop\eZMigrationBundle\API\LanguageAwareInterface;
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\Value\Migration;
14
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
15
use Kaliop\eZMigrationBundle\API\Exception\MigrationStepExecutionException;
16
use Kaliop\eZMigrationBundle\API\Exception\MigrationAbortedException;
17
use Kaliop\eZMigrationBundle\API\Event\BeforeStepExecutionEvent;
18
use Kaliop\eZMigrationBundle\API\Event\StepExecutedEvent;
19
use Kaliop\eZMigrationBundle\API\Event\MigrationAbortedEvent;
20
21
class MigrationService
22
{
23
    use RepositoryUserSetterTrait;
24
25
    /**
26
     * Constant defining the default Admin user ID.
27
     * @todo inject via config parameter
28
     */
29
    const ADMIN_USER_ID = 14;
30
31
    /**
32
     * @var LoaderInterface $loader
33
     */
34
    protected $loader;
35
    /**
36
     * @var StorageHandlerInterface $storageHandler
37
     */
38
    protected $storageHandler;
39
40 20
    /** @var DefinitionParserInterface[] $DefinitionParsers */
41 5
    protected $DefinitionParsers = array();
42 20
43 20
    /** @var ExecutorInterface[] $executors */
44 20
    protected $executors = array();
45 20
46 20
    protected $repository;
47
48 20
    protected $dispatcher;
49
50 20
    protected $eventPrefix = 'ez_migration.';
51 20
52
    public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository, EventDispatcherInterface $eventDispatcher)
53 20
    {
54
        $this->loader = $loader;
55 20
        $this->storageHandler = $storageHandler;
56 20
        $this->repository = $repository;
57 20
        $this->dispatcher = $eventDispatcher;
58 20
    }
59
60
    public function addDefinitionParser(DefinitionParserInterface $DefinitionParser)
61
    {
62
        $this->DefinitionParsers[] = $DefinitionParser;
63
    }
64
65
    public function addExecutor(ExecutorInterface $executor)
66 20
    {
67
        foreach ($executor->supportedTypes() as $type) {
68
            $this->executors[$type] = $executor;
69 20
        }
70 20
    }
71 20
72 20
    /**
73 20
     * @param string $type
74 20
     * @return ExecutorInterface
75 20
     * @throws \InvalidArgumentException If executor doesn't exist
76 20
     */
77
    public function getExecutor($type)
78
    {
79 20
        if (!isset($this->executors[$type])) {
80
            throw new \InvalidArgumentException("Executor with type '$type' doesn't exist");
81
        }
82
83 20
        return $this->executors[$type];
84 2
    }
85
86
    /**
87
     * @return string[]
88
     */
89
    public function listExecutors()
90
    {
91 20
        return array_keys($this->executors);
92
    }
93 20
94
    /**
95
     * NB: returns UNPARSED definitions
96
     *
97
     * @param string[] $paths
98
     * @return MigrationDefinitionCollection key: migration name, value: migration definition as binary string
99
     */
100 19
    public function getMigrationsDefinitions(array $paths = array())
101
    {
102 19
        // we try to be flexible in file types we support, and the same time avoid loading all files in a directory
103
        $handledDefinitions = array();
104
        foreach ($this->loader->listAvailableDefinitions($paths) as $migrationName => $definitionPath) {
105
            foreach ($this->DefinitionParsers as $definitionParser) {
106
                if ($definitionParser->supports($migrationName)) {
107
                    $handledDefinitions[] = $definitionPath;
108
                }
109 19
            }
110
        }
111 19
112 19
        // we can not call loadDefinitions with an empty array using the Filesystem loader, or it will start looking in bundles...
113
        if (empty($handledDefinitions) && !empty($paths)) {
114
            return new MigrationDefinitionCollection();
115
        }
116
117 19
        return $this->loader->loadDefinitions($handledDefinitions);
118
    }
119 19
120
    /**
121
     * Returns the list of all the migrations which where executed or attempted so far
122
     *
123
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
124
     */
125
    public function getMigrations()
126
    {
127
        return $this->storageHandler->loadMigrations();
128
    }
129
130 20
    /**
131
     * @param string $migrationName
132 20
     * @return Migration|null
133 20
     */
134
    public function getMigration($migrationName)
135 20
    {
136
        return $this->storageHandler->loadMigration($migrationName);
137
    }
138 20
139 13
    /**
140 1
     * @param MigrationDefinition $migrationDefinition
141 1
     * @return Migration
142 1
     */
143 1
    public function addMigration(MigrationDefinition $migrationDefinition)
144 1
    {
145 1
        return $this->storageHandler->addMigration($migrationDefinition);
146 1
    }
147 1
148
    /**
149 20
     * @param Migration $migration
150
     */
151 20
    public function deleteMigration(Migration $migration)
152 12
    {
153 15
        return $this->storageHandler->deleteMigration($migration);
154
    }
155
156
    /**
157
     * @param MigrationDefinition $migrationDefinition
158
     * @return Migration
159
     */
160
    public function skipMigration(MigrationDefinition $migrationDefinition)
161
    {
162
        return $this->storageHandler->skipMigration($migrationDefinition);
163
    }
164
165
    /**
166 12
     * Not be called by external users for normal use cases, you should use executeMigration() instead
167
     *
168 12
     * @param Migration $migration
169
     */
170
    public function endMigration(Migration $migration)
171
    {
172 12
        return $this->storageHandler->endMigration($migration);
173 12
    }
174
175
    /**
176
     * Parses a migration definition, return a parsed definition.
177 12
     * If there is a parsing error, the definition status will be updated accordingly
178
     *
179
     * @param MigrationDefinition $migrationDefinition
180 12
     * @return MigrationDefinition
181
     * @throws \Exception if the migrationDefinition has no suitable parser for its source format
182
     */
183
    public function parseMigrationDefinition(MigrationDefinition $migrationDefinition)
184
    {
185
        foreach ($this->DefinitionParsers as $definitionParser) {
186 12
            if ($definitionParser->supports($migrationDefinition->name)) {
187
                // parse the source file
188 12
                $migrationDefinition = $definitionParser->parseMigrationDefinition($migrationDefinition);
189
190
                // and make sure we know how to handle all steps
191
                foreach ($migrationDefinition->steps as $step) {
192
                    if (!isset($this->executors[$step->type])) {
193 12
                        return new MigrationDefinition(
194
                            $migrationDefinition->name,
195 12
                            $migrationDefinition->path,
196
                            $migrationDefinition->rawDefinition,
197 12
                            MigrationDefinition::STATUS_INVALID,
198
                            array(),
199 12
                            "Can not handle migration step of type '{$step->type}'"
200
                        );
201 12
                    }
202
                }
203 10
204
                return $migrationDefinition;
205 10
            }
206 10
        }
207
208 9
        throw new \Exception("No parser available to parse migration definition '{$migrationDefinition->name}'");
209
    }
210
211 12
    /**
212 9
     * @param MigrationDefinition $migrationDefinition
213 9
     * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration
214 9
     * @param string $defaultLanguageCode
215 9
     * @throws \Exception
216
     *
217 9
     * @todo add support for skipped migrations, partially executed migrations
218
     */
219 9
    public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true, $defaultLanguageCode = null)
220
    {
221
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
222
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
223
        }
224
225
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
226
            throw new \Exception("Can not execute migration '{$migrationDefinition->name}': {$migrationDefinition->parsingError}");
227
        }
228
229 12
        // Inject default language code in executors that support it.
230
        if ($defaultLanguageCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $defaultLanguageCode of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
231 12
            foreach ($this->executors as $executor) {
232
                if ($executor instanceof LanguageAwareInterface) {
233
                    $executor->setDefaultLanguageCode($defaultLanguageCode);
234
                }
235
            }
236
        }
237
238
        // set migration as begun - has to be in own db transaction
239
        $migration = $this->storageHandler->startMigration($migrationDefinition);
240
241
        if ($useTransaction) {
242 3
            $this->repository->beginTransaction();
243 3
        }
244 3
245 3
        $previousUserId = null;
246 3
247 3
        try {
248 3
249 3
            $i = 1;
250
            $finalStatus = Migration::STATUS_DONE;
251 12
            $finalMessage = null;
252 12
253 12
            try {
254
255
                foreach ($migrationDefinition->steps as $step) {
256
                    // we validated the fact that we have a good executor at parsing time
257
                    $executor = $this->executors[$step->type];
258
259
                    $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor);
260
                    $this->dispatcher->dispatch($this->eventPrefix . 'before_execution', $beforeStepExecutionEvent);
261
                    // allow some sneaky trickery here: event listeners can manipulate 'live' the step definition and the executor
262 19
                    $executor = $beforeStepExecutionEvent->getExecutor();
263
                    $step = $beforeStepExecutionEvent->getStep();
264 3
265 3
                    $result = $executor->execute($step);
266
267 3
                    $this->dispatcher->dispatch($this->eventPrefix . 'step_executed', new StepExecutedEvent($step, $result));
268 3
269 19
                    $i++;
270
                }
271
272
            } catch (MigrationAbortedException $e) {
273
                // allow a migration step (or events) to abort the migration via a specific exception
274
275
                $this->dispatcher->dispatch($this->eventPrefix . 'migration_aborted', new MigrationAbortedEvent($step, $e));
276
277
                $finalStatus = $e->getCode();
278
                $finalMessage = "Abort in execution of step $i: " . $e->getMessage();
279
            }
280
281
            // set migration as done
282
            $this->storageHandler->endMigration(new Migration(
283
                $migration->name,
284
                $migration->md5,
285
                $migration->path,
286
                $migration->executionDate,
287
                $finalStatus,
288
                $finalMessage
289
            ));
290
291
            if ($useTransaction) {
292
                // there might be workflows or other actions happening at commit time that fail if we are not admin
293
                $previousUserId = $this->loginUser(self::ADMIN_USER_ID);
294
                $this->repository->commit();
295
                $this->loginUser($previousUserId);
296 3
            }
297
298
        } catch (\Exception $e) {
299
300
            $errorMessage = $this->getFullExceptionMessage($e) . ' in file ' . $e->getFile() . ' line ' . $e->getLine();
301
            $finalStatus = Migration::STATUS_FAILED;
302
303
            if ($useTransaction) {
304
                try {
305
                    // cater to the case where the $this->repository->commit() call above throws an exception
306
                    if ($previousUserId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $previousUserId of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
307
                        $this->loginUser($previousUserId);
308
                    }
309
310
                    // there is no need to become admin here, at least in theory
311
                    $this->repository->rollBack();
312
313
                } catch (\Exception $e2) {
314
                    // This check is not rock-solid, but at the moment is all we can do to tell apart 2 cases of
315
                    // exceptions originating above: the case where the commit was successful but a commit-queue event
316
                    // failed, from the case where something failed beforehand
317
                    if ($previousUserId && $e2->getMessage() == 'There is no active transaction.') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $previousUserId of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
318
                        // since the migration succeeded and it was committed, no use to mark it as failed...
319
                        $finalStatus = Migration::STATUS_DONE;
320
                        $errorMessage = 'Error post migration execution: ' . $this->getFullExceptionMessage($e2) .
321
                            ' in file ' . $e2->getFile() . ' line ' . $e2->getLine();
322
                    } else {
323
                        $errorMessage .= '. In addition, an exception was thrown while rolling back: ' .
324
                            $this->getFullExceptionMessage($e2) . ' in file ' . $e2->getFile() . ' line ' . $e2->getLine();
325
                    }
326
                }
327
            }
328
329
            // set migration as failed
330
            // NB: we use the 'force' flag here because we might be catching an exception happened during the call to
331
            // $this->repository->commit() above, in which case the Migration might already be in the DB with a status 'done'
332
            $this->storageHandler->endMigration(
333
                new Migration(
334
                    $migration->name,
335
                    $migration->md5,
336
                    $migration->path,
337
                    $migration->executionDate,
338
                    $finalStatus,
339
                    $errorMessage
340
                ),
341
                true
342
            );
343
344
            throw $e;
345
        }
346
    }
347
348
    /**
349
     * Turns eZPublish cryptic exceptions into something more palatable for random devs
350
     * @todo should this be moved to a lower layer ?
351
     *
352
     * @param \Exception $e
353
     * @return string
354
     */
355
    protected function getFullExceptionMessage(\Exception $e)
356
    {
357
        $message = $e->getMessage();
358
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
359
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException') ||
360
            is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')
361
        ) {
362
            if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException')) {
363
                $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...
364
                if ($errorsArray == null) {
365
                    return $message;
366
                }
367
            } else if (is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')) {
368
                $errorsArray = array();
369
                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...
370
                    // we get the 1st language
371
                    $errorsArray[] = reset($limitationError);
372
                }
373
            } else {
374
                $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...
375
            }
376
377
            foreach ($errorsArray as $errors) {
378
                // sometimes error arrays are 2-level deep, sometimes 1...
379
                if (!is_array($errors)) {
380
                    $errors = array($errors);
381
                }
382
                foreach ($errors as $error) {
383
                    /// @todo find out what is the proper eZ way of getting a translated message for these errors
384
                    $translatableMessage = $error->getTranslatableMessage();
385
                    if (is_a($translatableMessage, '\eZ\Publish\API\Repository\Values\Translation\Plural')) {
386
                        $msgText = $translatableMessage->plural;
387
                    } else {
388
                        $msgText = $translatableMessage->message;
389
                    }
390
391
                    $message .= "\n" . $msgText . " - " . var_export($translatableMessage->values, true);
392
                }
393
            }
394
        }
395
396
        while (($e = $e->getPrevious()) != null) {
397
            $message .= "\n" . $e->getMessage();
398
        }
399
400
        return $message;
401
    }
402
}
403