Completed
Pull Request — master (#91)
by
unknown
06:19
created

MigrationService::getMigrationsDefinitions()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.0208

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
ccs 11
cts 12
cp 0.9167
rs 8.8571
cc 6
eloc 9
nc 8
nop 1
crap 6.0208
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\Event\BeforeStepExecutionEvent;
17
use Kaliop\eZMigrationBundle\API\Event\StepExecutedEvent;
18
19
class MigrationService
20
{
21
    use RepositoryUserSetterTrait;
22
23
    /**
24
     * Constant defining the default Admin user ID.
25
     * @todo inject via config parameter
26
     */
27
    const ADMIN_USER_ID = 14;
28
29
    /**
30
     * @var LoaderInterface $loader
31
     */
32
    protected $loader;
33
    /**
34
     * @var StorageHandlerInterface $storageHandler
35
     */
36
    protected $storageHandler;
37
38
    /** @var DefinitionParserInterface[] $DefinitionParsers */
39
    protected $DefinitionParsers = array();
40
41
    /** @var ExecutorInterface[] $executors */
42
    protected $executors = array();
43
44
    protected $repository;
45
46
    protected $dispatcher;
47
48 27
    public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository, EventDispatcherInterface $eventDispatcher)
49
    {
50 27
        $this->loader = $loader;
51 27
        $this->storageHandler = $storageHandler;
52 27
        $this->repository = $repository;
53 27
        $this->dispatcher = $eventDispatcher;
54 27
    }
55
56 27
    public function addDefinitionParser(DefinitionParserInterface $DefinitionParser)
57 3
    {
58 27
        $this->DefinitionParsers[] = $DefinitionParser;
59 27
    }
60
61 27
    public function addExecutor(ExecutorInterface $executor)
62
    {
63 27
        foreach($executor->supportedTypes() as $type) {
64 27
            $this->executors[$type] = $executor;
65 27
        }
66 27
    }
67
68
    /**
69
     * @param $type string
70
     * @return ExecutorInterface
71
     * @throws \InvalidArgumentException If executor doesn't exist
72
     */
73 5
    public function getExecutor($type)
74 4
    {
75 5
        if (!isset($this->executors[$type])) {
76
            throw new \InvalidArgumentException("Executor with type '$type' doesn't exist");
77
        }
78
79 1
        return $this->executors[$type];
80
    }
81
82
    /**
83
     * NB: returns UNPARSED definitions
84
     *
85
     * @param string[] $paths
86
     * @return MigrationDefinitionCollection key: migration name, value: migration definition as binary string
87
     */
88 27
    public function getMigrationsDefinitions(array $paths = array())
89
    {
90
        // we try to be flexible in file types we support, and the same time avoid loading all files in a directory
91 27
        $handledDefinitions = array();
92 27
        foreach($this->loader->listAvailableDefinitions($paths) as $migrationName => $definitionPath) {
93 27
            foreach($this->DefinitionParsers as $definitionParser) {
94 27
                if ($definitionParser->supports($migrationName)) {
95 27
                    $handledDefinitions[] = $definitionPath;
96 27
                }
97 27
            }
98 27
        }
99
100
        // we can not call loadDefinitions with an empty array using the Filesystem loader, or it will start looking in bundles...
101 27
        if (empty($handledDefinitions) && !empty($paths)) {
102
            return new MigrationDefinitionCollection();
103
        }
104
105 27
        return $this->loader->loadDefinitions($handledDefinitions);
106
    }
107
108
    /**
109
     * Returns the list of all the migrations which where executed or attempted so far
110
     *
111
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
112
     */
113 27
    public function getMigrations()
114
    {
115 27
        return $this->storageHandler->loadMigrations();
116
    }
117
118
    /**
119
     * @param string $migrationName
120
     * @return Migration|null
121
     */
122 26
    public function getMigration($migrationName)
123
    {
124 26
        return $this->storageHandler->loadMigration($migrationName);
125
    }
126
127
    /**
128
     * @param MigrationDefinition $migrationDefinition
129
     * @return Migration
130
     */
131 26
    public function addMigration(MigrationDefinition $migrationDefinition)
132
    {
133 26
        return $this->storageHandler->addMigration($migrationDefinition);
134
    }
135
136
    /**
137
     * @param Migration $migration
138
     */
139 26
    public function deleteMigration(Migration $migration)
140
    {
141 26
        return $this->storageHandler->deleteMigration($migration);
142
    }
143
144
    /**
145
     * @param MigrationDefinition $migrationDefinition
146
     * @return Migration
147
     */
148
    public function skipMigration(MigrationDefinition $migrationDefinition)
149
    {
150
        return $this->storageHandler->skipMigration($migrationDefinition);
151
    }
152
153
    /**
154
     * Not be called by external users for normal use cases, you should use executeMigration() instead
155
     *
156
     * @param Migration $migration
157
     */
158
    public function endMigration(Migration $migration)
159
    {
160
        return $this->storageHandler->endMigration($migration);
161
    }
162
163
    /**
164
     * Parses a migration definition, return a parsed definition.
165
     * If there is a parsing error, the definition status will be updated accordingly
166
     *
167
     * @param MigrationDefinition $migrationDefinition
168
     * @return MigrationDefinition
169
     * @throws \Exception if the migrationDefinition has no suitable parser for its source format
170
     */
171 27
    public function parseMigrationDefinition(MigrationDefinition $migrationDefinition)
172
    {
173 27
        foreach($this->DefinitionParsers as $definitionParser) {
174 27
            if ($definitionParser->supports($migrationDefinition->name)) {
175
                // parse the source file
176 27
                $migrationDefinition = $definitionParser->parseMigrationDefinition($migrationDefinition);
177
178
                // and make sure we know how to handle all steps
179 27
                foreach($migrationDefinition->steps as $step) {
180 18
                    if (!isset($this->executors[$step->type])) {
181 1
                        return new MigrationDefinition(
182 1
                            $migrationDefinition->name,
183 1
                            $migrationDefinition->path,
184 1
                            $migrationDefinition->rawDefinition,
185 1
                            MigrationDefinition::STATUS_INVALID,
186 1
                            array(),
187 1
                            "Can not handle migration step of type '{$step->type}'"
188 1
                        );
189
                    }
190 27
                }
191
192 27
                return $migrationDefinition;
193
            }
194 12
        }
195
196
        throw new \Exception("No parser available to parse migration definition '$migrationDefinition'");
197 17
    }
198
199
    /**
200
     * @param MigrationDefinition $migrationDefinition
201
     * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration
202
     * @param string $defaultLanguageCode
203
     * @throws \Exception
204
     *
205
     * @todo add support for skipped migrations, partially executed migrations
206
     */
207 26
    public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true, $defaultLanguageCode = null)
