Completed
Push — nln-php7 ( 6ce259...311875 )
by Nicolas
02:29
created

Hydrator::hydratedFiles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace Karma;
6
7
use Gaufrette\Filesystem;
8
use Psr\Log\NullLogger;
9
use Karma\FormatterProviders\NullProvider;
10
11
class Hydrator implements ConfigurableProcessor
12
{
13
    use \Karma\Logging\LoggerAware;
14
15
    const
16
        TODO_VALUE = '__TODO__',
17
        FIXME_VALUE = '__FIXME__',
18
        VARIABLE_REGEX = '~<%(?P<variableName>[A-Za-z0-9_\.\-]+)%>~';
19
20
    private
21
        $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...
22
        $suffix,
23
        $reader,
24
        $dryRun,
25
        $enableBackup,
26
        $finder,
27
        $formatterProvider,
28
        $currentFormatterName,
29
        $currentTargetFile,
30
        $systemEnvironment,
31
        $unusedVariables,
32
        $unvaluedVariables,
33
        $target,
34
        $nonDistFilesOverwriteAllowed,
35
        $hydratedFiles;
36
37 87
    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...
38
    {
39 87
        $this->logger = new NullLogger();
40
41 87
        $this->sources = $sources;
42 87
        $this->target = $target;
43 87
        $this->reader = $reader;
44 87
        $this->finder = $finder;
45
46 87
        $this->suffix = Application::DEFAULT_DISTFILE_SUFFIX;
47 87
        $this->dryRun = false;
48 87
        $this->enableBackup = false;
49
50 87
        $this->formatterProvider = $formatterProvider;
51 87
        if($this->formatterProvider === null)
52
        {
53 41
            $this->formatterProvider = new NullProvider();
54
        }
55
56 87
        $this->currentFormatterName = null;
57 87
        $this->currentTargetFile = null;
58 87
        $this->systemEnvironment = null;
59 87
        $this->unusedVariables = array_flip($reader->getAllVariables());
60 87
        $this->unvaluedVariables = [];
61 87
        $this->nonDistFilesOverwriteAllowed = false;
62 87
        $this->hydratedFiles = [];
63 87
    }
64
65 87
    public function setSuffix(string $suffix)
66
    {
67 87
        $this->suffix = $suffix;
68
69 87
        return $this;
70
    }
71
72 2
    public function setDryRun(bool $value = true): ConfigurableProcessor
73
    {
74 2
        $this->dryRun = $value;
75
76 2
        return $this;
77
    }
78
79 1
    public function enableBackup(bool $value = true): ConfigurableProcessor
80
    {
81 1
        $this->enableBackup = $value;
82
83 1
        return $this;
84
    }
85
    
86 15
    public function allowNonDistFilesOverwrite(bool $nonDistFilesOverwriteAllowed = true)
87
    {
88 15
        $this->nonDistFilesOverwriteAllowed = $nonDistFilesOverwriteAllowed;
89
90 15
        return $this;
91
    }
92
93 3
    public function setFormatterProvider(FormatterProvider $formatterProvider)
94
    {
95 3
        $this->formatterProvider = $formatterProvider;
96
97 3
        return $this;
98
    }
99
100 5
    public function setSystemEnvironment(?string $environment): ConfigurableProcessor
101
    {
102 5
        $this->systemEnvironment = $environment;
103
104 5
        return $this;
105
    }
106
107 82
    public function hydrate(string $environment): void
108
    {
109 82
        $files = $this->collectFiles();
110
111 82
        foreach($files as $file)
112
        {
113 82
            $this->hydrateFile($file, $environment);
114
        }
115
116 60
        if($this->nonDistFilesOverwriteAllowed === true)
117
        {
118 3
            $this->copyNonDistFiles();
119
        }
120
121 60
        $this->info(sprintf(
122 60
           '%d files generated',
123 60
            count($files)
124
        ));
125 60
    }
126
127 86
    private function collectFiles(): iterable
128
    {
129 86
        $pattern = sprintf('.*%s$', preg_quote($this->suffix, '~'));
130
        
131 86
        return $this->finder->findFiles(sprintf('~%s~', $pattern));
132
    }
133
    
134 3
    private function copyNonDistFiles(): void
135
    {
136 3
        $filesToCopy = $this->collectNonDistFiles();
137
138 3
        foreach($filesToCopy as $file)
139
        {
140 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...
141
        }
142 3
    }
143
    
144 3
    private function collectNonDistFiles(): iterable
145
    {
146 3
        $pattern = sprintf('(?<!%s)$', preg_quote($this->suffix, '~'));
147
        
148 3
        return $this->finder->findFiles(sprintf('~%s~', $pattern));
149
    }
150
    
151
152 82
    private function hydrateFile(string $file, string $environment): void
153
    {
154 82
        $this->currentTargetFile = preg_replace(sprintf(
155 82
            '~(.*)(%s)$~',
156 82
            preg_quote($this->suffix, '~')
157 82
        ), '$1', $file);
158
159 82
        if($this->nonDistFilesOverwriteAllowed)
160
        {
161 4
            $this->currentTargetFile = (new \SplFileInfo($this->currentTargetFile))->getFilename();
162
        }
163
164 82
        $content = (string) $this->sources->read($file);
165 82
        $replacementCounter = $this->parseFileDirectives($file, $content, $environment);
166
167 63
        $targetContent = $this->injectValues($file, $content, $environment, $replacementCounter);
168
169 61
        $this->debug("Write $this->currentTargetFile");
170
171 61
        if($this->dryRun === false)
172
        {
173 60
            if($this->hasBeenHydrated($this->currentTargetFile) && $this->nonDistFilesOverwriteAllowed)
174
            {
175 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...
176
            }
177
178 60
            $this->backupFile($this->currentTargetFile);
179 60
            $this->target->write($this->currentTargetFile, $targetContent, true);
180
        }
181
182 61
        $this->hydratedFiles[$this->currentTargetFile] = $replacementCounter;
183 61
    }
184
185 60
    private function hasBeenHydrated(string $file): bool
186
    {
187 60
        return array_key_exists($file, $this->hydratedFiles);
188
    }
189
190 82
    private function parseFileDirectives(string $file, string & $fileContent, string $environment): int
191
    {
192 82
        $this->currentFormatterName = null;
193
194 82
        $this->parseFormatterDirective($file, $fileContent);
195 81
        $replacementCounter = $this->parseListDirective($file, $fileContent, $environment);
196
197 63
        $fileContent = $this->removeFileDirectives($fileContent);
198
199 63
        return $replacementCounter;
200
    }
201
202 82
    private function parseFormatterDirective(string $file, string $fileContent): void
203
    {
204 82
        if($count = preg_match_all('~<%\s*karma:formatter\s*=\s*(?P<formatterName>[^%]+)%>~', $fileContent, $matches))
205
        {
206 3
            if($count !== 1)
207
            {
208 1
                throw new \RuntimeException(sprintf(
209 1
                    'Syntax error in %s : only one formatter directive is allowed (%d found)',
210 1
                    $file,
211 1
                    $count
212
                ));
213
            }
214
215 2
            $this->currentFormatterName = strtolower(trim($matches['formatterName'][0]));
216
        }
217 81
    }
218
219 81
    private function parseListDirective(string $file, string & $fileContent, string $environment): int
220
    {
221 81
        $replacementCounter = 0;
222
223 81
        $regexDelimiter = '(delimiter="(?P<delimiterName>[^"]*)")?';
224 81
        $regexWrapper = '(wrapper="(?P<wrapperPrefix>[^"]*)":"(?P<wrapperSuffix>[^"]*)")?';
225 81
        $regex = '~<%\s*karma:list\s*var=(?P<variableName>[\S]+)\s*' . $regexDelimiter . '\s*' . $regexWrapper . '\s*%>~i';
226
227 81
        while(preg_match($regex, $fileContent, $matches))
228
        {
229 30
            $delimiter = '';
230 30
            if(isset($matches['delimiterName']))
231
            {
232 26
                $delimiter = $matches['delimiterName'];
233
            }
234
235 30
            $wrapper = ['prefix' => '', 'suffix' => ''];
236 30
            if(isset($matches['wrapperPrefix'], $matches['wrapperSuffix']))
237
            {
238
                $wrapper = [
239 9
                    'prefix' => $matches['wrapperPrefix'],
240 9
                    'suffix' => $matches['wrapperSuffix']
241
                ];
242
            }
243
244 30
            $generatedList = $this->generateContentForListDirective($matches['variableName'], $environment, $delimiter, $wrapper);
245 29
            $fileContent = str_replace($matches[0], $generatedList, $fileContent);
246
247 29
            $replacementCounter++;
248
        }
249
250 80
        $this->lookingForSyntaxErrorInListDirective($file, $fileContent);
251
252 63
        return $replacementCounter;
253
    }
254
255 80
    private function lookingForSyntaxErrorInListDirective(string $file, string $fileContent): void
256
    {
257 80
        if(preg_match('~<%.*karma\s*:\s*list\s*~i', $fileContent))
258
        {
259
            // karma:list detected but has not matches full regexp
260 17
            throw new \RuntimeException("Invalid karma:list directive in file $file");
261
        }
262 63
    }
263
264 30
    private function generateContentForListDirective(string $variable, string $environment, string $delimiter, array $wrapper): string
265
    {
266 30
        $values = $this->readValueToInject($variable, $environment);
267 29
        $formatter = $this->getFormatterForCurrentTargetFile();
268
269 29
        if(! is_array($values))
270
        {
271 8
            $values = [$values];
272
        }
273
274
        array_walk($values, function (& $value) use ($formatter) {
275 24
            $value = $formatter->format($value);
276 29
        });
277
278 29
        $generated = implode($delimiter, $values);
279 29
        return sprintf(
280 29
            '%s%s%s',
281 29
            ! empty($generated) ? $wrapper['prefix'] : '',
282 29
            $generated,
283 29
            ! empty($generated) ? $wrapper['suffix'] : ''
284
        );
285
    }
286
287 63
    private function removeFileDirectives($fileContent)
288
    {
289 63
        return preg_replace('~(<%\s*karma:[^%]*%>\s*)~i', '', $fileContent);
290
    }
291
292 63
    private function injectValues(string $sourceFile, string $content, string $environment, int & $replacementCounter = 0): string
293
    {
294 63
        $replacementCounter += $this->injectScalarValues($content, $environment);
295 62
        $replacementCounter += $this->injectListValues($content, $environment);
296
297 61
        if($replacementCounter === 0)
298
        {
299 9
            $this->warning("No variable found in $sourceFile");
300
        }
301
302 61
        return $content;
303
    }
304
305 60
    private function readValueToInject(string $variableName, string $environment)
306
    {
307 60
        if($this->systemEnvironment !== null && $this->reader->isSystem($variableName) === true)
308
        {
309 4
            $environment = $this->systemEnvironment;
310
        }
311
312 60
        $this->markVariableAsUsed($variableName);
313
314 60
        $value = $this->reader->read($variableName, $environment);
315
316 59
        $this->checkValueIsAllowed($variableName, $environment, $value);
317
318 58
        return $value;
319
    }
320
321 59
    private function checkValueIsAllowed(string $variableName, string $environment, $value): void
322
    {
323 59
        if($value === self::FIXME_VALUE)
324
        {
325 1
            throw new \RuntimeException(sprintf(
326 1
                'Missing value for variable %s in environment %s (FIXME marker found)',
327 1
                $variableName,
328 1
                $environment
329
            ));
330
        }
331
332 58
        if($value === self::TODO_VALUE)
333
        {
334 2
            $this->unvaluedVariables[] = $variableName;
335
        }
336 58
    }
337
338 63
    private function getFormatterForCurrentTargetFile(): Formatter
339
    {
340 63
        $fileExtension = pathinfo($this->currentTargetFile, PATHINFO_EXTENSION);
341
342 63
        return $this->formatterProvider->getFormatter($fileExtension, $this->currentFormatterName);
343
    }
344
345 63
    private function injectScalarValues(string & $content, string $environment): int
346
    {
347 63
        $formatter = $this->getFormatterForCurrentTargetFile();
348
349
        $content = preg_replace_callback(self::VARIABLE_REGEX, function(array $matches) use($environment, $formatter)
350
        {
351 34
            $value = $this->readValueToInject($matches['variableName'], $environment);
352
353 33
            if(is_array($value))
354
            {
355
                // don't replace lists at this time
356 14
                return $matches[0];
357
            }
358
359 26
            return $formatter->format($value);
360
361 63
        }, $content, -1, $count);
362
363 62
        return $count;
364
    }
365
366 62
    private function injectListValues(string & $content, string $environment): int
367
    {
368 62
        $formatter = $this->getFormatterForCurrentTargetFile();
369 62
        $replacementCounter = 0;
370
371 62
        $eol = $this->detectEol($content);
372
373 62
        while(preg_match(self::VARIABLE_REGEX, $content))
374
        {
375 15
            $lines = explode($eol, $content);
376 15
            $result = [];
377
378 15
            foreach($lines as $lineNumber => $line)
379
            {
380 15
                if(preg_match(self::VARIABLE_REGEX, $line, $matches))
381
                {
382 15
                    $values = $this->readValueToInject($matches['variableName'], $environment);
383
384 15
                    if(!is_array($values))
385
                    {
386 1
                        throw new \RuntimeException(sprintf(
387 1
                            "Nested variable detected [%s] while writing %s at line %d",
388 1
                            $matches['variableName'],
389 1
                            $this->currentTargetFile,
390 1
                            $lineNumber
391
                        ));
392
                    }
393
                    
394 14
                    $replacementCounter++;
395 14
                    foreach($values as $value)
396
                    {
397 14
                        $result[] = preg_replace(self::VARIABLE_REGEX, $formatter->format($value), $line, 1);
398
                    }
399
400 14
                    continue;
401
                }
402
403 8
                $result[] = $line;
404
            }
405
406 14
            $content = implode($eol, $result);
407
        }
408
409 61
        return $replacementCounter;
410
    }
411
412 62
    private function detectEol(string $content): string
413
    {
414 62
        $types = array("\r\n", "\r", "\n");
415
416 62
        foreach($types as $type)
417
        {
418 62
            if(strpos($content, $type) !== false)
419
            {
420 62
                return $type;
421
            }
422
        }
423
424 53
        return "\n";
425
    }
426
427 60
    private function backupFile(string $targetFile): void
428
    {
429 60
        if($this->enableBackup === true)
430
        {
431 1
            if($this->target->has($targetFile))
432
            {
433 1
                $backupFile = $targetFile . Application::BACKUP_SUFFIX;
434 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...
435
            }
436
        }
437 60
    }
438
439 4
    public function rollback(): void
440
    {
441 4
        $files = $this->collectFiles();
442
443 4
        foreach($files as $file)
444
        {
445 2
            $this->rollbackFile($file);
446
        }
447 4
    }
448
449 2
    private function rollbackFile(string $file): void
450
    {
451 2
        $this->debug("- $file");
452
453 2
        $targetFile = substr($file, 0, strlen($this->suffix) * -1);
454 2
        $backupFile = $targetFile . Application::BACKUP_SUFFIX;
455
456 2
        if($this->sources->has($backupFile))
457
        {
458 2
            $this->info("  Writing $targetFile");
459
460 2
            if($this->dryRun === false)
461
            {
462 1
                $backupContent = $this->sources->read($backupFile);
463 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 462 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...
464
            }
465
        }
466 2
    }
467
468 9
    public function getUnusedVariables(): array
469
    {
470 9
        return array_merge(array_flip($this->unusedVariables));
471
    }
472
473 60
    private function markVariableAsUsed(string $variableName): void
474
    {
475 60
        if(isset($this->unusedVariables[$variableName]))
476
        {
477 58
            unset($this->unusedVariables[$variableName]);
478
        }
479 60
    }
480
481 9
    public function getUnvaluedVariables(): array
482
    {
483 9
        return $this->unvaluedVariables;
484
    }
485
486
    public function hydratedFiles(): array
487
    {
488
        return $this->hydratedFiles;
489
    }
490
}
491