Passed
Push — master ( 42c4d1...80535d )
by Allan
02:39 queued 12s
created

Applier::scanOutputForFailures()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 26
nc 4
nop 2
dl 0
loc 50
rs 8.5706
c 0
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Applier::extractArrayValue() 0 3 1
A Applier::validateOutput() 0 35 5
1
<?php
2
/**
3
 * Copyright © Vaimo Group. All rights reserved.
4
 * See LICENSE_VAIMO.txt for license details.
5
 */
6
namespace Vaimo\ComposerPatches\Patch\File;
7
8
use Vaimo\ComposerPatches\Config as PluginConfig;
9
use Vaimo\ComposerPatches\Repository\PatchesApplier\Operation as PatcherOperation;
10
11
class Applier
12
{
13
    /**
14
     * @var \Vaimo\ComposerPatches\Logger
15
     */
16
    private $logger;
17
18
    /**
19
     * @var \Vaimo\ComposerPatches\Shell
20
     */
21
    private $shell;
22
23
    /**
24
     * @var array
25
     */
26
    private $config;
27
28
    /**
29
     * @var \Vaimo\ComposerPatches\Utils\ConfigUtils
30
     */
31
    private $applierUtils;
32
33
    /**
34
     * @var \Vaimo\ComposerPatches\Utils\TemplateUtils
35
     */
36
    private $templateUtils;
37
38
    /**
39
     * @var \Vaimo\ComposerPatches\Utils\DataUtils
40
     */
41
    private $dataUtils;
42
43
    /**
44
     * @var \Vaimo\ComposerPatches\Console\OutputAnalyser
45
     */
46
    private $outputAnalyser;
47
    
48
    /**
49
     * @var array
50
     */
51
    private $resultCache;
52
53
    /**
54
     * @param \Vaimo\ComposerPatches\Logger $logger
55
     * @param array $config
56
     */
57
    public function __construct(
58
        \Vaimo\ComposerPatches\Logger $logger,
59
        array $config
60
    ) {
61
        $this->logger = $logger;
62
        $this->config = $config;
63
64
        $this->shell = new \Vaimo\ComposerPatches\Shell($logger);
65
        $this->applierUtils = new \Vaimo\ComposerPatches\Utils\ConfigUtils();
66
        $this->templateUtils = new \Vaimo\ComposerPatches\Utils\TemplateUtils();
67
        $this->dataUtils = new \Vaimo\ComposerPatches\Utils\DataUtils();
68
        $this->outputAnalyser = new \Vaimo\ComposerPatches\Console\OutputAnalyser();
69
    }
70
71
    public function applyFile($filename, $cwd, array $config = array())
72
    {
73
        $applierConfig = $this->applierUtils->mergeApplierConfig($this->config, array_filter($config));
74
75
        $applierConfig = $this->applierUtils->sortApplierConfig($applierConfig);
76
77
        $patchers = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_APPLIERS);
78
        $operations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_OPERATIONS);
79
        $levels = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_LEVELS);
80
        $failureMessages = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_FAILURES);
81
82
        $sanityOperations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_SANITY);
83
84
        try {
85
            $this->applierUtils->validateConfig($applierConfig);
86
        } catch (\Vaimo\ComposerPatches\Exceptions\ConfigValidationException $exception) {
87
            $this->logger->writeVerbose('error', $exception->getMessage());
88
        }
89
90
        $arguments = array(
91
            PluginConfig::PATCHER_ARG_FILE => $filename,
92
            PluginConfig::PATCHER_ARG_CWD => $cwd
93
        );
94
95
        $patcherName = $this->executeOperations($patchers, $sanityOperations, $arguments);
96
97
        if (!$patcherName) {
98
            $message = sprintf(
99
                'None of the patch appliers seem to be available (tried: %s)',
100
                implode(', ', array_keys($patchers))
101
            );
102
103
            throw new \Vaimo\ComposerPatches\Exceptions\RuntimeException($message);
104
        }
105
        
106
        $errorGroups = array();
