Completed
Push — nln-php7 ( 1a6b54...2856d9 )
by Nicolas
02:57
created

Hydrator::injectListValues()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 45
ccs 22
cts 22
cp 1
rs 8.5777
c 0
b 0
f 0
cc 6
nc 6
nop 2
crap 6
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
    private const
16
        TODO_VALUE = '__TODO__',
17
        FIXME_VALUE = '__FIXME__',
18
        VARIABLE_REGEX = '~<%(?P<variableName>[A-Za-z0-9_\.\-]+)%>~';
19
20
    private Filesystem
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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