Completed
Push — master ( 1375a1...3368bc )
by Nicolas
03:04
created

Hydrator::generateContentForListDirective()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
359
        {
360
            if($this->sources->has($targetFile))
361
            {
362
                $backupFile = $targetFile . Application::BACKUP_SUFFIX;
363
                $this->sources->write($backupFile, $this->sources->read($targetFile), true);
0 ignored issues
show
Bug introduced by
It seems like $this->sources->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...
364
            }
365
        }
366
    }
367
368
    public function rollback()
369
    {
370
        $distFiles = $this->collectDistFiles();
371
372
        foreach($distFiles as $file)
373
        {
374
            $this->rollbackFile($file);
375
        }
376
    }
377
378
    private function rollbackFile($file)
379
    {
380
        $this->debug("- $file");
381
382
        $targetFile = substr($file, 0, strlen($this->suffix) * -1);
383
        $backupFile = $targetFile . Application::BACKUP_SUFFIX;
384
385 View Code Duplication
        if($this->sources->has($backupFile))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
386
        {
387
            $this->info("  Writing $targetFile");
388
389
            if($this->dryRun === false)
390
            {
391
                $backupContent = $this->sources->read($backupFile);
392
                $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 391 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...
393
            }
394
        }
395
    }
396
397
    public function getUnusedVariables()
398
    {
399
        return array_merge(array_flip($this->unusedVariables));
400
    }
401
402
    private function markVariableAsUsed($variableName)
403
    {
404
        if(isset($this->unusedVariables[$variableName]))
405
        {
406
            unset($this->unusedVariables[$variableName]);
407
        }
408
    }
409
410
    public function getUnvaluedVariables()
411
    {
412
        return $this->unvaluedVariables;
413
    }
414
}
415