ValidateCommand::execute()   A
last analyzed

Complexity

Conditions 3
Paths 1

Size

Total Lines 54
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 31
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 54
rs 9.424

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Composer\Repository\WritableRepositoryInterface as PackageRepository;
13
14
use Vaimo\ComposerPatches\Composer\ConfigKeys;
15
use Vaimo\ComposerPatches\Config;
16
use Vaimo\ComposerPatches\Patch\Definition as Patch;
17
use Vaimo\ComposerPatches\Patch\DefinitionList\Loader\ComponentPool;
18
use Vaimo\ComposerPatches\Composer\Context as ComposerContext;
19
use Vaimo\ComposerPatches\Utils\PathUtils;
20
21
/**
22
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
23
 */
24
class ValidateCommand extends \Composer\Command\BaseCommand
25
{
26
    protected function configure()
27
    {
28
        parent::configure();
29
30
        $this->setName('patch:validate');
31
32
        $this->setDescription('Validate that all patches have proper target configuration');
33
34
        $this->addOption(
35
            '--from-source',
36
            null,
37
            InputOption::VALUE_NONE,
38
            'Use latest information from package configurations in vendor folder'
39
        );
40
41
        $this->addOption(
42
            '--local',
43
            null,
44
            InputOption::VALUE_NONE,
45
            'Only validate patches that are owned by the ROOT package'
46
        );
47
    }
48
49
    protected function execute(InputInterface $input, OutputInterface $output)
50
    {
51
        $output->writeln('<info>Scanning packages for orphan patches</info>');
52
53
        $composer = $this->getComposer();
54
55
        $localOnly = $input->getOption('local');
56
57
        $patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
58
59
        $pluginConfig = array(
60
            Config::PATCHER_SOURCES => $this->createSourcesEnablerConfig($localOnly)
61
        );
62
63
        $contextFactory = new \Vaimo\ComposerPatches\Factories\ComposerContextFactory($composer);
64
        $composerContext = $contextFactory->create();
65
66
        $configFactory = new \Vaimo\ComposerPatches\Factories\ConfigFactory($composerContext, array(
67
            Config::PATCHER_FROM_SOURCE => (bool)$input->getOption('from-source')
68
        ));
69
70
        $repository = $composer->getRepositoryManager()->getLocalRepository();
71
72
        $pluginConfig = $configFactory->create(array($pluginConfig));
73
74
        $patchesLoader = $this->createPatchesLoader($composerContext, $pluginConfig);
75
76
        $patches = $patchesLoader->loadFromPackagesRepository($repository);
77
78
        $patchPaths = $patchListAnalyser->extractValue($patches, array(Patch::PATH, Patch::SOURCE));
79
80
        $patchDefines = array_combine(
81
            $patchPaths,
82
            $patchListAnalyser->extractDictionary($patches, array(Patch::OWNER, Patch::URL))
83
        );
84
85
        $patchStatuses = array_filter(
86
            array_combine(
87
                $patchPaths,
88
                $patchListAnalyser->extractValue($patches, array(Patch::STATUS_LABEL))
89
            ) ?: array()
90
        );
91
92
        $matches = $this->resolveValidationTargets($repository, $pluginConfig);
93
        $installPaths = $this->collectInstallPaths($matches);
94
        $fileMatches = $this->collectPatchFilesFromPackages($matches, $pluginConfig);
95
        $groups = $this->collectOrphans($fileMatches, $patchDefines, $installPaths, $patchStatuses);
96
        $this->outputOrphans($output, $groups);
97
98
        $output->writeln(
99
            $groups ? '<error>Orphans found!</error>' : '<info>Validation completed successfully</info>'
100
        );
101
102
        return (int)(bool)$groups;
103
    }
104
105
    private function createSourcesEnablerConfig($localOnly)
106
    {
107
        $configDefaults = new Config\Defaults();
108
109
        $defaultValues = $configDefaults->getPatcherConfig();
110
111
        if (isset($defaultValues[Config::PATCHER_SOURCES]) && is_array($defaultValues[Config::PATCHER_SOURCES])) {
112
            $sourceKeys = array_keys((array)$defaultValues[Config::PATCHER_SOURCES]);
113
114
            return $localOnly
115
                ? array_replace(array_fill_keys($sourceKeys, false), array('project' => true))
116
                : array_fill_keys($sourceKeys, true);
117
        }
118
119
        return array();
120
    }
121
122
    private function collectInstallPaths(array $matches)
123
    {
124
        $composer = $this->getComposer();
125
        $projectRoot = getcwd();
126
127
        $installationManager = $composer->getInstallationManager();
128
129
        $installPaths = array();
130
        foreach ($matches as $packageName => $package) {
131
            $installPaths[$packageName] = $package instanceof \Composer\Package\RootPackageInterface
132
                ? $projectRoot
133
                : $installationManager->getInstallPath($package);
134
        }
135
136
        return $installPaths;
137
    }
138
139
    private function collectPatchFilesFromPackages(array $matches, Config $pluginConfig)
140
    {
141
        $composer = $this->getComposer();
142
        $composerConfig = $composer->getConfig();
143
144
        $configReaderFactory = new \Vaimo\ComposerPatches\Factories\PatcherConfigReaderFactory($composer);
145
        $dataUtils = new \Vaimo\ComposerPatches\Utils\DataUtils();
146
        $filterUtils = new \Vaimo\ComposerPatches\Utils\FilterUtils();
147
        $fileSystemUtils = new \Vaimo\ComposerPatches\Utils\FileSystemUtils();
148
149
        $projectRoot = getcwd();
150
151
        $vendorRoot = $composerConfig->get(ConfigKeys::VENDOR_DIR);
152
        $vendorPath = ltrim(
153
            substr($vendorRoot, strlen($projectRoot)),
0 ignored issues
show
Bug introduced by
It seems like $vendorRoot can also be of type boolean and null; however, parameter $string of substr() does only seem to accept string, 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

153
            substr(/** @scrutinizer ignore-type */ $vendorRoot, strlen($projectRoot)),
Loading history...
154
            DIRECTORY_SEPARATOR
155
        );
156
157
        $defaultIgnores = array($vendorPath, '.hg', '.git', '.idea');
158
159
        $patcherConfigReader = $configReaderFactory->create($pluginConfig);
160
161
        $installPaths = $this->collectInstallPaths($matches);
162
163
        $fileMatchGroups = array();
164
165
        foreach ($matches as $packageName => $package) {
166
            $patcherConfig = $patcherConfigReader->readFromPackage($package);
167
168
            $ignores = $dataUtils->getValueByPath(
169
                $patcherConfig,
170
                array(Config::PATCHER_CONFIG_ROOT, Config::PATCHES_IGNORE),
171
                array()
172
            );
173
174
            $installPath = $installPaths[$packageName];
175
176
            $skippedPaths = $dataUtils->prefixArrayValues(
177
                array_merge($defaultIgnores, $ignores),
178
                $installPath . DIRECTORY_SEPARATOR
179
            );
180
181
            $filter = $filterUtils->composeRegex(
182
                $filterUtils->invertRules($skippedPaths),
183
                '/'
184
            );
185
186
            $filter = sprintf('%s/i', rtrim($filter, '/') . Config::PATCH_FILE_REGEX_MATCHER);
187
            $searchResult = $fileSystemUtils->collectFilePathsRecursively($installPath, $filter);
188
189
            $fileMatchGroups[] = array_fill_keys($searchResult, array(
190
                Patch::OWNER => $packageName,
191
                Patch::URL => ''
192
            ));
193
        }
194
195
        return array_reduce($fileMatchGroups, 'array_replace', array());
196
    }
197
198
    private function resolveValidationTargets(PackageRepository $repository, Config $pluginConfig)
199
    {
200
        $composer = $this->getComposer();
201
202
        $packageResolver = new \Vaimo\ComposerPatches\Composer\Plugin\PackageResolver(
203
            array($composer->getPackage())
204
        );
205
206
        $srcResolverFactory = new \Vaimo\ComposerPatches\Factories\SourcesResolverFactory($composer);
207
        $packageListUtils = new \Vaimo\ComposerPatches\Utils\PackageListUtils();
208
209
        $srcResolver = $srcResolverFactory->create($pluginConfig);
210
211
        $sources = $srcResolver->resolvePackages($repository);
212
213
        $repositoryUtils = new \Vaimo\ComposerPatches\Utils\RepositoryUtils();
214
215
        $pluginPackage = $packageResolver->resolveForNamespace(
216
            $repository->getCanonicalPackages(),
217
            __NAMESPACE__
218
        );
219
220
        $pluginName = $pluginPackage->getName();
221
222
        $pluginUsers = array_merge(
223
            $repositoryUtils->filterByDependency($repository, $pluginName),
224
            array($composer->getPackage())
225
        );
226
227
        return array_intersect_key(
228
            $packageListUtils->listToNameDictionary($sources),
229
            $packageListUtils->listToNameDictionary($pluginUsers)
230
        );
231
    }
232
233
    private function createPatchesLoader(ComposerContext $composerContext, Config $pluginConfig)
234
    {
235
        $composer = $this->getComposer();
236
237
        $loaderFactory = new \Vaimo\ComposerPatches\Factories\PatchesLoaderFactory($composer);
238
239
        $componentOverrides = array(
240
            'constraints' => false,
241
            'platform' => false,
242
            'targets-resolver' => false,
243
            'local-exclude' => false,
244
            'root-patch' => false,
245
            'global-exclude' => false
246
        );
247
248
        $loaderComponentsPool = $this->createLoaderPool($composerContext, $componentOverrides);
249
250
        return $loaderFactory->create($loaderComponentsPool, $pluginConfig, true);
251
    }
252
253
    private function createLoaderPool(ComposerContext $composerContext, array $componentUpdates = array())
254
    {
255
        $appIO = $this->getIO();
256
257
        $componentPool = new ComponentPool($composerContext, $appIO, true);
258
259
        foreach ($componentUpdates as $componentName => $replacement) {
260
            $componentPool->registerComponent($componentName, $replacement);
261
        }
262
263
        return $componentPool;
264
    }
265
266
    private function collectOrphans(array $files, array $patches, array $paths, array $statuses)
267
    {
268
        $orphanFiles = array_diff_key($files, $patches);
269
        $orphanConfig = array_diff_key($patches, $files);
270
271
        /**
272
         * Make sure that downloaded patches are not perceived as missing files
273
         */
274
        $orphanConfig = array_diff_key(
275
            $orphanConfig,
276
            array_flip(
277
                array_filter(array_keys($orphanConfig), 'file_exists')
278
            )
279
        );
280
281
        $groups = array_fill_keys(array_keys($paths), array());
282
283
        $results = array(
284
            $this->findWithMissingConfig($orphanFiles, $paths),
285
            $this->findWithMissingFile($orphanConfig, $paths, $statuses)
286
        );
287
288
        foreach ($results as $result) {
289
            foreach ($result as $ownerName => $items) {
290
                $groups[$ownerName] = array_merge($groups[$ownerName], $items);
291
            }
292
        }
293
294
        return array_filter($groups);
295
    }
296
297
    private function findWithMissingConfig($orphanFiles, $paths)
298
    {
299
        $pathFlags = array_fill_keys(array_keys($paths), true);
300
        $groups = array();
301
302
        foreach ($orphanFiles as $path => $config) {
303
            $ownerName = $config[Patch::OWNER];
304
            $installPath = $paths[$ownerName];
305
306
            if (!isset($pathFlags[$ownerName])) {
307
                continue;
308
            }
309
310
            $groups[$ownerName][] = array(
311
                'issue' => 'NO CONFIG',
312
                'path' => $config[Patch::URL] ?: PathUtils::reducePathLeft($path, $installPath)
313
            );
314
        }
315
316
        return $groups;
317
    }
318
319
    private function findWithMissingFile($orphanConfig, $paths, $statuses)
320
    {
321
        $pathFlags = array_fill_keys(array_keys($paths), true);
322
        $groups = array();
323
324
        foreach ($orphanConfig as $path => $config) {
325
            $ownerName = $config[Patch::OWNER];
326
            $installPath = $paths[$ownerName];
327
328
            if (!isset($pathFlags[$ownerName])) {
329
                continue;
330
            }
331
332
            $groups[$ownerName][] = array(
333
                'issue' => isset($statuses[$path]) && $statuses[$path]
334
                    ? $statuses[$path]
335
                    : 'NO FILE',
336
                'path' => $config[Patch::URL] ?: PathUtils::reducePathLeft($path, $installPath)
337
            );
338
        }
339
340
        return $groups;
341
    }
342
343
    private function outputOrphans(OutputInterface $output, array $groups)
344
    {
345
        $lines = array();
346
347
        foreach ($groups as $packageName => $items) {
348
            $lines[] = sprintf('  - <info>%s</info>', $packageName);
349
350
            foreach ($items as $item) {
351
                $lines[] = sprintf(
352
                    '    ~ %s [<fg=red>%s</>]',
353
                    $item['path'],
354
                    $item['issue']
355
                );
356
            }
357
        }
358
359
        foreach ($lines as $line) {
360
            $output->writeln($line);
361
        }
362
    }
363
}
364