ListCommand   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 208
c 8
b 0
f 1
dl 0
loc 370
rs 9.92
wmc 31

10 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 77 1
B execute() 0 104 7
A createConfigWithEnabledSources() 0 19 3
A createUnfilteredPatchLoaderPool() 0 19 1
A createLoaderPool() 0 13 2
A composerFilteredPatchesList() 0 45 4
A createQueueGenerator() 0 8 1
A createStateGenerator() 0 8 1
A createStatusLabel() 0 24 6
A generateOutput() 0 24 5
1
<?php
2
/**
3
 * Copyright © Vaimo Group. All rights reserved.
4
 * See LICENSE_VAIMO.txt for license details.
5
 */
6
namespace Vaimo\ComposerPatches\Composer\Commands;
7
8
use Symfony\Component\Console\Input\InputInterface;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
12
use Symfony\Component\Console\Input\InputArgument;
13
use Vaimo\ComposerPatches\Patch\Definition as Patch;
14
15
use Composer\Composer;
16
17
use Vaimo\ComposerPatches\Interfaces\ListResolverInterface as ListResolver;
18
use Vaimo\ComposerPatches\Repository\PatchesApplier\ListResolvers;
19
use Vaimo\ComposerPatches\Config;
20
use Vaimo\ComposerPatches\Patch\DefinitionList\LoaderComponents;
21
use Vaimo\ComposerPatches\Composer\Context as ComposerContext;
22
23
/**
24
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
25
 */
