Completed
Push — master ( e7f7b5...2ae0e9 )
by Gaetano
06:53
created

MigrationService::getFullExceptionMessage()   C

Complexity

Conditions 8
Paths 3

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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