Applier   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 35
eloc 148
c 6
b 1
f 0
dl 0
loc 313
rs 9.6

10 Methods

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