Completed
Push — master ( 02d4dc...3be5a1 )
by Gaetano
07:46
created

MigrationService   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 73.12%

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 13
dl 0
loc 338
ccs 98
cts 134
cp 0.7312
rs 8.4864
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A addDefinitionParser() 0 4 1
A addExecutor() 0 6 2
B getMigrationsDefinitions() 0 19 6
A getMigrations() 0 4 1
A getMigration() 0 4 1
A addMigration() 0 4 1
A deleteMigration() 0 4 1
A skipMigration() 0 4 1
A endMigration() 0 4 1
B parseMigrationDefinition() 0 27 5
F executeMigration() 0 114 15
C getFullExceptionMessage() 0 41 12

How to fix   Complexity   

Complex Class

Complex classes like MigrationService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MigrationService, and based on these observations, apply Extract Interface, too.

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