Completed
Push — master ( d24520...9dfa2d )
by Nicolas
05:12
created

Hydrator::hydrateFile()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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