Completed
Pull Request — master (#60)
by Jérôme
07:11
created

MigrationService::executeMigration()   D

Complexity

Conditions 13
Paths 122

Size

Total Lines 83
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 2 Features 1
Metric Value
c 5
b 2
f 1
dl 0
loc 83
cc 13
eloc 42
nc 122
nop 3
rs 4.6605

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core;
4
5
use Kaliop\eZMigrationBundle\API\Collection\MigrationDefinitionCollection;
6
use Kaliop\eZMigrationBundle\API\LanguageAwareInterface;
7
use Kaliop\eZMigrationBundle\API\StorageHandlerInterface;
8
use Kaliop\eZMigrationBundle\API\LoaderInterface;
9
use Kaliop\eZMigrationBundle\API\DefinitionParserInterface;
10
use Kaliop\eZMigrationBundle\API\ExecutorInterface;
11
use Kaliop\eZMigrationBundle\API\Value\Migration;
12
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
13
use eZ\Publish\API\Repository\Repository;
14
use Kaliop\eZMigrationBundle\API\Exception\MigrationStepExecutionException;
15
16
class MigrationService
17
{
18
    /**
19
     * @var LoaderInterface $loader
20
     */
21
    protected $loader;
22
    /**
23
     * @var StorageHandlerInterface $storageHandler
24
     */
25
    protected $storageHandler;
26
27
    /** @var DefinitionParserInterface[] $DefinitionParsers */
28
    protected $DefinitionParsers = array();
29
30
    /** @var ExecutorInterface[] $executors */
31
    protected $executors = array();
32
33
    protected $repository;
34
35
    public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository)
36
    {
37
        $this->loader = $loader;
38
        $this->storageHandler = $storageHandler;
39
        $this->repository = $repository;
40
    }
41
42
    public function addDefinitionParser(DefinitionParserInterface $DefinitionParser)
43
    {
44
        $this->DefinitionParsers[] = $DefinitionParser;
45
    }
46
47
    public function addExecutor(ExecutorInterface $executor)
48
    {
49
        foreach($executor->supportedTypes() as $type) {
50
            $this->executors[$type] = $executor;
51
        }
52
    }
53
54
    /**
55
     * NB: returns UNPARSED definitions
56
     *
57
     * @param string[] $paths
58
     * @return MigrationDefinitionCollection key: migration name, value: migration definition as binary string
59
     */
60
    public function getMigrationsDefinitions(array $paths = array())
61
    {
62
        // we try to be flexible in file types we support, and the same time avoid loading all files in a directory
63
        $handledDefinitions = array();
64
        foreach($this->loader->listAvailableDefinitions($paths) as $migrationName => $definitionPath) {
65
            foreach($this->DefinitionParsers as $definitionParser) {
66
                if ($definitionParser->supports($migrationName)) {
67
                    $handledDefinitions[] = $definitionPath;
68
                }
69
            }
70
        }
71
72
        // we can not call loadDefinitions with an empty array using the Filesystem loader, or it will start looking in bundles...
73
        if (empty($handledDefinitions) && !empty($paths)) {
74
            return new MigrationDefinitionCollection();
75
        }
76
77
        return $this->loader->loadDefinitions($handledDefinitions);
78
    }
79
80
    /**
81
     * Return the list of all the migrations which where executed or attempted so far
82
     *
83
     * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection
84
     */
85
    public function getMigrations()
86
    {
87
        return $this->storageHandler->loadMigrations();
88
    }
89
90
    /**
91
     * @param string $migrationName
92
     * @return Migration|null
93
     */
94
    public function getMigration($migrationName)
95
    {
96
        return $this->storageHandler->loadMigration($migrationName);
97
    }
98
99
    /**
100
     * @param MigrationDefinition $migrationDefinition
101
     * @return Migration
102
     */
103
    public function addMigration(MigrationDefinition $migrationDefinition)
104
    {
105
        return $this->storageHandler->addMigration($migrationDefinition);
106
    }
107
108
    /**
109
     * @param Migration $migration
110
     */
111
    public function deleteMigration(Migration $migration)
112
    {
113
        return $this->storageHandler->deleteMigration($migration);
114
    }
115
116
    /**
117
     * Parses a migration definition, return a parsed definition.
118
     * If there is a parsing error, the definition status will be updated accordingly
119
     *
120
     * @param MigrationDefinition $migrationDefinition
121
     * @return MigrationDefinition
122
     * @throws \Exception if the migrationDefinition has no suitable parser for its source format
123
     */
124
    public function parseMigrationDefinition(MigrationDefinition $migrationDefinition)
125
    {
126
        foreach($this->DefinitionParsers as $definitionParser) {
127
            if ($definitionParser->supports($migrationDefinition->name)) {
128
                // parse the source file
129
                $migrationDefinition = $definitionParser->parseMigrationDefinition($migrationDefinition);
130
131
                // and make sure we know how to handle all steps
132
                foreach($migrationDefinition->steps as $step) {
133
                    if (!isset($this->executors[$step->type])) {
134
                        return new MigrationDefinition(
135
                            $migrationDefinition->name,
136
                            $migrationDefinition->path,
137
                            $migrationDefinition->rawDefinition,
138
                            MigrationDefinition::STATUS_INVALID,
139
                            array(),
140
                            "Can not handle migration step of type '{$step->type}'"
141
                        );
142
                    }
143
                }
144
145
                return $migrationDefinition;
146
            }
147
        }
148
149
        throw new \Exception("No parser available to parse migration definition '$migrationDefinition'");
150
    }
151
152
    /**
153
     * @param MigrationDefinition $migrationDefinition
154
     * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration
155
     * @throws \Exception
156
     *
157
     * @todo add support for skipped migrations, partially executed migrations
158
     */
159
    public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true, $defaultLanguageCode = null)
