Passed
Push — master ( b3a69d...79ebcc )
by Allan
02:21 queued 11s
created

Applier::collectErrors()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 2
dl 0
loc 22
rs 9.8666
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 \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
        $this->applierErrorFactory = new \Vaimo\ComposerPatches\Factories\ApplierErrorFactory();
0 ignored issues
show
Bug Best Practice introduced by
The property applierErrorFactory does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
70
    }
71
72
    public function applyFile($filename, $cwd, array $config = array())
73
    {
74
        $applierConfig = $this->applierUtils->mergeApplierConfig($this->config, array_filter($config));
75
        $applierConfig = $this->applierUtils->sortApplierConfig($applierConfig);
76
        $patchers = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_APPLIERS);
77
        $operations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_OPERATIONS);
78
        $levels = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_LEVELS);
79
        $failureMessages = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_FAILURES);
80
        $sanityOperations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_SANITY);
81
82
        $operations = array_filter($operations, function ($item) {
83
            return $item !== false;
84
        });
85
86
        try {
87
            $this->applierUtils->validateConfig($applierConfig);
88
        } catch (\Vaimo\ComposerPatches\Exceptions\ConfigValidationException $exception) {
89
            $this->logger->writeVerbose('error', $exception->getMessage());
90
        }
91
92
        $arguments = array(
93
            PluginConfig::PATCHER_ARG_FILE => $filename,
94
            PluginConfig::PATCHER_ARG_CWD => $cwd
95
        );
96
97
        $patcherName = $this->executeOperations($patchers, $sanityOperations, $arguments);
98
99
        if (!$patcherName) {
100
            $message = sprintf(
101
                'None of the patch appliers seem to be available (tried: %s)',
102
                implode(', ', array_keys($patchers))
103
            );
104
105
            throw new \Vaimo\ComposerPatches\Exceptions\RuntimeException($message);
106
        }
107
108
        $errorGroups = array();
109
110
        foreach ($levels as $patchLevel) {
111
            $arguments = array_replace(
112
                $arguments,
113
                array(PluginConfig::PATCHER_ARG_LEVEL => $patchLevel)
114
            );
115
116
            try {
117
                $patcherName = $this->executeOperations($patchers, $operations, $arguments, $failureMessages);
118
            } catch (\Vaimo\ComposerPatches\Exceptions\ApplierFailure $exception) {
119
                $errorGroups[] = $exception->getErrors();
120
                continue;
121
            }
122
123
            $this->logger->writeVerbose(
124
                'info',
125
                'SUCCESS with type=%s (p=%s)',
126
                array($patcherName, $patchLevel)
127
            );
128
129
            return;
130
        }
131
132
        $failure = new \Vaimo\ComposerPatches\Exceptions\ApplierFailure(
133
            sprintf('Cannot apply patch %s', $filename)
134
        );
135
136
        $failure->setErrors(
137
            array_reduce($errorGroups, 'array_merge_recursive', array())
138
        );
139
140
        throw $failure;
141
    }
142
143
    private function executeOperations(
144
        array $patchers,
145
        array $operations,
146
        array $args = array(),
147
        array $failures = array()
148
    ) {
149
        $outputRecords = array();
150
151
        foreach ($patchers as $type => $patcher) {
152
            if (!$patcher) {
153
                continue;
154
            }
155
156
            try {
157
                return $this->processOperationItems($patcher, $operations, $args, $failures);
158
            } catch (\Vaimo\ComposerPatches\Exceptions\OperationFailure $exception) {
159
                $operationReference = is_string($exception->getMessage())
160
                    ? $exception->getMessage()
161
                    : PatcherOperation::TYPE_UNKNOWN;
162
163
                $outputRecords[$type] = $exception->getOutput();
164
165
                $messageArgs = array(
166
                    strtoupper($operationReference),
167
                    $type,
168
                    $this->extractStringValue($args, PluginConfig::PATCHER_ARG_LEVEL)
169
                );
170
171
                $this->logger->writeVerbose('warning', '%s (type=%s) failed with p=%s', $messageArgs);
172
            }
173
        }
174
175
        throw $this->applierErrorFactory->create($outputRecords);
176
    }
177
178
    private function processOperationItems($patcher, $operations, $args, $failures)
