Completed
Push — master ( f1e9d1...b1108a )
by Nicolas
03:12
created

Hydrator::collectNonDistFiles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Karma;
4
5
use Gaufrette\Filesystem;
6
use Psr\Log\NullLogger;
7
use Karma\FormatterProviders\NullProvider;
8
9
class Hydrator implements ConfigurableProcessor
10
{
11
    use \Karma\Logging\LoggerAware;
12
13
    const
14
        TODO_VALUE = '__TODO__',
15
        FIXME_VALUE = '__FIXME__',
16
        VARIABLE_REGEX = '~<%(?P<variableName>[A-Za-z0-9_\.\-]+)%>~';
17
18
    private
19
        $sources,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
20
        $suffix,
21
        $reader,
22
        $dryRun,
23
        $enableBackup,
24
        $finder,
25
        $formatterProvider,
26
        $currentFormatterName,
27
        $currentTargetFile,
28
        $systemEnvironment,
29
        $unusedVariables,
30
        $unvaluedVariables,
31
        $target,
32
        $nonDistFilesOverwriteAllowed,
33
        $hydratedFiles;
34
35 89
    public function __construct(Filesystem $sources, Filesystem $target, Configuration $reader, Finder $finder, FormatterProvider $formatterProvider = null)
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 150 characters; contains 156 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
36
    {
37 89
        $this->logger = new NullLogger();
38
39 89
        $this->sources = $sources;
40 89
        $this->target = $target;
41 89
        $this->reader = $reader;
42 89
        $this->finder = $finder;
43
44 89
        $this->suffix = Application::DEFAULT_DISTFILE_SUFFIX;
45 89
        $this->dryRun = false;
46 89
        $this->enableBackup = false;
47
48 89
        $this->formatterProvider = $formatterProvider;
49 89
        if($this->formatterProvider === null)
50 89
        {
51 43
            $this->formatterProvider = new NullProvider();
52 43
        }
53
54 89
        $this->currentFormatterName = null;
55 89
        $this->currentTargetFile = null;
56 89
        $this->systemEnvironment = null;
57 89
        $this->unusedVariables = array_flip($reader->getAllVariables());
58 89
        $this->unvaluedVariables = array();
59 89
        $this->nonDistFilesOverwriteAllowed = false;
60 89
        $this->hydratedFiles = [];
61 89
    }
62
63 86
    public function setSuffix($suffix)
64
    {
65 86
        $this->suffix = $suffix;
66
67 86
        return $this;
68
    }
69
70 2
    public function setDryRun($value = true)
71
    {
72 2
        $this->dryRun = (bool) $value;
73
74 2
        return $this;
75
    }
76
77 1
    public function enableBackup($value = true)
78
    {
79 1
        $this->enableBackup = (bool) $value;
80
81 1
        return $this;
82
    }
83
    
84 15
    public function allowNonDistFilesOverwrite($nonDistFilesOverwriteAllowed = true)
85
    {
86 15
        $this->nonDistFilesOverwriteAllowed = $nonDistFilesOverwriteAllowed;
87
88 15
        return $this;
89
    }
90
91 3
    public function setFormatterProvider(FormatterProvider $formatterProvider)
92
    {
93 3
        $this->formatterProvider = $formatterProvider;
94
95 3
        return $this;
96
    }
97
98 5
    public function setSystemEnvironment($environment)
99
    {
100 5
        $this->systemEnvironment = $environment;
101
102 5
        return $this;
103
    }
104
105 81
    public function hydrate($environment)
106
    {
107 81
        $files = $this->collectFiles();
108
109 81
        foreach($files as $file)
110
        {
111 81
            $this->hydrateFile($file, $environment);
112 61
        }
113
114 60
        if($this->nonDistFilesOverwriteAllowed === true)
115 60
        {
116 3
            $this->copyNonDistFiles();
117 3
        }
118
119 60
        $this->info(sprintf(
120 60
           '%d files generated',
121 60
            count($files)
122 60
        ));
123 60
    }
124
125 85
    private function collectFiles()
126
    {
127 85
        $pattern = sprintf('.*%s$', preg_quote($this->suffix));
128
        
129 85
        return $this->finder->findFiles(sprintf('~%s~', $pattern));
130
    }
131
    
132 3
    private function copyNonDistFiles()
133
    {
134 3
        $filesToCopy = $this->collectNonDistFiles();
135
136 3
        foreach($filesToCopy as $file)
137
        {
138 2
            $this->target->write($file, $this->sources->read($file));
0 ignored issues
show
Bug introduced by
It seems like $this->sources->read($file) targeting Gaufrette\Filesystem::read() can also be of type boolean; however, Gaufrette\Filesystem::write() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
139 3
        }
140 3
    }
141
    
142 3
    private function collectNonDistFiles()
143
    {
144 3
        $pattern = sprintf('(?<!%s)$', preg_quote($this->suffix));
145
        
146 3
        return $this->finder->findFiles(sprintf('~%s~', $pattern));
147
    }
148
    
149
150 81
    private function hydrateFile($file, $environment)
151
    {
152 81
        $this->currentTargetFile = preg_replace(sprintf('~(.*)(%s)$~', preg_quote($this->suffix)), '$1', $file);
153
154 81
        if($this->nonDistFilesOverwriteAllowed)
155 81
        {
156 4
            $this->currentTargetFile = (new \SplFileInfo($this->currentTargetFile))->getFilename();
157 4
        }
158
159 81
        $content = $this->sources->read($file);
160 81
        $replacementCounter = $this->parseFileDirectives($file, $content, $environment);
161
162 62
        $targetContent = $this->injectValues($file, $content, $environment, $replacementCounter);
163
164 61
        $this->debug("Write $this->currentTargetFile");
165
166 61
        if($this->dryRun === false)
167 61
        {
168 60
            if(in_array($this->currentTargetFile, $this->hydratedFiles) && $this->nonDistFilesOverwriteAllowed)
169 60
            {
170 1
                throw new \RuntimeException(sprintf('The fileName "%s" is defined in 2 config folders (not allowed with targetPath config enabled)', $this->currentTargetFile));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 150 characters; contains 176 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
171
            }
172
173 60
            $this->backupFile($this->currentTargetFile);
174 60
            $this->target->write($this->currentTargetFile, $targetContent, true);
175 60
        }
176
177 61
        $this->hydratedFiles[] = $this->currentTargetFile;
178 61
    }
179
180 81
    private function parseFileDirectives($file, & $fileContent, $environment)
181
    {
182 81
        $this->currentFormatterName = null;
183
184 81
        $this->parseFormatterDirective($file, $fileContent);
185 80
        $replacementCounter = $this->parseListDirective($file, $fileContent, $environment);
186
187 62
        $fileContent = $this->removeFileDirectives($fileContent);
188
189 62
        return $replacementCounter;
190
    }
191
192 81
    private function parseFormatterDirective($file, $fileContent)
193
    {
194 81
        if($count = preg_match_all('~<%\s*karma:formatter\s*=\s*(?P<formatterName>[^%]+)%>~', $fileContent, $matches))
195 81
        {
196 3
            if($count !== 1)
197 3
            {
198 1
                throw new \RuntimeException(sprintf(
199 1
                    'Syntax error in %s : only one formatter directive is allowed (%d found)',
200 1
                    $file,
201
                    $count
202 1
                ));
203
            }
204
205 2
            $this->currentFormatterName = strtolower(trim($matches['formatterName'][0]));
206 2
        }
207 80
    }
208
209 80
    private function parseListDirective($file, & $fileContent, $environment)
210
    {
211 80
        $replacementCounter = 0;
212
213 80
        $regexDelimiter = '(delimiter="(?P<delimiterName>[^"]*)")?';
214 80
        $regexWrapper = '(wrapper="(?P<wrapperPrefix>[^"]*)":"(?P<wrapperSuffix>[^"]*)")?';
215 80
        $regex = '~<%\s*karma:list\s*var=(?P<variableName>[\S]+)\s*' . $regexDelimiter . '\s*' . $regexWrapper . '\s*%>~i';
216
217 80
        while(preg_match($regex, $fileContent, $matches))
218
        {
219 30
            $delimiter = '';
220 30
            if(isset($matches['delimiterName']))
221 30
            {
222 26
                $delimiter = $matches['delimiterName'];
223 26
            }
224
225 30
            $wrapper = ['prefix' => '', 'suffix' => ''];
226 30
            if(isset($matches['wrapperPrefix'], $matches['wrapperSuffix']))
227 30
            {
228
                $wrapper = [
229 9
                    'prefix' => $matches['wrapperPrefix'],
230 9
                    'suffix' => $matches['wrapperSuffix']
231 9
                ];
232 9
            }
233
234 30
            $generatedList = $this->generateContentForListDirective($matches['variableName'], $environment, $delimiter, $wrapper);
235 29
            $fileContent = str_replace($matches[0], $generatedList, $fileContent);
236
237 29
            $replacementCounter++;
238 29
        }
239
240 79
        $this->lookingForSyntaxErrorInListDirective($file, $fileContent);
241
242 62
        return $replacementCounter;
243
    }
244
245 79
    private function lookingForSyntaxErrorInListDirective($file, $fileContent)
246
    {
247 79
        if(preg_match('~<%.*karma\s*:\s*list\s*~i', $fileContent))
248 79
        {
249
            // karma:list detected but has not matches full regexp
250 17
            throw new \RuntimeException("Invalid karma:list directive in file $file");
251
        }
252 62
    }
253
254 30
    private function generateContentForListDirective($variable, $environment, $delimiter, array $wrapper)
255
    {
256 30
        $values = $this->readValueToInject($variable, $environment);
257 29
        $formatter = $this->getFormatterForCurrentTargetFile();
258
259 29
        if(! is_array($values))
260 29
        {
261 8
            $values = array($values);
262 8
        }
263
264
        array_walk($values, function (& $value) use ($formatter) {
265 24
            $value = $formatter->format($value);
266 29
        });
267
268 29
        $generated = implode($delimiter, $values);
269 29
        return sprintf(
270 29
            '%s%s%s',
271 29
            ! empty($generated) ? $wrapper['prefix'] : '',
272 29
            $generated,
273 29
            ! empty($generated) ? $wrapper['suffix'] : ''
274 29
        );
275
    }
276
277 62
    private function removeFileDirectives($fileContent)
278
    {
279 62
        return preg_replace('~(<%\s*karma:[^%]*%>\s*)~i', '', $fileContent);
280
    }
281
282 62
    private function injectValues($sourceFile, $content, $environment, $replacementCounter = 0)
283
    {
284 62
        $replacementCounter += $this->injectScalarValues($content, $environment);
285 61
        $replacementCounter += $this->injectListValues($content, $environment);
286
287 61
        if($replacementCounter === 0)
288 61
        {
289 9
            $this->warning("No variable found in $sourceFile");
290 9
        }
291
292 61
        return $content;
293
    }
294
295 59
    private function readValueToInject($variableName, $environment)
296
    {
297 59
        if($this->systemEnvironment !== null && $this->reader->isSystem($variableName) === true)
298 59
        {
299 4
            $environment = $this->systemEnvironment;
300 4
        }
301
302 59
        $this->markVariableAsUsed($variableName);
303
304 59
        $value = $this->reader->read($variableName, $environment);
305
306 58
        $this->checkValueIsAllowed($variableName, $environment, $value);
307
308 57
        return $value;
309
    }
310
311 58
    private function checkValueIsAllowed($variableName, $environment, $value)
312
    {
313 58
        if($value === self::FIXME_VALUE)
314 58
        {
315 1
            throw new \RuntimeException(sprintf(
316 1
                'Missing value for variable %s in environment %s (FIXME marker found)',
317 1
                $variableName,
318
                $environment
319 1
            ));
320
        }
321 57
        elseif($value === self::TODO_VALUE)
322
        {
323 2
            $this->unvaluedVariables[] = $variableName;
324 2
        }
325 57
    }
326
327 62
    private function getFormatterForCurrentTargetFile()
328
    {
329 62
        $fileExtension = pathinfo($this->currentTargetFile, PATHINFO_EXTENSION);
330
331 62
        return $this->formatterProvider->getFormatter($fileExtension, $this->currentFormatterName);
332
    }
333
334 62
    private function injectScalarValues(& $content, $environment)
335
    {
336 62
        $formatter = $this->getFormatterForCurrentTargetFile();
337
338 62
        $content = preg_replace_callback(self::VARIABLE_REGEX, function(array $matches) use($environment, $formatter)
339
        {
340 33
            $value = $this->readValueToInject($matches['variableName'], $environment);
341
342 32
            if(is_array($value))
343 32
            {
344
                // don't replace lists at this time
345 14
                return $matches[0];
346
            }
347
348 25
            return $formatter->format($value);
349
350 62
        }, $content, -1, $count);
351
352 61
        return $count;
353
    }
354
355 61
    private function injectListValues(& $content, $environment)
356
    {
357 61
        $formatter = $this->getFormatterForCurrentTargetFile();
358 61
        $replacementCounter = 0;
359
360 61
        $eol = $this->detectEol($content);
361
362 61
        while(preg_match(self::VARIABLE_REGEX, $content))
363
        {
364 14
            $lines = explode($eol, $content);
365 14
            $result = array();
366
367 14
            foreach($lines as $line)
368
            {
369 14
                if(preg_match(self::VARIABLE_REGEX, $line, $matches))
370 14
                {
371 14
                    $values = $this->readValueToInject($matches['variableName'], $environment);
372
373 14
                    $replacementCounter++;
374 14
                    foreach($values as $value)
375
                    {
376 14
                        $result[] = preg_replace(self::VARIABLE_REGEX, $formatter->format($value), $line, 1);
377 14
                    }
378
379 14
                    continue;
380
                }
381
382 8
                $result[] = $line;
383 14
            }
384
385 14
            $content = implode($eol, $result);
386 14
        }
387
388 61
        return $replacementCounter;
389
    }
390
391 61
    private function detectEol($content)
392
    {
393 61
        $types = array("\r\n", "\r", "\n");
394
395 61
        foreach($types as $type)
396
        {
397 61
            if(strpos($content, $type) !== false)
398 61
            {
399 14
                return $type;
400
            }
401 60
        }
402
403 52
        return "\n";
404
    }
405
406 60
    private function backupFile($targetFile)
407
    {
408 60
        if($this->enableBackup === true)
409 60
        {
410 1
            if($this->target->has($targetFile))
411 1
            {
412 1
                $backupFile = $targetFile . Application::BACKUP_SUFFIX;
413 1
                $this->target->write($backupFile, $this->target->read($targetFile), true);
0 ignored issues
show
Bug introduced by
It seems like $this->target->read($targetFile) targeting Gaufrette\Filesystem::read() can also be of type boolean; however, Gaufrette\Filesystem::write() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
414 1
            }
415 1
        }
416 60
    }
417
418 4
    public function rollback()
419
    {
420 4
        $files = $this->collectFiles();
421
422 4
        foreach($files as $file)
423
        {
424 2
            $this->rollbackFile($file);
425 4
        }
426 4
    }
427
428 2
    private function rollbackFile($file)
429
    {
430 2
        $this->debug("- $file");
431
432 2
        $targetFile = substr($file, 0, strlen($this->suffix) * -1);
433 2
        $backupFile = $targetFile . Application::BACKUP_SUFFIX;
434
435 2
        if($this->sources->has($backupFile))
436 2
        {
437 2
            $this->info("  Writing $targetFile");
438
439 2
            if($this->dryRun === false)
440 2
            {
441 1
                $backupContent = $this->sources->read($backupFile);
442 1
                $this->sources->write($targetFile, $backupContent, true);
0 ignored issues
show
Bug introduced by
It seems like $backupContent defined by $this->sources->read($backupFile) on line 441 can also be of type boolean; however, Gaufrette\Filesystem::write() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
443 1
            }
444 2
        }
445 2
    }
446
447 9
    public function getUnusedVariables()
448
    {
449 9
        return array_merge(array_flip($this->unusedVariables));
450
    }
451
452 59
    private function markVariableAsUsed($variableName)
453
    {
454 59
        if(isset($this->unusedVariables[$variableName]))
455 59
        {
456 57
            unset($this->unusedVariables[$variableName]);
457 57
        }
458 59
    }
459
460 9
    public function getUnvaluedVariables()
461
    {
462 9
        return $this->unvaluedVariables;
463
    }
464
}
465