ListCommand::createStatusLabel()   A
last analyzed

Complexity

Conditions 6
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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