Passed
Push — master ( 11eaad...3f8f84 )
by Allan
02:05
created

src/Patch/File/Applier.php (1 issue)

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
322
            return true;
323
        }
324
        
325
        $lines = explode(PHP_EOL, $output);
326
        
327
        $matches = array();
328
        
329
        foreach ($lines as $line) {
330
            if (preg_match($pathMatcher, $line)) {
331
                $matches[] = $line;
332
333
                continue;
334
            }
335
            
336
            foreach ($patternsWithResults as $patternCode => $pattern) {
337
                if (!preg_match($pattern, $line)) {
338
                    continue;
339
                }
340
341
                $message = sprintf(
342
                    'Success changed to FAILURE due to output analysis (%s)',
343
                    $patternCode
344
                );
345
                
346
                $matches[] = $message;
347
348
                $this->logger->writeVerbose(
349
                    'warning',
350
                    sprintf('%s: %s', $message, $pattern)
351
                );
352
            }
353
        }
354
        
355
        $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure('Output analysis failed');
356
357
        $failure->setOutput(
358
            implode(PHP_EOL, $matches)
359
        );
360
361
        throw $failure;
362
    }
363
364
    private function extractArrayValue($data, $key)
365
    {
366
        return $this->extractValue($data, $key, array());
367
    }
368
369
    private function extractStringValue($data, $key)
370
    {
371
        return $this->extractValue($data, $key, '');
372
    }
373
374
    private function extractValue($data, $key, $default)
375
    {
376
        return isset($data[$key]) ? $data[$key] : $default;
377
    }
378
}
379