107
108
        foreach ($levels as $patchLevel) {
109
            $arguments = array_replace(
110
                $arguments,
111
                array(PluginConfig::PATCHER_ARG_LEVEL => $patchLevel)
112
            );
113
114
            try {
115
                $patcherName = $this->executeOperations($patchers, $operations, $arguments, $failureMessages);
116
            } catch (\Vaimo\ComposerPatches\Exceptions\ApplierFailure $exception) {
117
                $errorGroups[] = $exception->getErrors();
118
                continue;
119
            }
120
            
121
            $this->logger->writeVerbose(
122
                'info',
123
                'SUCCESS with type=%s (p=%s)',
124
                array($patcherName, $patchLevel)
125
            );
126
127
            return;
128
        }
129
        
130
        $failure = new \Vaimo\ComposerPatches\Exceptions\ApplierFailure(
131
            sprintf('Cannot apply patch %s', $filename)
132
        );
133
        
134
        $failure->setErrors(
135
            array_reduce($errorGroups, 'array_merge_recursive', array())
136
        );
137
        
138
        throw $failure;
139
    }
140
141
    private function executeOperations(
142
        $patchers,
143
        array $operations,
144
        array $args = array(),
145
        array $failures = array()
146
    ) {
147
        $outputRecords = array();
148
        
149
        foreach ($patchers as $type => $patcher) {
150
            if (!$patcher) {
151
                continue;
152
            }
153
154
            try {
155
                return $this->processOperationItems($patcher, $operations, $args, $failures);
156
            } catch (\Vaimo\ComposerPatches\Exceptions\OperationFailure $exception) {
157
                $operationReference = is_string($exception->getMessage())
158
                    ? $exception->getMessage()
159
                    : PatcherOperation::TYPE_UNKNOWN;
160
161
                $outputRecords[$type] = $exception->getOutput();
162
                    
163
                $messageArgs = array(
164
                    strtoupper($operationReference),
165
                    $type,
166
                    $this->extractStringValue($args, PluginConfig::PATCHER_ARG_LEVEL)
167
                );
168
169
                $this->logger->writeVerbose('warning', '%s (type=%s) failed with p=%s', $messageArgs);
170
            }
171
        }
172
173
        $messages = $this->collectErrors($outputRecords, array(
174
            'failed',
175
            'unexpected',
176
            'malformed',
177
            'error',
178
            'corrupt',
179
            'can\'t find file',
180
            'patch unexpectedly ends',
181
            'due to output analysis',
182
            'no file to patch'
183
        ));
184
        
185
        $failure = new \Vaimo\ComposerPatches\Exceptions\ApplierFailure();
186
        
187
        $failure->setErrors($messages);
188
189
        throw $failure;
190
    }
191
    
192
    private function collectErrors(array $outputRecords, array $filters)
193
    {
194
        $pathMarker = '\|\+\+\+\s(?P<match>.*?)(\t|$)';
195
        
196
        $errorMatcher = sprintf(
197
            '/(%s)/i',
198
            implode('|', array_merge($filters, array($pathMarker)))
199
        );
200
201
        $pathMatcher = sprintf('/^%s/i', $pathMarker);
202
203
        $result = array();
204
        
205
        foreach ($outputRecords as $code => $output) {
206
            $lines = is_array($output) ? $output : explode(PHP_EOL, $output);
207
            
208
            $result[$code] = $this->dataUtils->listToGroups(
209
                preg_grep($errorMatcher, $lines),
210
                $pathMatcher
211
            );
212
        }
213
        
214
        return $result;
215
    }
216
217
    private function processOperationItems($patcher, $operations, $args, $failures)
218
    {
219
        $operationResults = array_fill_keys(array_keys($operations), '');
220
221
        $result = true;
222
223
        foreach (array_keys($operations) as $operationCode) {
224
            if (!isset($patcher[$operationCode])) {
225
                continue;
226
            }
227
228
            $args = array_replace($args, $operationResults);
229
230
            $applierOperations = is_array($patcher[$operationCode])
231
                ? $patcher[$operationCode]
232
                : array($patcher[$operationCode]);
233
234
235
            $operationFailures = $this->extractArrayValue($failures, $operationCode);
236
237
            list($result, $output) = $this->resolveOperationOutput(
238
                $applierOperations,
239
                $args,
240
                $operationFailures
241
            );
242
243
            if ($output !== false) {
244
                $operationResults[$operationCode] = $output;
245
            }
246
247
            if ($result) {
248
                continue;
249
            }
250
251
            $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure($operationCode);
252
253
            $failure->setOutput(explode(PHP_EOL, $output));
254
            
255
            throw $failure;
256
        }
257
258
        return $result;
259
    }
