Passed
Push — master ( 79ebcc...8d3dd7 )
by Allan
02:23 queued 11s
created

Applier::outputBehaviourContextInfo()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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