Passed
Push — master ( e3bb8d...817f2e )
by Allan
02:41
created

Applier::processOperationItems()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 10
nop 4
dl 0
loc 32
rs 9.1111
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
10
class Applier
11
{
12
    /**
13
     * @var \Vaimo\ComposerPatches\Logger
14
     */
15
    private $logger;
16
    
17
    /**
18
     * @var \Vaimo\ComposerPatches\Shell
19
     */
20
    private $shell;
21
22
    /**
23
     * @var array
24
     */
25
    private $config;
26
27
    /**
28
     * @var \Vaimo\ComposerPatches\Utils\ConfigUtils
29
     */
30
    private $applierUtils;
31
32
    /**
33
     * @var \Vaimo\ComposerPatches\Utils\TemplateUtils
34
     */
35
    private $templateUtils;
36
37
    /**
38
     * @var array
39
     */
40
    private $resultCache;
41
    
42
    /**
43
     * @param \Vaimo\ComposerPatches\Logger $logger
44
     * @param array $config
45
     */
46
    public function __construct(
47
        \Vaimo\ComposerPatches\Logger $logger,
48
        array $config
49
    ) {
50
        $this->logger = $logger;
51
        $this->config = $config;
52
53
        $this->shell = new \Vaimo\ComposerPatches\Shell($logger);
54
        $this->applierUtils = new \Vaimo\ComposerPatches\Utils\ConfigUtils();
55
        $this->templateUtils = new \Vaimo\ComposerPatches\Utils\TemplateUtils();
56
    }
57
58
    public function applyFile($filename, $cwd, array $config = array())
59
    {
60
        $applierConfig = $this->applierUtils->mergeApplierConfig($this->config, array_filter($config));
61
        
62
        $applierConfig = $this->applierUtils->sortApplierConfig($applierConfig);
63
64
        $patchers = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_APPLIERS);
65
        $operations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_OPERATIONS);
66
        $levels = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_LEVELS);
67
        $failureMessages = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_FAILURES);
68
69
        $sanityOperations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_SANITY);
70
        
71
        try {
72
            $this->applierUtils->validateConfig($applierConfig);
73
        } catch (\Vaimo\ComposerPatches\Exceptions\ConfigValidationException $exception) {
74
            $this->logger->writeVerbose('error', $exception->getMessage());
75
        }
76
77
        $arguments = array(
78
            PluginConfig::PATCHER_ARG_FILE => $filename,
79
            PluginConfig::PATCHER_ARG_CWD => $cwd
80
        );
81
            
82
        $patcherName = $this->executeOperations($patchers, $sanityOperations, $arguments);
83
        
84
        if (!$patcherName) {
85
            $message = sprintf(
86
                'None of the patch appliers seem to be available (tried: %s)',
87
                implode(', ', array_keys($patchers))
88
            );
89
            
90
            throw new \Vaimo\ComposerPatches\Exceptions\RuntimeException($message);
91
        }
92
93
        foreach ($levels as $patchLevel) {
94
            $arguments = array_replace(
95
                $arguments,
96
                array(PluginConfig::PATCHER_ARG_LEVEL => $patchLevel)
97
            );
98
            
99
            $patcherName = $this->executeOperations(
100
                $patchers,
101
                $operations,
102
                $arguments,
103
                $failureMessages
104
            );
105
            
106
            if (!$patcherName) {
107
                continue;
108
            }
109
110
            $this->logger->writeVerbose(
111
                'info',
112
                'SUCCESS with type=%s (p=%s)',
113
                array($patcherName, $patchLevel)
114
            );
115
116
            return;
117
        }
118
119
        throw new \Exception(
120
            sprintf('Cannot apply patch %s', $filename)
121
        );
122
    }
123
    
124
    private function executeOperations($patchers, array $operations, array $args = array(), array $failures = array())