260
261
    private function resolveOperationOutput($applierOperations, $args, $operationFailures)
262
    {
263
        $variableFormats = array(
264
            '{{%s}}' => array('escapeshellarg'),
265
            '[[%s]]' => array()
266
        );
267
268
        $output = '';
269
        
270
        foreach ($applierOperations as $operation) {
271
            $passOnFailure = strpos($operation, '!') === 0;
272
            $operation = ltrim($operation, '!');
273
274
            $command = $this->templateUtils->compose($operation, $args, $variableFormats);
275
276
            $cwd = $this->extractStringValue($args, PluginConfig::PATCHER_ARG_CWD);
277
278
            $resultKey = sprintf('%s | %s', $cwd, $command);
279
280
            if ($passOnFailure) {
281
                $this->logger->writeVerbose(
282
                    \Vaimo\ComposerPatches\Logger::TYPE_NONE,
283
                    '<comment>***</comment> '
284
                    . 'The expected result to execution is a failure'
285
                    . '<comment>***</comment>'
286
                );
287
            }
288
289
            if (isset($this->resultCache[$resultKey])) {
290
                $this->logger->writeVerbose(
291
                    \Vaimo\ComposerPatches\Logger::TYPE_NONE,
292
                    sprintf('(using cached result for: %s = %s)', $command, reset($this->resultCache[$resultKey]))
293
                );
294
            }
295
            
296
            if (!isset($this->resultCache[$resultKey])) {
297
                $this->resultCache[$resultKey] = $this->shell->execute($command, $cwd);
298
            }
299
300
            list($result, $output) = $this->resultCache[$resultKey];
301
302
            if ($result) {
303
                $this->validateOutput($output, $operationFailures);
304
            }
305
306
            if ($passOnFailure) {
307
                $result = !$result;
308
            }
309
310
            if (!$result) {
311
                continue;
312
            }
313
314
            return array($result, $output);
315
        }
316
317
        return array(false, $output);
318
    }
319
    
320
    private function validateOutput($output, $operationFailures)
321
    {
322
        $pathMarker = '\|\+\+\+\s(?P<match>.*?)(\t|$)';
323
        $pathMatcher = sprintf('/^%s/', $pathMarker);
324
        
325
        $failures = $this->outputAnalyser->scanOutputForFailures($output, $pathMatcher, $operationFailures);
326
        
327
        if (!$failures) {
328
            return;
329
        }
330
        
331
        foreach ($failures as $patternCode => $items) {
332
            foreach ($items as $index => $item) {
333
                if (preg_match($pathMatcher, $item)) {
334
                    continue;
335
                }
336
337
                $message = sprintf(
338
                    'Success changed to FAILURE due to output analysis (%s):',
339
                    $patternCode
340
                );
341
342
                $failures[$patternCode][$index] = implode(PHP_EOL, array($message, $item));
343
                
344
                $this->logger->writeVerbose(
345
                    'warning',
346
                    sprintf('%s: %s', $message, $operationFailures[$patternCode])
347
                );
348
            }
349
        }
350
        
351
        $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure('Output analysis failed');
352
353
        throw $failure->setOutput(
354
            array_reduce($failures, 'array_merge', array())
355
        );
356
    }
357
    
358
    private function extractArrayValue($data, $key)
359
    {
360
        return $this->extractValue($data, $key, array());
361
    }
362
363
    private function extractStringValue($data, $key)
364
    {
365
        return $this->extractValue($data, $key, '');
366
    }
367
368
    private function extractValue($data, $key, $default)
369
    {
370
        return isset($data[$key]) ? $data[$key] : $default;
371
    }
372
}
373