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, |
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) |
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)); |
|
|
|
|
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)); |
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
|
|
|
$file, |
211
|
|
|
$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
|
29 |
|
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
|
|
|
$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
|
|
|
$variableName, |
328
|
|
|
$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
|
63 |
|
$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
|
|
|
$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
|
14 |
|
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); |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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.