208
    {
209 17
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
210
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
211
        }
212
213 17
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
214
            throw new \Exception("Can not execute migration '{$migrationDefinition->name}': {$migrationDefinition->parsingError}");
215 26
        }
216
217
        // Inject default language code in executors that support it.
218 17
        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...
219
            foreach ($this->executors as $executor) {
220
                if ($executor instanceof LanguageAwareInterface) {
221
                    $executor->setDefaultLanguageCode($defaultLanguageCode);
222
                }
223
            }
224
        }
225
226
        // set migration as begun - has to be in own db transaction
227 17
        $migration = $this->storageHandler->startMigration($migrationDefinition);
228
229 17
        if ($useTransaction) {
230
            $this->repository->beginTransaction();
231
        }
232
233 17
        $previousUserId = null;
234
235
        try {
236
237 17
            $i = 1;
238
239 17
            foreach($migrationDefinition->steps as $step) {
240
                // we validated the fact that we have a good executor at parsing time
241 17
                $executor = $this->executors[$step->type];
242
243 17
                $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor);
244 17
                $this->dispatcher->dispatch('ez_migration.before_execution', $beforeStepExecutionEvent);
245
                // allow some sneaky trickery here: event listeners can manipulate 'live' the step definition and the executor
246 17
                $executor = $beforeStepExecutionEvent->getExecutor();
247 17
                $step = $beforeStepExecutionEvent->getStep();
248
249 17
                $result = $executor->execute($step);
250
251 15
                $this->dispatcher->dispatch('ez_migration.step_executed', new StepExecutedEvent($step, $result));
252
253 15
                $i++;
254 15
            }
255
256
            // set migration as done
257 14
            $this->storageHandler->endMigration(new Migration(
258 14
                $migration->name,
259 14
                $migration->md5,
260 14
                $migration->path,
261 14
                $migration->executionDate,
262
                Migration::STATUS_DONE
263 14
            ));