160
    {
161
        if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) {
162
            $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition);
163
        }
164
165
        if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
166
            throw new \Exception("Can not execute migration '{$migrationDefinition->name}': {$migrationDefinition->parsingError}");
167
        }
168
169
        // Inject default language code in executors that support it.
170
        if ($defaultLanguageCode) {
171
            foreach ($this->executors as $executor) {
172
                if ($executor instanceof LanguageAwareInterface) {
173
                    $executor->setDefaultLanguageCode($defaultLanguageCode);
174
                }
175
            }
176
        }
177
178
        // set migration as begun - has to be in own db transaction
179
        $migration = $this->storageHandler->startMigration($migrationDefinition);
180
181
        if ($useTransaction) {
182
            $this->repository->beginTransaction();
183
        }
184
        try {
185
186
            $i = 1;
187
188
            foreach($migrationDefinition->steps as $step) {
189
                // we validated the fact that we have a good executor at parsing time
190
                $executor = $this->executors[$step->type];
191
                $executor->execute($step);
192
193
                $i++;
194
            }
195
196
            $status = Migration::STATUS_DONE;
197
198
            // set migration as done
199
            $this->storageHandler->endMigration(new Migration(
200
                $migration->name,
201
                $migration->md5,
202
                $migration->path,
203
                $migration->executionDate,
204
                $status
205
            ));
206
207
            if ($useTransaction) {
208
                try {
209
                    $this->repository->commit();
210
                } catch(\RuntimeException $e) {
211
                    // at present time, the ez5 repo does not support nested commits. So if some migration step has committed
212
                    // already, we get an exception a this point. Extremely poor design, but what can we do ?
213
                    /// @todo log warning
214
                }
215
            }
216
217
        } catch(\Exception $e) {
218
219
            if ($useTransaction) {
220
                try {
221
                    $this->repository->rollBack();
222
                } catch(\RuntimeException $e2) {
223
                    // at present time, the ez5 repo does not support nested commits. So if some migration step has committed
224
                    // already, we get an exception a this point. Extremely poor design, but what can we do ?
225
                    /// @todo log error
226
                }
227
            }
228
229
            /// set migration as failed
230
            $this->storageHandler->endMigration(new Migration(
231
                $migration->name,
232
                $migration->md5,
233
                $migration->path,
234
                $migration->executionDate,
235
                Migration::STATUS_FAILED,
236
                $e->getMessage()
237
            ));
238
239
            throw new MigrationStepExecutionException($this->getFullExceptionMessage($e) . ' in file ' . $e->getFile() . ' line ' . $e->getLine(), $i, $e);
240
        }
241
    }
242
243
    /**
244
     * Turns eZPublish cryptic exceptions into something more palatable for random devs
245
     * @todo should this be moved to a lower layer ?
246
     *
247
     * @param \Exception $e
248
     * @return string
249
     */
250
    protected function getFullExceptionMessage(\Exception $e)
251
    {
252
        $message = $e->getMessage();
253
        if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldDefinitionValidationException') ||
254
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\ContentTypeFieldValidationException') ||
255
            is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException')
256
        ) {
257
            if (is_a($e, '\eZ\Publish\API\Repository\Exceptions\LimitationValidationException')) {
258
                $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...
259
                if ($errorsArray == null) {
260
                    return $message;
261
                }
262
                $errorsArray = array($errorsArray);
263
            } else {
264
                $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...
265
            }
266
267
            foreach ($errorsArray as $errors) {
268
269
                foreach ($errors as $error) {
270
                    /// @todo find out what is the proper eZ way of getting a translated message for these errors
271
                    $translatableMessage = $error->getTranslatableMessage();
272
                    if (is_a($e, 'eZ\Publish\API\Repository\Values\Translation\Plural')) {
273
                        $msgText = $translatableMessage->plural;
274
                    } else {
275
                        $msgText = $translatableMessage->message;
276
                    }
277
278
                    $message .= "\n" . $msgText . " - " . var_export($translatableMessage->values, true);
279
                }
280
            }
281
        }
282
        return $message;
283
    }
284
}
285