Passed
Push — master ( bd4ad1...e3bb8d )
by Allan
03:57
created

PatchesApplier::shouldAllowOutput()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 2
dl 0
loc 4
rs 10
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\Repository;
7
8
use Composer\Repository\WritableRepositoryInterface as Repository;
9
use Composer\Package\PackageInterface as Package;
10
use Vaimo\ComposerPatches\Patch\Definition as Patch;
11
12
class PatchesApplier
13
{
14
    /**
15
     * @var \Vaimo\ComposerPatches\Package\Collector
16
     */
17
    private $packageCollector;
18
19
    /**
20
     * @var \Vaimo\ComposerPatches\Managers\RepositoryManager
21
     */
22
    private $repositoryManager;
23
24
    /**
25
     * @var \Vaimo\ComposerPatches\Package\PatchApplier
26
     */
27
    private $packagePatchApplier;
28
29
    /**
30
     * @var \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator
31
     */
32
    private $queueGenerator;
33
34
    /**
35
     * @var \Vaimo\ComposerPatches\Managers\PatcherStateManager
36
     */
37
    private $patcherStateManager;
38
39
    /**
40
     * @var \Vaimo\ComposerPatches\Repository\StateGenerator
41
     */
42
    private $repositoryStateGenerator;
43
44
    /**
45
     * @var \Vaimo\ComposerPatches\Package\PatchApplier\InfoLogger
46
     */
47
    private $patchInfoLogger;
48
49
    /**
50
     * @var \Vaimo\ComposerPatches\Strategies\OutputStrategy
51
     */
52
    private $outputStrategy;
53
    
54
    /**
55
     * @var \Vaimo\ComposerPatches\Logger
56
     */
57
    private $logger;
58
59
    /**
60
     * @var \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser
61
     */
62
    private $patchListAnalyser;
63
    
64
    /**
65
     * @var \Vaimo\ComposerPatches\Patch\DefinitionList\Transformer
66
     */
67
    private $patchListTransformer;
68
    
69
    /**
70
     * @var \Vaimo\ComposerPatches\Package\PatchApplier\StatusConfig
71
     */
72
    private $statusConfig;
73
    
74
    /**
75
     * @var \Vaimo\ComposerPatches\Utils\PackageUtils
76
     */
77
    private $packageUtils;
78
79
    /**
80
     * \Vaimo\ComposerPatches\Utils\PatchListUtils
81
     */
82
    private $patchListUtils;
83
    
84
    /**
85
     * @param \Vaimo\ComposerPatches\Package\Collector $packageCollector
86
     * @param \Vaimo\ComposerPatches\Managers\RepositoryManager $repositoryManager
87
     * @param \Vaimo\ComposerPatches\Package\PatchApplier $patchApplier
88
     * @param \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator $queueGenerator
89
     * @param \Vaimo\ComposerPatches\Managers\PatcherStateManager $patcherStateManager
90
     * @param \Vaimo\ComposerPatches\Package\PatchApplier\InfoLogger $patchInfoLogger
91
     * @param \Vaimo\ComposerPatches\Strategies\OutputStrategy $outputStrategy
92
     * @param \Vaimo\ComposerPatches\Logger $logger
93
     */
94
    public function __construct(
95
        \Vaimo\ComposerPatches\Package\Collector $packageCollector,
96
        \Vaimo\ComposerPatches\Managers\RepositoryManager $repositoryManager,
97
        \Vaimo\ComposerPatches\Package\PatchApplier $patchApplier,
98
        \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator $queueGenerator,
99
        \Vaimo\ComposerPatches\Managers\PatcherStateManager $patcherStateManager,
100
        \Vaimo\ComposerPatches\Package\PatchApplier\InfoLogger $patchInfoLogger,
101
        \Vaimo\ComposerPatches\Strategies\OutputStrategy $outputStrategy,
102
        \Vaimo\ComposerPatches\Logger $logger
103
    ) {
104
        $this->packageCollector = $packageCollector;
105
        $this->repositoryManager = $repositoryManager;
106
        $this->packagePatchApplier = $patchApplier;
107
        $this->queueGenerator = $queueGenerator;
108
        $this->patcherStateManager = $patcherStateManager;
109
        $this->patchInfoLogger = $patchInfoLogger;
110
        $this->logger = $logger;
111
112
        $this->repositoryStateGenerator = new \Vaimo\ComposerPatches\Repository\StateGenerator(
113
            $this->packageCollector
114
        );
115
116
        $this->outputStrategy = $outputStrategy;
117
118
        $this->patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
119
        $this->patchListTransformer = new \Vaimo\ComposerPatches\Patch\DefinitionList\Transformer();
120
        $this->statusConfig = new \Vaimo\ComposerPatches\Package\PatchApplier\StatusConfig();
121
        $this->packageUtils = new \Vaimo\ComposerPatches\Utils\PackageUtils();
122
        $this->patchListUtils = new \Vaimo\ComposerPatches\Utils\PatchListUtils();
123
    }
124
125
    /**
126
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
127
     * @SuppressWarnings(PHPMD.NPathComplexity)
128
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
129
     *
130
     * @param Repository $repository
131
     * @param array $patches
132
     * @return bool
133
     * @throws \Vaimo\ComposerPatches\Exceptions\PackageNotFound
134
     * @throws \Vaimo\ComposerPatches\Exceptions\PackageResetException
135
     */
136
    public function apply(Repository $repository, array $patches)
137
    {
138
        $packages = $this->packageCollector->collect($repository);
139
140
        $packagesUpdated = false;
141
142
        $repositoryState = $this->repositoryStateGenerator->generate($repository);
143
        
144
        $applyQueue = $this->queueGenerator->generateApplyQueue($patches, $repositoryState);
145
        $removeQueue = $this->queueGenerator->generateRemovalQueue($applyQueue, $repositoryState);
146
        $resetQueue = $this->queueGenerator->generateResetQueue($applyQueue);
147
        
148
        $applyQueue = array_map('array_filter', $applyQueue);
149
        
150
        $patchQueueFootprints = $this->patchListTransformer->createSimplifiedList($applyQueue);
151
152
        $labels = array_diff_key($this->statusConfig->getLabels(), array('unknown' => true));
153
154
        $applyQueue = $this->updateStatusLabels($applyQueue, $labels);
155
        $removeQueue = $this->updateStatusLabels($removeQueue, $labels);
156
        
157
        foreach ($packages as $packageName => $package) {
158
            $hasPatches = !empty($applyQueue[$packageName]);
159
160
            $patchTargets = $hasPatches ?
161
                $this->patchListAnalyser->getAllTargets(array($applyQueue[$packageName]))
162
                : array($packageName);
163
                
164
            $itemsToReset = array_intersect($resetQueue, $patchTargets);
165
166
            $resetResult = array();
167
168
            foreach ($itemsToReset as $targetName) {
169
                $resetTarget = $packages[$targetName];
170
171
                $resetPatches = $this->packageUtils->resetAppliedPatches($resetTarget);
172
                $resetResult[$targetName] = is_array($resetPatches) ? $resetPatches : array();
173
174
                if (!$hasPatches && $resetPatches && !isset($patchQueueFootprints[$targetName])) {
175
                    $this->logger->writeRaw(
176
                        'Resetting patches for <info>%s</info> (%s)',
177
                        array($targetName, count($resetResult[$targetName]))
178
                    );
179
                }
180
181
                $this->repositoryManager->resetPackage($repository, $resetTarget);
182
183
                $packagesUpdated = $packagesUpdated || (bool)$resetResult[$targetName];
184
            }
185
            
186
            $resetQueue = array_diff($resetQueue, $patchTargets);
187
188
            if (!$hasPatches) {
189
                continue;
190
            }
191
            
192
            $changedTargets = $this->resolveChangedTargets($packages, $patchTargets, $patchQueueFootprints);
193
            
194
            if (empty($changedTargets)) {
195
                continue;
196
            }
197
198
            $queuedPatches = array_filter(
199
                $applyQueue[$packageName],
200
                function ($data) use ($changedTargets) {
201
                    return array_intersect($data[Patch::TARGETS], $changedTargets);
202
                }
203
            );
204
            
205
            $muteDepth = null;
206
            
207
            $patchRemovals = isset($removeQueue[$packageName])
208
                ? $removeQueue[$packageName]
209
                : array();
210
            
211
            if (!$this->shouldAllowOutput($queuedPatches, $patchRemovals)) {
212
                $muteDepth = $this->logger->mute();
213
            }
214
215
            $this->logger->writeRaw(
216
                'Applying patches for <info>%s</info> (%s)',
217
                array($packageName, count($queuedPatches))
218
            );
219
            
220
            $this->processQueues($repository, $package, $queuedPatches, $patchRemovals);
221
222
            $packagesUpdated = true;
223
224
            $this->logger->writeNewLine();
225
226
            if ($muteDepth !== null) {
227
                $this->logger->unMute($muteDepth);
228
            }
229
        }
230
231
        return $packagesUpdated;
232
    }
233
234
    private function updateStatusLabels(array $queue, array $labels)
235
    {
236
        foreach ($queue as $target => $group) {
237
            foreach ($group as $path => $item) {
238
                $status = isset($item[Patch::STATUS]) ? $item[Patch::STATUS] : 'unknown';
239
240
                if (!isset($labels[$status])) {
241
                    continue;
242
                }
243
244
                $queue[$target][$path][Patch::STATUS_LABEL] = $labels[$status];
245
            }
246
        }
247
248
        return $queue;
249
    }
250
    
251
    private function processQueues(Repository $repository, Package $package, $additions, $removals)
252
    {
253
        try {
254
            if ($removals) {
255
                $processIndentation = $this->logger->push('~');
256
257
                foreach ($removals as $item) {
258
                    $this->patchInfoLogger->outputPatchInfo($item);
259
                }
260
261
                $this->logger->reset($processIndentation);
262
            }
263
264
            $this->processPatchesForPackage($repository, $package, $additions);
265
        } catch (\Exception $exception) {
266
            $this->logger->unMute();
267
268
            throw $exception;
269
        }
270
    }
271
    
272
    private function resolveChangedTargets(array $packages, array $patchTargets, array $patchFootprints)
273
    {
274
        $changesMap = array();
275
276
        foreach ($patchTargets as $targetName) {
277
            $targetQueue = array();
278
279
            if (isset($patchFootprints[$targetName])) {
280
                $targetQueue = $patchFootprints[$targetName];
281
            }
282
283
            if (!isset($packages[$targetName])) {
284
                throw new \Vaimo\ComposerPatches\Exceptions\PackageNotFound(
285
                    sprintf(
286
                        'Unknown target "%s" found when checking patch changes for: %s',
287
                        $targetName,
288
                        implode(',', array_keys($targetQueue))
289
                    )
290
                );
291
            }
292
293
            $changesMap[$targetName] = $this->packageUtils->hasPatchChanges(
294
                $packages[$targetName],
295
                $targetQueue
296
            );
297
        }
298
299
        return array_keys(array_filter($changesMap));
300
    }
301
    
302
    private function processPatchesForPackage(Repository $repository, Package $package, array $patchesQueue)
303
    {
304
        $processIndentation = $this->logger->push('~');
305
306
        try {
307
            $appliedPatches = $this->packagePatchApplier->applyPatches($package, $patchesQueue);
308
309
            $this->patcherStateManager->registerAppliedPatches($repository, $appliedPatches);
310
311
            $this->logger->reset($processIndentation);
312
        } catch (\Vaimo\ComposerPatches\Exceptions\PatchFailureException $exception) {
313
            $failedPath = $exception->getFailedPatchPath();
314
315
            $paths = array_keys($patchesQueue);
316
            $appliedPaths = array_slice($paths, 0, array_search($failedPath, $paths));
0 ignored issues
show
Bug introduced by
It seems like array_search($failedPath, $paths) can also be of type false and string; however, parameter $length of array_slice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

316
            $appliedPaths = array_slice($paths, 0, /** @scrutinizer ignore-type */ array_search($failedPath, $paths));
Loading history...
317
            $appliedPatches = array_intersect_key($patchesQueue, array_flip($appliedPaths));
318
319
            $this->patcherStateManager->registerAppliedPatches($repository, $appliedPatches);
320
321
            throw $exception;
322
        }
323
    }
324
    
325
    private function shouldAllowOutput(array $patches, array $removals)
326
    {
327
        return $this->outputStrategy->shouldAllowForPatches($patches)
328
            || $this->outputStrategy->shouldAllowForPatches($removals);
329
    }
330
}
331