125
    {
126
        list($operationName, $operationCode) = array_fill(0, 4, 'UNKNOWN');
127
        
128
        foreach ($patchers as $type => $patcher) {
129
            if (!$patcher) {
130
                continue;
131
            }
132
            
133
            $result = $this->processOperationItems($patcher, $operations, $args, $failures);
134
            
135
            if ($result) {
136
                return $type;
137
            }
138
            
139
            $messageArgs = array(
140
                is_string($operationName) ? $operationName : $operationCode,
141
                $type,
142
                $this->extractStringValue($args, PluginConfig::PATCHER_ARG_LEVEL)
143
            );
144
            
145
            $this->logger->writeVerbose('warning', '%s (type=%s) failed with p=%s', $messageArgs);
146
        }
147
        
148
        return '';
149
    }
150
    
151
    private function processOperationItems($patcher, $operations, $args, $failures)
152
    {
153
        $operationResults = array_fill_keys(array_keys($operations), '');
154
155
        $result = true;
156
157
        foreach ($operations as $operationCode => $operationName) {
158
            if (!isset($patcher[$operationCode])) {
159
                continue;
160
            }
161
162
            $args = array_replace($args, $operationResults);
163
164
            $applierOperations = is_array($patcher[$operationCode])
165
                ? $patcher[$operationCode]
166
                : array($patcher[$operationCode]);
167
168
169
            $operationFailures = $this->extractArrayValue($failures, $operationCode);
170
            
171
            $output = $this->getOperationOutput($applierOperations, $args, $operationFailures);
172
            
173
            if ($output !== false) {
174
                $operationResults[$operationCode] = $output;
175
            }
176
            
177
            if (!$result) {
178
                break;
179
            }
180
        }
181
        
182
        return $result;
183
    }
184
    
185
    private function getOperationOutput($applierOperations, $args, $operationFailures)
186
    {
187
        $variableFormats = array(
188
            '{{%s}}' => array('escapeshellarg'),
189
            '[[%s]]' => array()
190
        );
191
        
192
        foreach ($applierOperations as $operation) {
193
            $passOnFailure = strpos($operation, '!') === 0;
194
            $operation = ltrim($operation, '!');
195
196
            $command = $this->templateUtils->compose($operation, $args, $variableFormats);
197
198
            $cwd = $this->extractStringValue($args, PluginConfig::PATCHER_ARG_CWD);
199
200
            $resultKey = sprintf('%s | %s', $cwd, $command);
201
202
            if ($passOnFailure) {
203
                $this->logger->writeVerbose(
204
                    \Vaimo\ComposerPatches\Logger::TYPE_NONE,
205
                    '<comment>***</comment> '
206
                    . 'The expected result to execution is a failure'
207
                    . '<comment>***</comment>'
208
                );
209
            }
210
211
            if (!isset($this->resultCache[$resultKey])) {
212
                $this->resultCache[$resultKey] = $this->shell->execute($command, $cwd);
213
            }
214
215
            list($result, $output) = $this->resultCache[$resultKey];
216
217
            if ($result) {
218
                $result = $this->scanOutputForFailures($output, $operationFailures);
219
            }
220
221
            if ($passOnFailure) {
222
                $result = !$result;
223
            }
224
225
            if (!$result) {
226
                continue;
227
            }
228
229
            return $output;
230
        }
231
        
232
        return false;
233
    }
234
    
235
    private function scanOutputForFailures($output, array $failureMessages)
236
    {
237
        foreach ($failureMessages as $patternCode => $pattern) {
238
            if (!$pattern || !preg_match($pattern, $output)) {
239
                continue;
240
            }
241
242
            $this->logger->writeVerbose(
243
                'warning',
244
                sprintf(
245
                    'Success changed to FAILURE due to output analysis (%s): %s',
246
                    $patternCode,
247
                    $pattern
248
                )
249
            );
250
251
            return false;
252
        }
253
        
254
        return true;
255
    }
256
257
    private function extractArrayValue($data, $key)
258
    {
259
        return $this->extractValue($data, $key, array());
260
    }
261
262
    private function extractStringValue($data, $key)
263
    {
264
        return $this->extractValue($data, $key, '');
265
    }
266
267
    private function extractValue($data, $key, $default)
268
    {
269
        return isset($data[$key]) ? $data[$key] : $default;
270
    }
271
}
272