Passed
Push — master ( dc1c28...b12c89 )
by Allan
02:38
created

ValidateCommand::createPatchesLoader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 16
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 24
rs 9.7333
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 Composer\Repository\WritableRepositoryInterface as PackageRepository;
9
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Symfony\Component\Console\Input\InputOption;
13
14
use Vaimo\ComposerPatches\Composer\ConfigKeys;
15
use Vaimo\ComposerPatches\Config;
16
use Vaimo\ComposerPatches\Patch\DefinitionList\LoaderComponents;
17
use Vaimo\ComposerPatches\Patch\Definition as Patch;
18
19
class ValidateCommand extends \Composer\Command\BaseCommand
20
{
21
    protected function configure()
22
    {
23
        parent::configure();
24
25
        $this->setName('patch:validate');
26
27
        $this->setDescription('Validate that all patches have proper target configuration');
28
29
        $this->addOption(
30
            '--from-source',
31
            null,
32
            InputOption::VALUE_NONE,
33
            'Use latest information from package configurations in vendor folder'
34
        );
35
36
        $this->addOption(
37
            '--local',
38
            null,
39
            InputOption::VALUE_NONE,
40
            'Only validate patches that are owned by the ROOT package'
41
        );
42
    }
43
    
44
    protected function execute(InputInterface $input, OutputInterface $output)
45
    {
46
        $output->writeln('<info>Scanning packages for orphan patches</info>');
47
48
        $composer = $this->getComposer();
49
        
50
        $localOnly = $input->getOption('local');
51
52
        $configDefaults = new \Vaimo\ComposerPatches\Config\Defaults();
53
        $patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
54
        
55
        $defaultValues = $configDefaults->getPatcherConfig();
56
57
        $sourceKeys = array_keys($defaultValues[Config::PATCHER_SOURCES]);
0 ignored issues
show
Bug introduced by
It seems like $defaultValues[Vaimo\Com...onfig::PATCHER_SOURCES] can also be of type boolean; however, parameter $input of array_keys() does only seem to accept array, 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

57
        $sourceKeys = array_keys(/** @scrutinizer ignore-type */ $defaultValues[Config::PATCHER_SOURCES]);
Loading history...
58
        
59
        $patchSources = $localOnly
60
            ? array_replace(array_fill_keys($sourceKeys, false), array('project' => true))
61
            : array_fill_keys($sourceKeys, true);
62
63
        $pluginConfig = array(
64
            Config::PATCHER_SOURCES => $patchSources
65
        );
66
67
        $configFactory = new \Vaimo\ComposerPatches\Factories\ConfigFactory($composer, array(
68
            Config::PATCHER_FROM_SOURCE => (bool)$input->getOption('from-source')
69
        ));
70
        
71
        $repository = $composer->getRepositoryManager()->getLocalRepository();
72
73
        $pluginConfig = $configFactory->create(array($pluginConfig));
74
        
75
        $patchesLoader = $this->createPatchesLoader($pluginConfig);
76
77
        $patches = $patchesLoader->loadFromPackagesRepository($repository);
78
        
79
        $patchPaths = $patchListAnalyser->extractValue($patches, array(Patch::PATH, Patch::SOURCE));
80
        
81
        $patchDefines = array_combine(
82
            $patchPaths,
83
            $patchListAnalyser->extractValue($patches, array(Patch::OWNER))
84
        );
85
        
86
        $patchStatuses = array_filter(
87
            array_combine(
88
                $patchPaths,
89
                $patchListAnalyser->extractValue($patches, array(Patch::STATUS_LABEL))
90
            ) ?: array()
91
        );
92
        
93
        $matches = $this->resolveValidationTargets($repository, $pluginConfig);
94
95
        $installPaths = $this->collectInstallPaths($matches);
96
        
97
        $fileMatches = $this->collectPatchFilesFromPackages($matches, $pluginConfig);
98
        
99
        $groups = array_filter(
100
            $this->collectOrphans($fileMatches, $patchDefines, $installPaths, $patchStatuses)
101
        );
102
        
103
        $this->outputOrphans($output, $groups);
104
105
        $output->writeln(
106
            $groups ? '<error>Orphans found!</error>' : '<info>Validation completed successfully</info>'
107
        );
108
        
109
        return (int)(bool)$groups;
110
    }
111
112
    private function collectInstallPaths(array $matches)
113
    {
114
        $composer = $this->getComposer();
115
        $projectRoot = getcwd();
116
117
        $installationManager = $composer->getInstallationManager();
118
        
119
        $installPaths = array();
120
        foreach ($matches as $packageName => $package) {
121
            $installPaths[$packageName] = $package instanceof \Composer\Package\RootPackage
122
                ? $projectRoot
123
                : $installationManager->getInstallPath($package);
124
        }
125
        
126
        return $installPaths;
127
    }
128
129
    private function collectPatchFilesFromPackages(array $matches, Config $pluginConfig)
130
    {
131
        $composer = $this->getComposer();
132
        $composerConfig = $composer->getConfig();
133
134
        $configReaderFactory = new \Vaimo\ComposerPatches\Factories\PatcherConfigReaderFactory($composer);
135
        $dataUtils = new \Vaimo\ComposerPatches\Utils\DataUtils();
136
        $filterUtils = new \Vaimo\ComposerPatches\Utils\FilterUtils();
137
        $fileSystemUtils = new \Vaimo\ComposerPatches\Utils\FileSystemUtils();
138
139
        $projectRoot = getcwd();
140
141
        $vendorRoot = $composerConfig->get(ConfigKeys::VENDOR_DIR);
142
        $vendorPath = ltrim(
143
            substr($vendorRoot, strlen($projectRoot)),
144
            DIRECTORY_SEPARATOR
145
        );
146
        
147
        $defaultIgnores = array($vendorPath, '.hg', '.git', '.idea');
148
149
        $patcherConfigReader = $configReaderFactory->create($pluginConfig);
150
        
151
        $installPaths = $this->collectInstallPaths($matches);
152
153
        $fileMatchGroups = array();
154
155
        foreach ($matches as $packageName => $package) {
156
            $patcherConfig = $patcherConfigReader->readFromPackage($package);
157
158
            $ignores = $dataUtils->getValueByPath(
159
                $patcherConfig,
160
                array(Config::PATCHER_CONFIG_ROOT, Config::PATCHES_IGNORE),
161
                array()
162
            );
163
164
            $installPath = $installPaths[$packageName];
165
166
            $skippedPaths = $dataUtils->prefixArrayValues(
167
                array_merge($defaultIgnores, $ignores),
168
                $installPath . DIRECTORY_SEPARATOR
169
            );
170
171
            $filter = $filterUtils->composeRegex(
172
                $filterUtils->invertRules($skippedPaths),
173
                '/'
174
            );
175
176
            $filter = sprintf('%s.+\.patch/i', rtrim($filter, '/'));
177
178
            $searchResult = $fileSystemUtils->collectPathsRecursively($installPath, $filter);
179
180
            $fileMatchGroups[] = array_fill_keys($searchResult, $packageName);
181
        }
182
183
        return array_reduce($fileMatchGroups, 'array_replace', array());
184
    }
185
    
186
    private function resolveValidationTargets(PackageRepository $repository, Config $pluginConfig)
187
    {
188
        $composer = $this->getComposer();
189
190
        $packageResolver = new \Vaimo\ComposerPatches\Composer\Plugin\PackageResolver(
191
            array($composer->getPackage())
192
        );
193
        
194
        $srcResolverFactory = new \Vaimo\ComposerPatches\Factories\SourcesResolverFactory($composer);
195
        $packageListUtils = new \Vaimo\ComposerPatches\Utils\PackageListUtils();
196
197
        $srcResolver = $srcResolverFactory->create($pluginConfig);
198
        
199
        $sources = $srcResolver->resolvePackages($repository);
200
201
        $repositoryUtils = new \Vaimo\ComposerPatches\Utils\RepositoryUtils();
202
203
        $pluginPackage = $packageResolver->resolveForNamespace($repository, __NAMESPACE__);
204
205
        $pluginName = $pluginPackage->getName();
206
207
        $pluginUsers = array_merge(
208
            $repositoryUtils->filterByDependency($repository, $pluginName),
209
            array($composer->getPackage())
210
        );
211
212
        return array_intersect_key(
213
            $packageListUtils->listToNameDictionary($sources),
214
            $packageListUtils->listToNameDictionary($pluginUsers)
215
        );
216
    }
217
    
218
    private function createPatchesLoader(\Vaimo\ComposerPatches\Config $pluginConfig)
219
    {
220
        $composer = $this->getComposer();
221
        
222
        $composerConfig = clone $composer->getConfig();
223
        $downloader = new \Composer\Util\RemoteFilesystem($this->getIO(), $composerConfig);
224
225
        $loaderFactory = new \Vaimo\ComposerPatches\Factories\PatchesLoaderFactory($composer);
226
227
        $loaderComponentsPool = $this->createLoaderPool(array(
228
            'constraints' => false,
229
            'platform' => false,
230
            'targets-resolver' => false,
231
            'local-exclude' => false,
232
            'root-patch' => false,
233
            'global-exclude' => false,
234
            'downloader' => new LoaderComponents\DownloaderComponent(
235
                $composer->getPackage(),
236
                $downloader,
237
                true
238
            )
239
        ));
240
241
        return $loaderFactory->create($loaderComponentsPool, $pluginConfig, true);
242
    }
243
244
    private function createLoaderPool(array $componentUpdates = array())
245
    {
246
        $composer = $this->getComposer();
247
        $appIO = $this->getIO();
248
249
        $componentPool = new \Vaimo\ComposerPatches\Patch\DefinitionList\Loader\ComponentPool(
250
            $composer,
251
            $appIO
252
        );
253
254
        foreach ($componentUpdates as $componentName => $replacement) {
255
            $componentPool->registerComponent($componentName, $replacement);
256
        }
257
258
        return $componentPool;
259
    }
260
    
261
    private function collectOrphans($fileMatches, $patchesWithTargets, $installPaths, $patchStatuses)
262
    {
263
        $orphanFiles = array_diff_key($fileMatches, $patchesWithTargets);
264
        
265
        $orphanConfig = array_diff_key($patchesWithTargets, $fileMatches);
266
267
        /**
268
         * Make sure that downloaded patches are not perceived as missing files
269
         */
270
        $orphanConfig = array_diff_key(
271
            $orphanConfig,
272
            array_flip(
273
                array_filter(array_keys($orphanConfig), 'file_exists')
274
            )
275
        );
276
277
        $groups = array_fill_keys(array_keys($installPaths), array());
278
279
        foreach ($orphanFiles as $path => $ownerName) {
280
            $installPath = $installPaths[$ownerName];
281
            
282
            $groups[$ownerName][] = array(
283
                'issue' => 'NO CONFIG',
284
                'path' => ltrim(
285
                    substr($path, strlen($installPath)),
286
                    DIRECTORY_SEPARATOR
287
                )
288
            );
289
        }
290
        
291
        foreach ($orphanConfig as $path => $ownerName) {
292
            $installPath = $installPaths[$ownerName];
293
294
            $pathInfo = parse_url($path);
295
            $pathIncludesScheme = isset($pathInfo['scheme']) && $pathInfo['scheme'];
296
297
            $groups[$ownerName][] = array(
298
                'issue' => isset($patchStatuses[$path]) && $patchStatuses[$path]
299
                    ? $patchStatuses[$path]
300
                    : 'NO FILE',
301
                'path' => !$pathIncludesScheme
302
                    ? ltrim(substr($path, strlen($installPath)), DIRECTORY_SEPARATOR)
303
                    : $path
304
            );
305
        }
306
307
        return $groups;
308
    }
309
310
    private function outputOrphans(OutputInterface $output, array $groups)
311
    {
312
        $lines = array();
313
        
314
        foreach ($groups as $packageName => $items) {
315
            $lines[] = sprintf('  - <info>%s</info>', $packageName);
316
317
            foreach ($items as $item) {
318
                $lines[] = sprintf('    ~ %s [<fg=red>%s</>]', $item['path'], $item['issue']);
319
            }
320
        }
321
        
322
        foreach ($lines as $line) {
323
            $output->writeln($line);
324
        }
325
    }
326
}
327