PatchesApplier   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Importance

Changes 7
Bugs 1 Features 1
Metric Value
eloc 155
dl 0
loc 359
rs 9.68
c 7
b 1
f 1
wmc 34

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 33 1
A updatePackage() 0 21 3
A shouldAllowOutput() 0 4 2
A processQueues() 0 18 4
B apply() 0 81 11
A updateStatusLabels() 0 17 5
A resolveChangedTargets() 0 28 4
A processPatchesForPackage() 0 41 4
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
11
use Vaimo\ComposerPatches\Patch\Definition as Patch;
12
13
/**
14
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
15
 * @SuppressWarnings(PHPMD.TooManyFields)
16
 */
17
class PatchesApplier
18
{
19
    /**
20
     * @var \Composer\Composer
21
     */
22
    private $composer;
23
24
    /**
25
     * @var \Vaimo\ComposerPatches\Package\Collector
26
     */
27
    private $packageCollector;
28
29
    /**
30
     * @var \Vaimo\ComposerPatches\Managers\RepositoryManager
31
     */
32
    private $repositoryManager;
33
34
    /**
35
     * @var \Vaimo\ComposerPatches\Package\PatchApplier
36
     */
37
    private $packagePatchApplier;
38
39
    /**
40
     * @var \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator
41
     */
42
    private $queueGenerator;
43
44
    /**
45
     * @var \Vaimo\ComposerPatches\Managers\PatcherStateManager
46
     */
47
    private $patcherStateManager;
48
49
    /**
50
     * @var \Vaimo\ComposerPatches\Repository\StateGenerator
51
     */
52
    private $repoStateGenerator;
53
54
    /**
55
     * @var \Vaimo\ComposerPatches\Package\PatchApplier\InfoLogger
56
     */
57
    private $patchInfoLogger;
58
59
    /**
60
     * @var \Vaimo\ComposerPatches\Strategies\OutputStrategy
61
     */
62
    private $outputStrategy;
63
64
    /**
65
     * @var \Vaimo\ComposerPatches\Logger
66
     */
67
    private $logger;
68
69
    /**
70
     * @var \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser
71
     */
72
    private $patchListAnalyser;
73
74
    /**
75
     * @var \Vaimo\ComposerPatches\Patch\DefinitionList\Transformer
76
     */
77
    private $patchListTransformer;
78
79
    /**
80
     * @var \Vaimo\ComposerPatches\Package\PatchApplier\StatusConfig
81
     */
82
    private $statusConfig;
83
84
    /**
85
     * @var \Vaimo\ComposerPatches\Utils\PackageUtils
86
     */
87
    private $packageUtils;
88
89
    /**
90
     * @var \Vaimo\ComposerPatches\Utils\DataUtils
91
     */
92
    private $dataUtils;
93
94
    /**
95
     * @var \Vaimo\ComposerPatches\Console\OutputGenerator
96
     */
97
    private $outputGenerator;
98
99
    /**
100
     * @var \Vaimo\ComposerPatches\Compatibility\Executor
101
     */
102
    private $compExecutor;
103
104
    public function __construct(
105
        \Composer\Composer $composer,
106
        \Vaimo\ComposerPatches\Package\Collector $packageCollector,
107
        \Vaimo\ComposerPatches\Managers\RepositoryManager $repositoryManager,
108
        \Vaimo\ComposerPatches\Package\PatchApplier $patchApplier,
109
        \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator $queueGenerator,
110
        \Vaimo\ComposerPatches\Managers\PatcherStateManager $patcherStateManager,
111
        \Vaimo\ComposerPatches\Package\PatchApplier\InfoLogger $patchInfoLogger,
112
        \Vaimo\ComposerPatches\Strategies\OutputStrategy $outputStrategy,
113
        \Vaimo\ComposerPatches\Logger $logger
114
    ) {
115
        $this->composer = $composer;
116
        $this->packageCollector = $packageCollector;
117
        $this->repositoryManager = $repositoryManager;
118
        $this->packagePatchApplier = $patchApplier;
119
        $this->queueGenerator = $queueGenerator;
120
        $this->patcherStateManager = $patcherStateManager;
121
        $this->patchInfoLogger = $patchInfoLogger;
122
        $this->outputStrategy = $outputStrategy;
123
        $this->logger = $logger;
124
125
        $this->repoStateGenerator = new \Vaimo\ComposerPatches\Repository\StateGenerator(
126
            $this->packageCollector
127
        );
128
129
        $this->outputGenerator = new \Vaimo\ComposerPatches\Console\OutputGenerator($logger);
130
131
        $this->patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
132
        $this->patchListTransformer = new \Vaimo\ComposerPatches\Patch\DefinitionList\Transformer();
133
        $this->statusConfig = new \Vaimo\ComposerPatches\Package\PatchApplier\StatusConfig();
134
        $this->packageUtils = new \Vaimo\ComposerPatches\Utils\PackageUtils();
135
        $this->dataUtils = new \Vaimo\ComposerPatches\Utils\DataUtils();
136
        $this->compExecutor = new \Vaimo\ComposerPatches\Compatibility\Executor();
137
    }
138
139
    /**
140
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
141
     * @SuppressWarnings(PHPMD.NPathComplexity)
142
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
143
     *
144
     * @param Repository $repository
145
     * @param array $patches
146
     * @return bool
147
     * @throws \Vaimo\ComposerPatches\Exceptions\PackageNotFound
148
     * @throws \Vaimo\ComposerPatches\Exceptions\PackageResetException
149
     * @throws \Vaimo\ComposerPatches\Exceptions\PatchFailureException
150
     */
151
    public function apply(Repository $repository, array $patches)
152
    {
153
        $packages = $this->packageCollector->collect($repository);
154
        $packagesUpdated = false;
155
        $repositoryState = $this->repoStateGenerator->generate($repository);
156
        $applyQueue = $this->queueGenerator->generateApplyQueue($patches, $repositoryState);
157
        $removeQueue = $this->queueGenerator->generateRemovalQueue($applyQueue, $repositoryState);
158
        $resetQueue = $this->queueGenerator->generateResetQueue($applyQueue);
159
        $applyQueue = array_map('array_filter', $applyQueue);
160
        $patchQueueFootprints = $this->patchListTransformer->createSimplifiedList($applyQueue);
161
162
        $labels = array_diff_key(
163
            $this->statusConfig->getLabels(),
164
            array(Patch::STATUS_UNKNOWN => true)
165
        );
166
167
        $applyQueue = $this->updateStatusLabels($applyQueue, $labels);
168
        $removeQueue = $this->updateStatusLabels($removeQueue, $labels);
169
170
        foreach ($packages as $packageName => $package) {
171
            $hasPatches = !empty($applyQueue[$packageName]);
172
173
            $patchTargets = $hasPatches ?
174
                $this->patchListAnalyser->getAllTargets(array($applyQueue[$packageName]))
175
                : array($packageName);
176
177
            $itemsToReset = $this->dataUtils->extractItems($resetQueue, $patchTargets);
178
179
            $resetResult = array();
180
            $resets = array();
181
182
            foreach ($itemsToReset as $targetName) {
183
                $resetTarget = $packages[$targetName];
184
185
                $resetPatches = $this->packageUtils->resetAppliedPatches($resetTarget);
186
187
                $resetResult[$targetName] = is_array($resetPatches)
188
                    ? $resetPatches
189
                    : array();
190
191
                if (!$hasPatches && $resetPatches && !isset($patchQueueFootprints[$targetName])) {
192
                    $this->logger->writeRaw(
193
                        'Resetting patches for <info>%s</info> (%s)',
194
                        array($targetName, count($resetResult[$targetName]))
195
                    );
196
                }
197
198
                $resets[] = $this->repositoryManager->resetPackage($repository, $resetTarget);
199
            }
200
201
            $this->compExecutor->waitForCompletion($this->composer, $resets);
202
            $packagesUpdated = $packagesUpdated || (bool)array_filter($resetResult);
203
204
            if (!$hasPatches) {
205
                continue;
206
            }
207
208
            $changedTargets = $this->resolveChangedTargets($packages, $patchTargets, $patchQueueFootprints);
209
210
            if (empty($changedTargets)) {
211
                continue;
212
            }
213
214
            $queuedPatches = array_filter(
215
                $applyQueue[$packageName],
216
                function ($data) use ($changedTargets) {
217
                    return array_intersect($data[Patch::TARGETS], $changedTargets);
218
                }
219
            );
220
221
            $this->updatePackage(
222
                $package,
223
                $repository,
224
                $queuedPatches,
225
                $this->dataUtils->extractValue($removeQueue, $packageName, array())
226
            );
227
228
            $packagesUpdated = true;
229
        }
230
231
        return $packagesUpdated;
232
    }
233
234
    private function updatePackage(Package $package, Repository $repository, array $additions, array $removals)
235
    {
236
        $muteDepth = null;
237
238
        $packageName = $package->getName();
239
240
        if (!$this->shouldAllowOutput($additions, $removals)) {
241
            $muteDepth = $this->logger->mute();
242
        }
243
244
        $this->logger->writeRaw(
245
            'Applying patches for <info>%s</info> (%s)',
246
            array($packageName, count($additions))
247
        );
248
249
        $this->processQueues($package, $repository, $additions, $removals);
250
251
        $this->logger->writeNewLine();
252
253
        if ($muteDepth !== null) {
254
            $this->logger->unMute($muteDepth);
255
        }
256
    }
257
258
    private function updateStatusLabels(array $queue, array $labels)
259
    {
260
        foreach ($queue as $target => $group) {
261
            foreach ($group as $path => $item) {
262
                $status = isset($item[Patch::STATUS])
263
                    ? $item[Patch::STATUS]
264
                    : Patch::STATUS_UNKNOWN;
265
266
                if (!isset($labels[$status])) {
267
                    continue;
268
                }
269
270
                $queue[$target][$path][Patch::STATUS_LABEL] = $labels[$status];
271
            }
272
        }
273
274
        return $queue;
275
    }
276
277
    private function processQueues(Package $package, Repository $repository, $additions, $removals)
278
    {
279
        try {
280
            if ($removals) {
281
                $processIndentation = $this->logger->push('~');
282
283
                foreach ($removals as $item) {
284
                    $this->patchInfoLogger->outputPatchInfo($item);
285
                }
286
287
                $this->logger->reset($processIndentation);
288
            }
289
290
            $this->processPatchesForPackage($repository, $package, $additions);
291
        } catch (\Exception $exception) {
292
            $this->logger->unMute();
293
294
            throw $exception;
295
        }
296
    }
297
298
    private function resolveChangedTargets(array $packages, array $patchTargets, array $patchFootprints)
299
    {
300
        $changesMap = array();
301
302
        foreach ($patchTargets as $targetName) {
303
            $targetQueue = array();
304
305
            if (isset($patchFootprints[$targetName])) {
306
                $targetQueue = $patchFootprints[$targetName];
307
            }
308
309
            if (!isset($packages[$targetName])) {
310
                throw new \Vaimo\ComposerPatches\Exceptions\PackageNotFound(
311
                    sprintf(
312
                        'Unknown target "%s" found when checking patch changes for: %s',
313
                        $targetName,
314
                        implode(',', array_keys($targetQueue))
315
                    )
316
                );
317
            }
318
319
            $changesMap[$targetName] = $this->packageUtils->hasPatchChanges(
320
                $packages[$targetName],
321
                $targetQueue
322
            );
323
        }
324
325
        return array_keys(array_filter($changesMap));
326
    }
327
328
    private function processPatchesForPackage(Repository $repository, Package $package, array $patchesQueue)
329
    {
330
        $processIndentation = $this->logger->push('~');
331
332
        try {
333
            $appliedPatches = $this->packagePatchApplier->applyPatches($package, $patchesQueue);
334
335
            $this->patcherStateManager->registerAppliedPatches($repository, $appliedPatches);
336
337
            $this->logger->reset($processIndentation);
338
        } catch (\Vaimo\ComposerPatches\Exceptions\PatchFailureException $exception) {
339
            $this->logger->push();
340
341
            $this->logger->write('error', $exception->getMessage());
342
343
            $previousError = $exception->getPrevious();
344
345
            if ($previousError) {
0 ignored issues
show
introduced by
$previousError is of type Exception, thus it always evaluated to true.
Loading history...
346
                $this->outputGenerator->generateForException($previousError);
347
            }
348
349
            $failedPath = $exception->getFailedPatchPath();
350
351
            $paths = array_keys($patchesQueue);
352
353
            $failureIndex = array_search($failedPath, $paths, true);
354
355
            $appliedPatches = array();
356
357
            if (is_int($failureIndex)) {
358
                $appliedPaths = array_slice($paths, 0, $failureIndex);
359
360
                $appliedPatches = array_intersect_key(
361
                    $patchesQueue,
362
                    array_flip($appliedPaths)
363
                );
364
            }
365
366
            $this->patcherStateManager->registerAppliedPatches($repository, $appliedPatches);
367
368
            throw $exception;
369
        }
370
    }
371
372
    private function shouldAllowOutput(array $patches, array $removals)
373
    {
374
        return $this->outputStrategy->shouldAllowForPatches($patches)
375
            || $this->outputStrategy->shouldAllowForPatches($removals);
376
    }
377
}
378