Passed
Push — master ( afd02d...b0430a )
by Allan
03:37 queued 10s
created

Applier::scanOutputForFailures()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 3
nop 2
dl 0
loc 20
rs 9.9
c 0
b 0
f 0
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 array
40
     */
41
    private $resultCache;
42
43
    /**
44
     * @param \Vaimo\ComposerPatches\Logger $logger
45
     * @param array $config
46
     */
47
    public function __construct(
48
        \Vaimo\ComposerPatches\Logger $logger,
49
        array $config
50
    ) {
51
        $this->logger = $logger;
52
        $this->config = $config;
53
54
        $this->shell = new \Vaimo\ComposerPatches\Shell($logger);
55
        $this->applierUtils = new \Vaimo\ComposerPatches\Utils\ConfigUtils();
56
        $this->templateUtils = new \Vaimo\ComposerPatches\Utils\TemplateUtils();
57
    }
58
59
    public function applyFile($filename, $cwd, array $config = array())
60
    {
61
        $applierConfig = $this->applierUtils->mergeApplierConfig($this->config, array_filter($config));
62
63
        $applierConfig = $this->applierUtils->sortApplierConfig($applierConfig);
64
65
        $patchers = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_APPLIERS);
66
        $operations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_OPERATIONS);
67
        $levels = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_LEVELS);
68
        $failureMessages = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_FAILURES);
69
70
        $sanityOperations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_SANITY);
71
72
        try {
73
            $this->applierUtils->validateConfig($applierConfig);
74
        } catch (\Vaimo\ComposerPatches\Exceptions\ConfigValidationException $exception) {
75
            $this->logger->writeVerbose('error', $exception->getMessage());
76
        }
77
78
        $arguments = array(
79
            PluginConfig::PATCHER_ARG_FILE => $filename,
80
            PluginConfig::PATCHER_ARG_CWD => $cwd
81
        );
82
83
        $patcherName = $this->executeOperations($patchers, $sanityOperations, $arguments);
84
85
        if (!$patcherName) {
86
            $message = sprintf(
87
                'None of the patch appliers seem to be available (tried: %s)',
88
                implode(', ', array_keys($patchers))
89
            );
90
91
            throw new \Vaimo\ComposerPatches\Exceptions\RuntimeException($message);
92
        }
93
        
94
        $errors = [];
95
96
        foreach ($levels as $patchLevel) {
97
            $arguments = array_replace(
98
                $arguments,
99
                array(PluginConfig::PATCHER_ARG_LEVEL => $patchLevel)
100
            );
101
102
            try {
103
                $patcherName = $this->executeOperations($patchers, $operations, $arguments, $failureMessages);
104
            } catch (\Vaimo\ComposerPatches\Exceptions\ApplierFailure $exception) {
105
                $errors[] = $exception->getErrors();
106
                continue;
107
            }
108
            
109
            $this->logger->writeVerbose(
110
                'info',
111
                'SUCCESS with type=%s (p=%s)',
112
                array($patcherName, $patchLevel)
113
            );
114
115
            return;
116
        }
117
118
        $errors = array_map(
119
            'array_unique', 
120
            array_reduce($errors, 'array_merge_recursive', [])
121
        );
122
        
123
        foreach ($errors as $type => $messages) {
124
            $fileNotFoundMessages = preg_grep('/(can\'t find file|unable to find file|no such file)/i', $messages);
125
            
126
            if ($fileNotFoundMessages !== $messages) {
127
                $errors[$type] = array_diff($messages, $fileNotFoundMessages);
128
            } 
129
        }
130
131
        $failure = new \Vaimo\ComposerPatches\Exceptions\ApplierFailure(
132
            sprintf('Cannot apply patch %s', $filename)
133
        );
134
135
        $failure->setErrors($errors);
136
        
137
        throw $failure;
138
    }
139
140
    private function executeOperations(
141
        $patchers, array $operations, array $args = array(), array $failures = array()
142
    ) {
143
        $outputRecords = array();
144
        
145
        foreach ($patchers as $type => $patcher) {
146
            if (!$patcher) {
147
                continue;
148
            }
149
150
            try {
151
                return $this->processOperationItems($patcher, $operations, $args, $failures);
152
            } catch (\Vaimo\ComposerPatches\Exceptions\OperationFailure $exception) {
153
                $operationReference = is_string($exception->getMessage()) 
154
                    ? $exception->getMessage() 
155
                    : PatcherOperation::TYPE_UNKNOWN;
156
157
                $outputRecords[$type] = $exception->getOutput(); 
158
                    
159
                $messageArgs = array(
160
                    strtoupper($operationReference),
161
                    $type,
162
                    $this->extractStringValue($args, PluginConfig::PATCHER_ARG_LEVEL)
163
                );
164
165
                $this->logger->writeVerbose('warning', '%s (type=%s) failed with p=%s', $messageArgs);
166
            }
167
        }
168
169
        $failure = new \Vaimo\ComposerPatches\Exceptions\ApplierFailure();
170
171
        $failure->setErrors(
172
            $this->collectErrors($outputRecords)
173
        );
174
175
        throw $failure;
176
    }
