Completed
Push — master ( 1857c8...a46bee )
by Gaetano
07:53
created

FileExecutor::append()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 28
Code Lines 17

Duplication

Lines 16
Ratio 57.14 %

Code Coverage

Tests 6
CRAP Score 13.125

Importance

Changes 0
Metric Value
dl 16
loc 28
ccs 6
cts 12
cp 0.5
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 17
nc 5
nop 2
crap 13.125
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\Executor;
4
5
use Kaliop\eZMigrationBundle\API\Value\MigrationStep;
6
use Kaliop\eZMigrationBundle\Core\ReferenceResolver\PrefixBasedResolverInterface;
7
8
class FileExecutor extends AbstractExecutor
9
{
10
    use IgnorableStepExecutorTrait;
11
12
    protected $supportedStepTypes = array('file');
13
    protected $supportedActions = array('load', 'save', 'copy', 'move', 'delete', 'append', 'prepend', 'exists');
14
15
    /** @var PrefixBasedResolverInterface $referenceResolver */
16
    protected $referenceResolver;
17
18 73
    public function __construct(PrefixBasedResolverInterface $referenceResolver)
19
    {
20 73
        $this->referenceResolver = $referenceResolver;
21 73
    }
22
23
    /**
24
     * @param MigrationStep $step
25
     * @return mixed
26
     * @throws \Exception
27
     */
28 2 View Code Duplication
    public function execute(MigrationStep $step)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
29
    {
30 2
        parent::execute($step);
31
32 2
        if (!isset($step->dsl['mode'])) {
33
            throw new \Exception("Invalid step definition: missing 'mode'");
34
        }
35
36 2
        $action = $step->dsl['mode'];
37
38 2
        if (!in_array($action, $this->supportedActions)) {
39
            throw new \Exception("Invalid step definition: value '$action' is not allowed for 'mode'");
40
        }
41
42 2
        $this->skipStepIfNeeded($step);
43
44 2
        return $this->$action($step->dsl, $step->context);
45
    }
46
47
    /**
48
     * @param array $dsl
49
     * @param array $context
50
     * @return string
51
     * @throws \Exception
52
     */
53 2
    protected function load($dsl, $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
54
    {
55 2
        if (!isset($dsl['file'])) {
56
            throw new \Exception("Can not load file: name missing");
57
        }
58 2
        $fileName = $this->referenceResolver->resolveReference($dsl['file']);
59 2
        if (!file_exists($fileName)) {
60
            throw new \Exception("Can not load '$fileName': file missing");
61
        }
62
63 2
        $this->setReferences($fileName, $dsl);
64
65 2
        return file_get_contents($fileName);
66
    }
67
68
    /**
69
     * @param array $dsl
70
     * @param array $context
71
     * @return string
72
     * @throws \Exception
73
     */
74 1
    protected function exists($dsl, $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
75
    {
76 1
        if (!isset($dsl['file'])) {
77
            throw new \Exception("Can not check for existence of file: name missing");
78
        }
79 1
        $fileName = $this->referenceResolver->resolveReference($dsl['file']);
80
81 1
        $exists = file_exists($fileName);
82
83 1
        if (array_key_exists('references', $dsl)) {
84 1
            foreach ($dsl['references'] as $reference) {
85 1
                switch ($reference['attribute']) {
86 1
                    case 'exists':
87 1
                        $overwrite = false;
88 1
                        if (isset($reference['overwrite'])) {
89 1
                            $overwrite = $reference['overwrite'];
90
                        }
91 1
                        $this->referenceResolver->addReference($reference['identifier'], $exists, $overwrite);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Kaliop\eZMigrationBundle...xBasedResolverInterface as the method addReference() does only exist in the following implementations of said interface: Kaliop\eZMigrationBundle...ver\ChainPrefixResolver, Kaliop\eZMigrationBundle...CustomReferenceResolver.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
92 1
                        break;
93
                }
94
            }
95
        }
96
97 1
        return $exists;
98
    }
99
100
    /**
101
     * @param array $dsl
102
     * @param array $context
103
     * @return int
104
     * @throws \Exception
105
     */
106 1
    protected function save($dsl, $context)
107
    {
108 1 View Code Duplication
        if (!isset($dsl['file']) || (!isset($dsl['body']) && !isset($dsl['template']))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
109
            throw new \Exception("Can not save file: name or body or template missing");
110
        }
111
112 1 View Code Duplication
        if (is_string($dsl['body'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
113 1
            $contents = $this->resolveReferencesInText($dsl['body']);
114
        } elseif (is_string($dsl['template'])) {
115
            $path = $this->referenceResolver->resolveReference($dsl['template']);
116
            // we use the same logic as for the image/file fields in content: look up file 1st relative to the migration
117
            $template = dirname($context['path']) . '/' . $path;
118
            if (!is_file($template)) {
119
                $template = $path;
120 1
            }
121
            $contents = $this->resolveReferencesInText(file_get_contents($template));
122 1
        } else {
123 1
            throw new \Exception("Can not save file: either body or template tag must be a string");
124
        }
125
126
        $fileName = $this->referenceResolver->resolveReference($dsl['file']);
127 1
128
        $overwrite = isset($dsl['overwrite']) ? $overwrite = $dsl['overwrite'] : false;
129 1
        if (!$overwrite && file_exists($fileName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $overwrite of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false 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...
130
            throw new \Exception("Can not save file '$fileName: file already exists");
131 1
        }
132
133
        $return = file_put_contents($fileName, $contents);
134
135
        $this->setReferences($fileName, $dsl);
136
137
        return $return;
138
    }
139
140 1
    /**
141
     * @param array $dsl
142 1
     * @param array $context
143
     * @return int
144
     * @throws \Exception
145
     */
146 1
    protected function append($dsl, $context)
147 1
    {
148 View Code Duplication
        if (!isset($dsl['file']) || (!isset($dsl['body']) && !isset($dsl['template']))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
            throw new \Exception("Can not append to file: name or body or template missing");
150
        }
151
152 View Code Duplication
        if (is_string($dsl['body'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
153
            $contents = $this->resolveReferencesInText($dsl['body']);
154 1
        } elseif (is_string($dsl['template'])) {
155
            $path = $this->referenceResolver->resolveReference($dsl['template']);
156 1
            // we use the same logic as for the image/file fields in content: look up file 1st relative to the migration
157
            $template = dirname($context['path']) . '/' . $path;
158 1
            if (!is_file($template)) {
159
                $template = $path;
160 1
            }
161
            $contents = $this->resolveReferencesInText(file_get_contents($template));
162
        } else {
163
            throw new \Exception("Can not append to file: either body or template tag must be a string");
164
        }
165
166
        $fileName = $this->referenceResolver->resolveReference($dsl['file']);
167
168
        $return = file_put_contents($fileName, $contents, FILE_APPEND);
169
170
        $this->setReferences($fileName, $dsl);
171
172
        return $return;
173
    }
174
175
    /**
176
     * @param array $dsl
177
     * @param array $context
178
     * @return int
179
     * @throws \Exception
180
     */
181
    protected function prepend($dsl, $context)
182
    {
183 View Code Duplication
        if (!isset($dsl['file']) || (!isset($dsl['body']) && !isset($dsl['template']))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
            throw new \Exception("Can not prepend to file: name or body or template missing");
185
        }
186
187 View Code Duplication
        if (is_string($dsl['body'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
188
            $contents = $this->resolveReferencesInText($dsl['body']);
189
        } elseif (is_string($dsl['template'])) {
190
            $path = $this->referenceResolver->resolveReference($dsl['template']);
191
            // we use the same logic as for the image/file fields in content: look up file 1st relative to the migration
192
            $template = dirname($context['path']) . '/' . $path;
193
            if (!is_file($template)) {
194
                $template = $path;
195
            }
196
            $contents = $this->resolveReferencesInText(file_get_contents($template));
197
        } else {
198
            throw new \Exception("Can not append to file: either body or template tag must be a string");
199
        }
200
201
        $fileName = $this->referenceResolver->resolveReference($dsl['file']);
202 1
203
        if (file_exists($fileName)) {
204 1
            $contents .= file_get_contents($fileName);
205
        }
206
207
        $return = file_put_contents($fileName, $contents);
208 1
209 1
        $this->setReferences($fileName, $dsl);
210
211
        return $return;
212
    }
213 1
214
    /**
215 1
     * @param array $dsl
216 1
     * @param array $context
217 1
     * @return true
218
     * @throws \Exception
219
     */
220 View Code Duplication
    protected function copy($dsl, $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221 1
    {
222
        if (!isset($dsl['from']) || !isset($dsl['to'])) {
223
            throw new \Exception("Can not copy file: from or to missing");
224
        }
225 1
226
        $fileName = $this->referenceResolver->resolveReference($dsl['from']);
227
        if (!file_exists($fileName)) {
228
            throw new \Exception("Can not copy file '$fileName': file missing");
229
        }
230
231
        $this->setReferences($fileName, $dsl);
232
233
        $to = $this->referenceResolver->resolveReference($dsl['to']);
234 1
        $overwrite = isset($dsl['overwrite']) ? $overwrite = $dsl['overwrite'] : false;
235
        if (!$overwrite && file_exists($to)) {
236 1
            throw new \Exception("Can not copy file to '$to: file already exists");
237
        }
238
239
        if (!copy($fileName, $to)) {
240 1
            throw new \Exception("Can not copy file '$fileName' to '$to': operation failed");
241 1
        }
242
243
        return true;
244
    }
245 1
246
    /**
247 1
     * @param array $dsl
248 1
     * @param array $context
249 1
     * @return true
250
     * @throws \Exception
251
     */
252 View Code Duplication
    protected function move($dsl, $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
253 1
    {
254
        if (!isset($dsl['from']) || !isset($dsl['to'])) {
255
            throw new \Exception("Can not move file: from or to missing");
256
        }
257 1
258
        $fileName = $this->referenceResolver->resolveReference($dsl['from']);
259
        if (!file_exists($fileName)) {
260
            throw new \Exception("Can not move file '$fileName': file missing");
261
        }
262
263
        $this->setReferences($fileName, $dsl);
264
265
        $to = $this->referenceResolver->resolveReference($dsl['to']);
266 1
        $overwrite = isset($dsl['overwrite']) ? $overwrite = $dsl['overwrite'] : false;
267
        if (!$overwrite && file_exists($to)) {
268 1
            throw new \Exception("Can not move to '$to': file already exists");
269
        }
270
271
        if (!rename($fileName, $to)) {
272 1
            throw new \Exception("Can not move file '$fileName': operation failed");
273 1
        }
274
275
        return true;
276
    }
277 1
278
    /**
279 1
     * @param array $dsl
280
     * @param array $context
281
     * @return true
282
     * @throws \Exception
283 1
     */
284
    protected function delete($dsl, $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
285
    {
286 2
        if (!isset($dsl['file'])) {
287
            throw new \Exception("Can not delete file: name missing");
288 2
        }
289 1
290
        $fileName = $this->referenceResolver->resolveReference($dsl['file']);
291
        if (!file_exists($fileName)) {
292 2
            throw new \Exception("Can not move delete '$fileName': file missing");
293 2
        }
294
295 2
        $this->setReferences($fileName, $dsl);
296
297
        if (!unlink($fileName)) {
298
            throw new \Exception("Can not delete file '$fileName': operation failed");
299 2
        }
300 2
301 2
        return true;
302 2
    }
303 2
304 1
    protected function setReferences($fileName, $dsl)
305 1
    {
306 1
        if (!array_key_exists('references', $dsl)) {
307 1
            return false;
308
        }
309
310 1
        clearstatcache(true, $fileName);
311
        $stats = stat($fileName);
312
313 1
        if (!$stats) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stats of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
314 1
            throw new \Exception("Can not set references for file '$fileName': stat failed");
315 1
        }
316 1
317
        foreach ($dsl['references'] as $reference) {
318
            switch ($reference['attribute']) {
319 1
                case 'body':
320 1
                    $value = file_get_contents($fileName);
321 1
                    break;
322
                case 'size':
323
                    $value = $stats[7];
324
                    break;
325
                case 'uid':
326 2
                    $value = $stats[4];
327 2
                    break;
328
                case 'gid':
329
                    $value = $stats[5];
330 2
                    break;
331
                case 'atime':
332
                    $value = $stats[8];
333 2
                    break;
334
                case 'mtime':
335
                    $value = $stats[9];
336
                    break;
337
                case 'ctime':
338
                    $value = $stats[10];
339
                    break;
340
                default:
341
                    throw new \InvalidArgumentException('File executor does not support setting references for attribute ' . $reference['attribute']);
342 1
            }
343
344
            $overwrite = false;
345 1
            if (isset($reference['overwrite'])) {
346
                $overwrite = $reference['overwrite'];
347 1
            }
348
            $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Kaliop\eZMigrationBundle...xBasedResolverInterface as the method addReference() does only exist in the following implementations of said interface: Kaliop\eZMigrationBundle...ver\ChainPrefixResolver, Kaliop\eZMigrationBundle...CustomReferenceResolver.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
349 1
        }
350
351 1
        return true;
352 1
    }
353 1
354 1
    /**
355
     * Replaces any references inside a string
356
     *
357
     * @param string $text
358 1
     * @return string
359
     */
360 View Code Duplication
    protected function resolveReferencesInText($text)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
361
    {
362
        // we need to alter the regexp we get from the resolver, as it will be used to match parts of text, not the whole string
363
        $regexp = substr($this->referenceResolver->getRegexp(), 1, -1);
364
        // NB: here we assume that all regexp resolvers give us a regexp with a very specific format...
365
        $regexp = '/\[' . preg_replace(array('/^\^/'), array('', ''), $regexp) . '[^]]+\]/';
366
367
        $count = preg_match_all($regexp, $text, $matches);
368
        // $matches[0][] will have the matched full string eg.: [reference:example_reference]
369
        if ($count) {
370
            foreach ($matches[0] as $referenceIdentifier) {
371
                $reference = $this->referenceResolver->getReferenceValue(substr($referenceIdentifier, 1, -1));
372
                $text = str_replace($referenceIdentifier, $reference, $text);
373
            }
374
        }
375
376
        return $text;
377
    }
378
}
379