179
    {
180
        $operationResults = array_fill_keys(array_keys($operations), '');
181
        $result = true;
182
183
        foreach (array_keys($operations) as $operationCode) {
184
            if (!isset($patcher[$operationCode])) {
185
                continue;
186
            }
187
188
            $args = array_replace($args, $operationResults);
189
            $operationFailures = $this->extractArrayValue($failures, $operationCode);
190
            $applierOperations = is_array($patcher[$operationCode])
191
                ? $patcher[$operationCode]
192
                : array($patcher[$operationCode]);
193
194
            list($result, $output) = $this->resolveOperationOutput($applierOperations, $args, $operationFailures);
195
196
            if ($output !== false) {
197
                $operationResults[$operationCode] = $output;
198
            }
199
200
            if ($result) {
201
                continue;
202
            }
203
204
            $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure($operationCode);
205
            $failure->setOutput(explode(PHP_EOL, $output));
206
207
            throw $failure;
208
        }
209
210
        return $result;
211
    }
212
213
    private function resolveOperationOutput($applierOperations, $args, $operationFailures)
214
    {
215
        $variableFormats = array(
216
            '{{%s}}' => array('escapeshellarg'),
217
            '[[%s]]' => array()
218
        );
219
220
        $output = '';
221
222
        foreach ($applierOperations as $operation) {
223
            $passOnFailure = strpos($operation, '!') === 0;
224
            $operation = ltrim($operation, '!');
225
            $command = $this->templateUtils->compose($operation, $args, $variableFormats);
226
            $cwd = $this->extractStringValue($args, PluginConfig::PATCHER_ARG_CWD);
227
            $resultKey = sprintf('%s | %s', $cwd, $command);
228
229
            if ($passOnFailure) {
230
                $this->logger->writeVerbose(
231
                    \Vaimo\ComposerPatches\Logger::TYPE_NONE,
232
                    '<comment>***</comment> '
233
                    . 'The expected result to execution is a failure'
234
                    . '<comment>***</comment>'
235
                );
236
            }
237
238
            if (isset($this->resultCache[$resultKey])) {
239
                $this->logger->writeVerbose(
240
                    \Vaimo\ComposerPatches\Logger::TYPE_NONE,
241
                    sprintf('(using cached result for: %s = %s)', $command, reset($this->resultCache[$resultKey]))
242
                );
243
            }
244
245
            if (!isset($this->resultCache[$resultKey])) {
246
                $this->resultCache[$resultKey] = $this->shell->execute($command, $cwd);
247
            }
248
249
            list($result, $output) = $this->resultCache[$resultKey];
250
251
            if ($result) {
252
                $this->validateOutput($output, $operationFailures);
253
            }
254
255
            if ($passOnFailure) {
256
                $result = !$result;
257
            }
258
259
            if (!$result) {
260
                continue;
261
            }
262
263
            return array($result, $output);
264
        }
265
266
        return array(false, $output);
267
    }
268
269
    private function validateOutput($output, $operationFailures)
270
    {
271
        $pathMarker = '\|\+\+\+\s(?P<match>.*?)(\t|$)';
272
        $pathMatcher = sprintf('/^%s/', $pathMarker);
273
274
        $failures = $this->outputAnalyser->scanOutputForFailures($output, $pathMatcher, $operationFailures);
275
276
        if (!$failures) {
277
            return;
278
        }
279
280
        foreach ($failures as $patternCode => $items) {
281
            foreach ($items as $index => $item) {
282
                if (preg_match($pathMatcher, $item)) {
283
                    continue;
284
                }
285
286
                $message = sprintf(
287
                    'Success changed to FAILURE due to output analysis (%s):',
288
                    $patternCode
289
                );
290
291
                $failures[$patternCode][$index] = implode(PHP_EOL, array($message, $item));
292
293
                $this->logger->writeVerbose(
294
                    'warning',
295
                    sprintf('%s: %s', $message, $operationFailures[$patternCode])
296
                );
297
            }
298
        }
299
300
        $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure('Output analysis failed');
301
302
        throw $failure->setOutput(
303
            array_reduce($failures, 'array_merge', array())
304
        );
305
    }
306
307
    private function extractArrayValue($data, $key)
308
    {
309
        return $this->extractValue($data, $key, array());
310
    }
311
312
    private function extractStringValue($data, $key)
313
    {
314
        return $this->extractValue($data, $key, '');
315
    }
316
317
    private function extractValue($data, $key, $default)
318
    {
319
        return isset($data[$key]) ? $data[$key] : $default;
320
    }
321
}
322