177
    
178
    private function collectErrors(array $outputRecords)
179
    {
180
        $errors = array(
181
            'failed',
182
            'unexpected',
183
            'malformed',
184
            'error',
185
            'corrupt',
186
            'can\'t find file',
187
            'patch unexpectedly ends'
188
        );
189
        
190
        $errorMatcher = sprintf('/%s/i', implode('|', $errors));
191
        
192
        foreach ($outputRecords as $type => $output) {
193
            $messages = preg_grep('/^[^\|-]/i', explode(PHP_EOL, $output));
194
            $matches = preg_grep($errorMatcher, $messages);
195
196
            $outputRecords[$type] = reset($matches);
197
        }
198
        
199
        return $outputRecords;
200
    }
201
202
    private function processOperationItems($patcher, $operations, $args, $failures)
203
    {
204
        $operationResults = array_fill_keys(array_keys($operations), '');
205
206
        $result = true;
207
208
        foreach (array_keys($operations) as $operationCode) {
209
            if (!isset($patcher[$operationCode])) {
210
                continue;
211
            }
212
213
            $args = array_replace($args, $operationResults);
214
215
            $applierOperations = is_array($patcher[$operationCode])
216
                ? $patcher[$operationCode]
217
                : array($patcher[$operationCode]);
218
219
220
            $operationFailures = $this->extractArrayValue($failures, $operationCode);
221
222
            list($result, $output) = $this->resolveOperationOutput(
223
                $applierOperations, 
224
                $args, 
225
                $operationFailures
226
            );
227
228
            if ($output !== false) {
229
                $operationResults[$operationCode] = $output;
230
            }
231
232
            if ($result) {
233
                continue;
234
            }
235
236
            $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure($operationCode);
237
238
            $failure->setOutput($output);
239
            
240
            throw $failure;
241
        }
242
243
        return $result;
244
    }
245
246
    private function resolveOperationOutput($applierOperations, $args, $operationFailures)
247
    {
248
        $variableFormats = array(
249
            '{{%s}}' => array('escapeshellarg'),
250
            '[[%s]]' => array()
251
        );
252
253
        $output = '';
254
        
255
        foreach ($applierOperations as $operation) {
256
            $passOnFailure = strpos($operation, '!') === 0;
257
            $operation = ltrim($operation, '!');
258
259
            $command = $this->templateUtils->compose($operation, $args, $variableFormats);
260
261
            $cwd = $this->extractStringValue($args, PluginConfig::PATCHER_ARG_CWD);
262
263
            $resultKey = sprintf('%s | %s', $cwd, $command);
264
265
            if ($passOnFailure) {
266
                $this->logger->writeVerbose(
267
                    \Vaimo\ComposerPatches\Logger::TYPE_NONE,
268
                    '<comment>***</comment> '
269
                    . 'The expected result to execution is a failure'
270
                    . '<comment>***</comment>'
271
                );
272
            }
273
274
            if (!isset($this->resultCache[$resultKey])) {
275
                $this->resultCache[$resultKey] = $this->shell->execute($command, $cwd);
276
            }
277
278
            list($result, $output) = $this->resultCache[$resultKey];
279
280
            if ($result) {
281
                $result = $this->scanOutputForFailures($output, $operationFailures);
282
            }
283
284
            if ($passOnFailure) {
285
                $result = !$result;
286
            }
287
288
            if (!$result) {
289
                continue;
290
            }
291
292
            return array($result, $output);
293
        }
294
295
        return array(false, $output);
296
    }
297
298
    private function scanOutputForFailures($output, array $failureMessages)
299
    {
300
        foreach ($failureMessages as $patternCode => $pattern) {
301
            if (!$pattern || !preg_match($pattern, $output)) {
302
                continue;
303
            }
304
305
            $this->logger->writeVerbose(
306
                'warning',
307
                sprintf(
308
                    'Success changed to FAILURE due to output analysis (%s): %s',
309
                    $patternCode,
310
                    $pattern
311
                )
312
            );
313
314
            return false;
315
        }
316
317
        return true;
318
    }
319
320
    private function extractArrayValue($data, $key)
321
    {
322
        return $this->extractValue($data, $key, array());
323
    }
324
325
    private function extractStringValue($data, $key)
326
    {
327
        return $this->extractValue($data, $key, '');
328
    }
329
330
    private function extractValue($data, $key, $default)
331
    {
332
        return isset($data[$key]) ? $data[$key] : $default;
333
    }
334
}
335