26
class ListCommand extends \Composer\Command\BaseCommand
27
{
28
    protected function configure()
29
    {
30
        parent::configure();
31
32
        $this->setName('patch:list');
33
34
        $this->setDescription('List all registered and eligible (based on project config) patches');
35
36
        $this->addArgument(
37
            'targets',
38
            InputArgument::IS_ARRAY,
39
            'Packages for the patcher to target',
40
            array()
41
        );
42
43
        $this->addOption(
44
            '--no-dev',
45
            null,
46
            InputOption::VALUE_NONE,
47
            'Disables installation of require-dev packages'
48
        );
49
50
        $this->addOption(
51
            '--filter',
52
            null,
53
            InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
54
            'Mark certain patches with MATCH in the output list'
55
        );
56
57
        $this->addOption(
58
            '--excluded',
59
            null,
60
            InputOption::VALUE_NONE,
61
            'Include patches that have been ruled out based on some constraint mismatch'
62
        );
63
64
        $this->addOption(
65
            '--with-excludes',
66
            null,
67
            InputOption::VALUE_NONE,
68
            'Alias for \'excluded\' argument'
69
        );
70
71
        $this->addOption(
72
            '--with-affected',
73
            null,
74
            InputOption::VALUE_NONE,
75
            'Mark patches that would get re-applied when changed/new patches are added (due to package reinstall)'
76
        );
77
78
        $this->addOption(
79
            '--brief',
80
            null,
81
            InputOption::VALUE_NONE,
82
            'Show more compact output of the list (remove description, owner , etc)'
83
        );
84
85
        $this->addOption(
86
            '--status',
87
            null,
88
            InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
89
            'Match specific statuses (changed, new, applied, removed)',
90
            array()
91
        );
92
93
        $this->addOption(
94
            '--from-source',
95
            null,
96
            InputOption::VALUE_NONE,
97
            'Use latest information from package configurations in vendor folder'
98
        );
99
100
        $this->addOption(
101
            '--json',
102
            null,
103
            InputOption::VALUE_NONE,
104
            'Output the list of patches in JSON format'
105
        );
106
    }
107
108
    /**
109
     * @inheritDoc
110
     *
111
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
112
     */
113
    protected function execute(InputInterface $input, OutputInterface $output)
114
    {
115
        $composer = $this->getComposer();
0 ignored issues
show
Deprecated Code introduced by
The function Composer\Command\BaseCommand::getComposer() has been deprecated: since Composer 2.3.0 use requireComposer or tryComposer depending on whether you have $required set to true or false ( Ignorable by Annotation )

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

115
        $composer = /** @scrutinizer ignore-deprecated */ $this->getComposer();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
116
117
        $isDevMode = !$input->getOption('no-dev');
118
        $withExcluded = $input->getOption('excluded') || $input->getOption('with-excludes');
119
        $withAffected = $input->getOption('with-affected');
120
        $beBrief = $input->getOption('brief');
121
122
        $filters = array(
123
            Patch::SOURCE => $input->getOption('filter'),
124
            Patch::TARGETS => $input->getArgument('targets')
125
        );
126
127
        $statusFilters = array_map(
128
            'strtolower',
129
            array_filter((array)$input->getOption('status'))
130
        );
131
132
        $contextFactory = new \Vaimo\ComposerPatches\Factories\ComposerContextFactory($composer);
133
        $composerContext = $contextFactory->create();
134
135
        $pluginConfig = $this->createConfigWithEnabledSources($composerContext);
136
137
        $filteredPool = $this->createLoaderPool($composerContext);
138
139
        $listResolver = new ListResolvers\FilteredListResolver($filters);
140
        $loaderFactory = new \Vaimo\ComposerPatches\Factories\PatchesLoaderFactory($composer);
141
142
        $repository = $composer->getRepositoryManager()->getLocalRepository();
143
144
        $repoStateGenerator = $this->createStateGenerator($composer);
145
        $queueGenerator = $this->createQueueGenerator($listResolver);
146
        $filteredLoader = $loaderFactory->create($filteredPool, $pluginConfig, $isDevMode);
147
148
        $filteredPatches = $filteredLoader->loadFromPackagesRepository($repository);
149
        $repositoryState = $repoStateGenerator->generate($repository);
150
151
        $applyQueue = $queueGenerator->generateApplyQueue($filteredPatches, $repositoryState);
152
        $removeQueue = $queueGenerator->generateRemovalQueue($applyQueue, $repositoryState);
153
        $applyQueue = array_map('array_filter', $applyQueue);
154
155
        $patchListUtils = new \Vaimo\ComposerPatches\Utils\PatchListUtils();
156
        $patchListUpdater = new \Vaimo\ComposerPatches\Patch\DefinitionList\Updater();
157
158
        $filteredPatches = $this->composerFilteredPatchesList(
159
            $filteredPatches,
160
            $applyQueue,
161
            $removeQueue,
162
            $withAffected,
163
            $filters,
164
            $statusFilters
165
        );
166
167
        $patches = array_filter($filteredPatches);
168
169
        $filterUtils = new \Vaimo\ComposerPatches\Utils\FilterUtils();
170
171
        $shouldAddExcludes = $withExcluded
172
            && (
173
                empty($statusFilters)
174
                || preg_match($filterUtils->composeRegex($statusFilters, '/'), 'excluded')
175
            );
176
177
        if ($shouldAddExcludes) {
178
            $unfilteredPool = $this->createUnfilteredPatchLoaderPool($composerContext);
179
180
            $unfilteredLoader = $loaderFactory->create($unfilteredPool, $pluginConfig, $isDevMode);
181
182
            $allPatches = $unfilteredLoader->loadFromPackagesRepository($repository);
183
184
            $patchesQueue = $listResolver->resolvePatchesQueue($allPatches);
185
186
            $excludedPatches = $patchListUpdater->updateStatuses(
187
                array_filter($patchListUtils->diffListsByPath($patchesQueue, $filteredPatches)),
188
                'excluded'
189
            );
190
191
            $patches = array_replace_recursive(
192
                $patches,
193
                $patchListUpdater->updateStatuses($excludedPatches, 'excluded')
194
            );
195
196
            array_walk($patches, function (array &$group) {
197
                ksort($group);
198
            }, $patches);
199
        }
200
201
        if ($beBrief) {
202
            $patches = $patchListUpdater->embedInfoToItems($patches, array(
203
                Patch::LABEL => false,
204
                Patch::OWNER => false
205
            ));
206
        }
207
208
        if ($input->getOption('json')) {
209
            $output->writeln(json_encode($patches));
210
211
            return 0;
212
        }
213
214
        $this->generateOutput($output, $patches);
215
216
        return 0;
217
    }
218
219
    private function createUnfilteredPatchLoaderPool(\Vaimo\ComposerPatches\Composer\Context $composerContext)
220
    {
221
        $composer = $composerContext->getLocalComposer();
222
223
        $packageInfoResolver = new \Vaimo\ComposerPatches\Package\InfoResolver(
224
            $composer->getInstallationManager(),
225
            $composer->getConfig()->get(\Vaimo\ComposerPatches\Composer\ConfigKeys::VENDOR_DIR)
226
        );
227
228
        $componentOverrides =  array(
229
            'constraints' => false,
230
            'platform' => false,
231
            'local-exclude' => false,
232
            'root-patch' => false,
233
            'global-exclude' => false,
234
            'targets-resolver' => new LoaderComponents\TargetsResolverComponent($packageInfoResolver, true)
235
        );
236
237
        return $this->createLoaderPool($composerContext, $componentOverrides);
238
    }
239
240
    private function composerFilteredPatchesList($patches, $additions, $removals, $withAffected, $filters, $statuses)
241
    {
242
        $hasFilers = (bool)array_filter($filters);
243
244
        $listResolver = new ListResolvers\FilteredListResolver($filters);
245
246
        $patchListUtils = new \Vaimo\ComposerPatches\Utils\PatchListUtils();
247
        $patchListUpdater = new \Vaimo\ComposerPatches\Patch\DefinitionList\Updater();
248
249
        $filteredPatches = $patchListUtils->mergeLists($patches, $removals);
250
251
        if ($withAffected) {
252
            $additions = $patchListUpdater->embedInfoToItems(
253
                $additions,
254
                array(Patch::STATUS => 'affected'),
255
                true
256
            );
257
        }
258
259
        $filteredPatches = $patchListUtils->mergeLists(
260
            $filteredPatches,
261
            $patchListUtils->intersectListsByName($additions, $filteredPatches)
262
        );
263
264
        $filteredPatches = $patchListUpdater->embedInfoToItems(
265
            $filteredPatches,
266
            array(Patch::STATUS => 'applied'),
267
            true
268
        );
269
270
        if ($hasFilers) {
271
            $filteredPatches = $listResolver->resolvePatchesQueue($filteredPatches);
272
        }
273
274
        if (!empty($statuses)) {
275
            $filterUtils = new \Vaimo\ComposerPatches\Utils\FilterUtils();
276
277
            $filteredPatches = $patchListUtils->applyDefinitionKeyValueFilter(
278
                $filteredPatches,
279
                $filterUtils->composeRegex($statuses, '/'),
280
                Patch::STATUS
281
            );
282
        }
283
284
        return $filteredPatches;
285
    }
286
287
    private function createConfigWithEnabledSources(\Vaimo\ComposerPatches\Composer\Context $composerContext)
288
    {
289
        $configDefaults = new \Vaimo\ComposerPatches\Config\Defaults();
290
291
        $defaultValues = $configDefaults->getPatcherConfig();
292
293
        $sourceKeys = array();
294
295
        if (isset($defaultValues[Config::PATCHER_SOURCES]) && is_array($defaultValues[Config::PATCHER_SOURCES])) {
296
            $sourceKeys = array_keys((array)$defaultValues[Config::PATCHER_SOURCES]);
297
        }
298
299
        $pluginConfig = array(
300
            Config::PATCHER_SOURCES => array_fill_keys($sourceKeys, true)
301
        );
302
303
        $configFactory = new \Vaimo\ComposerPatches\Factories\ConfigFactory($composerContext);
304
305
        return $configFactory->create(array($pluginConfig));
306
    }
307
308
    private function createStateGenerator(Composer $composer)
309
    {
310
        $packageCollector = new \Vaimo\ComposerPatches\Package\Collector(
311
            array($composer->getPackage())
312
        );
313
314
        return new \Vaimo\ComposerPatches\Repository\StateGenerator(
315
            $packageCollector
316
        );
317
    }
318
319
    private function createQueueGenerator(ListResolver $listResolver)
320
    {
321
        $changesListResolver = new ListResolvers\ChangesListResolver($listResolver);
322
        $stateAnalyser = new \Vaimo\ComposerPatches\Repository\State\Analyser();
323
324
        return new \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator(
325
            $changesListResolver,
326
            $stateAnalyser
327
        );
328
    }
329
330
    private function createLoaderPool(ComposerContext $composerContext, array $componentUpdates = array())
331
    {
332
        $componentPool = new \Vaimo\ComposerPatches\Patch\DefinitionList\Loader\ComponentPool(
333
            $composerContext,
334
            $this->getIO(),
335
            true
336
        );
337
338
        foreach ($componentUpdates as $componentName => $replacement) {
339
            $componentPool->registerComponent($componentName, $replacement);
340
        }
341
342
        return $componentPool;
343
    }
344
345
    private function generateOutput(OutputInterface $output, array $list)
346
    {
347
        $statusConfig = new \Vaimo\ComposerPatches\Package\PatchApplier\StatusConfig();
348
349
        $statusDecorators = $statusConfig->getLabels();
350
351
        foreach ($list as $packageName => $patches) {
352
            $output->writeln(sprintf('<info>%s</info>', $packageName));
353
354
            foreach ($patches as $path => $info) {
355
                $owner = $info[Patch::OWNER];
356
                $patchInfoLabel = $this->createStatusLabel($path, $info, $statusDecorators);
357
                $output->writeln($owner ? sprintf('  ~ %s', $patchInfoLabel) : $patchInfoLabel);
358
359
                $descriptionLines = array_filter(
360
                    explode(PHP_EOL, $info[Patch::LABEL])
361
                );
362
363
                foreach ($descriptionLines as $line) {
364
                    $output->writeln(sprintf('    <comment>%s</comment>', $line));
365
                }
366
            }
367
368
            $output->writeln('');
369
        }
370
    }
371
372
    private function createStatusLabel($path, $info, array $statusDecorators)
373
    {
374
        $status = isset($info[Patch::STATUS])
375
            ? $info[Patch::STATUS]
376
            : Patch::STATUS_UNKNOWN;
377
378
        $owner = $info[Patch::OWNER];
379
380
        $stateDecorator = $statusDecorators[$status];
381
382
        if ($status === Patch::STATUS_ERRORS) {
383
            $stateDecorator = sprintf(
384
                $stateDecorator,
385
                $info[Patch::STATE_LABEL] ? $info[Patch::STATE_LABEL] : 'ERROR'
386
            );
387
        }
388
389
        $statusLabel = sprintf(' [%s]', $stateDecorator);
390
391
        if ($owner && $owner !== Patch::OWNER_UNKNOWN) {
392
            return sprintf('<info>%s</info>: %s%s', $owner, $path, $statusLabel);
393
        }
394
395
        return sprintf('%s%s', $path, $statusLabel);
396
    }
397
}
398