Completed
Pull Request — master (#96)
by Sébastien
03:27
created

Hydrator::markVariableAsUsed()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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