264
265 14
            if ($useTransaction) {
266
                // there might be workflows or other actions happening at commit time that fail if we are not admin
267
                $previousUserId = $this->loginUser(self::ADMIN_USER_ID);
268
                $this->repository->commit();
269
                $this->loginUser($previousUserId);
270
            }
271
272 17
        } catch(\Exception $e) {
273
274 3
            $errorMessage = $this->getFullExceptionMessage($e) . ' in file ' . $e->getFile() . ' line ' . $e->getLine();
275 3
            $finalStatus = Migration::STATUS_FAILED;
276
277 3
            if ($useTransaction) {
278
                try {
279
                    // cater to the case where the $this->repository->commit() call above throws an exception
280
                    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...
281
                        $this->loginUser($previousUserId);
282
                    }
283
284
                    // there is no need to become admin here, at least in theory
285
                    $this->repository->rollBack();
286
287
                } catch(\Exception $e2) {
288
                    // This check is not rock-solid, but at the moment is all we can do to tell apart 2 cases of
289
                    // exceptions originating above: the case where the commit was successful but a commit-queue event
290
                    // failed, from the case where something failed beforehand
291
                    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...
292
                        // since the migration succeeded and it was committed, no use to mark it as failed...
293
                        $finalStatus = Migration::STATUS_DONE;
294
                        $errorMessage = 'Error post migration execution: ' . $this->getFullExceptionMessage($e2) .
295
                            ' in file ' . $e2->getFile() . ' line ' . $e2->getLine();
296
                    } else {
297
                        $errorMessage .= '. In addition, an exception was thrown while rolling back: ' .
298
                            $this->getFullExceptionMessage($e2) . ' in file ' . $e2->getFile() . ' line ' . $e2->getLine();
299
                    }
300
                }
301
            }
302
303
            // set migration as failed
304
            // NB: we use the 'force' flag here because we might be catching an exception happened during the call to
305
            // $this->repository->commit() above, in which case the Migration might already be in the DB with a status 'done'
306 3
            $this->storageHandler->endMigration(
307 3
                new Migration(
308 3
                    $migration->name,
309 3
                    $migration->md5,
310 3
                    $migration->path,
311 3
                    $migration->executionDate,
312 3
                    $finalStatus,
313
                    $errorMessage
314 3
                ),
315
                true
316 3
            );
317
318 3
            throw new MigrationStepExecutionException($errorMessage, $i, $e);
319
        }
320 14
    }
321
322
    /**
323
     * Turns eZPublish cryptic exceptions into something more palatable for random devs
324
     * @todo should this be moved to a lower layer ?
325
     *
326
     * @param \Exception $e
327
     * @return string
328
     */
329 3
    protected function getFullExceptionMessage(\Exception $e)
330
    {
331 3
        $message = $e->getMessage();
332 3
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
333 3
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException') ||
334 3
            is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')
335 3
        ) {
336
            if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException')) {
337
                $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...
338
                if ($errorsArray == null) {
339
                    return $message;
340
                }
341
            } else if (is_a($e, '\eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException')) {
342
                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...
343
                    // we get the 1st language
344
                    $errorsArray[] = reset($limitationError);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$errorsArray was never initialized. Although not strictly required by PHP, it is generally a good practice to add $errorsArray = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
345
                }
346
            } else {
347
                $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...
348
            }
349
350
            foreach ($errorsArray as $errors) {
0 ignored issues
show
Bug introduced by
The variable $errorsArray does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
351
                // sometimes error arrays are 2-level deep, sometimes 1...
352
                if (!is_array($errors)) {
353
                    $errors = array($errors);
354
                }
355
                foreach ($errors as $error) {
356
                    /// @todo find out what is the proper eZ way of getting a translated message for these errors
357
                    $translatableMessage = $error->getTranslatableMessage();
358
                    if (is_a($translatableMessage, '\eZ\Publish\API\Repository\Values\Translation\Plural')) {
359
                        $msgText = $translatableMessage->plural;
360
                    } else {
361
                        $msgText = $translatableMessage->message;
362
                    }
363
364
                    $message .= "\n" . $msgText . " - " . var_export($translatableMessage->values, true);
365
                }
366
            }
367
        }
368
369 3
        while (($e = $e->getPrevious()) != null) {
370 1
            $message .= "\n" . $e->getMessage();
371 1
        }
372
373 3
        return $message;
374
    }
375
}
376