Passed
Push — master ( 119ef1...d221d7 )
by Allan
02:31
created

ValidateCommand::createSourcesEnablerConfig()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 1
dl 0
loc 15
rs 10
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 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
        $patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
53
        
54
        $pluginConfig = array(
55
            Config::PATCHER_SOURCES => $this->createSourcesEnablerConfig($localOnly)
56
        );
57
58
        $configFactory = new \Vaimo\ComposerPatches\Factories\ConfigFactory($composer, array(
59
            Config::PATCHER_FROM_SOURCE => (bool)$input->getOption('from-source')
60
        ));
61
        
62
        $repository = $composer->getRepositoryManager()->getLocalRepository();
63
64
        $pluginConfig = $configFactory->create(array($pluginConfig));
65
        
66
        $patchesLoader = $this->createPatchesLoader($pluginConfig);
67
68
        $patches = $patchesLoader->loadFromPackagesRepository($repository);
69
        
70
        $patchPaths = $patchListAnalyser->extractValue($patches, array(Patch::PATH, Patch::SOURCE));
71
        
72
        $patchDefines = array_combine(
73
            $patchPaths,
74
            $patchListAnalyser->extractValue($patches, array(Patch::OWNER))
75
        );
76
        
77
        $patchStatuses = array_filter(
78
            array_combine(
79
                $patchPaths,
80
                $patchListAnalyser->extractValue($patches, array(Patch::STATUS_LABEL))
81
            ) ?: array()
82
        );
83
        
84
        $matches = $this->resolveValidationTargets($repository, $pluginConfig);
85
86
        $installPaths = $this->collectInstallPaths($matches);
87
        
88
        $fileMatches = $this->collectPatchFilesFromPackages($matches, $pluginConfig);
89
        
90
        $groups = array_filter(
91
            $this->collectOrphans($fileMatches, $patchDefines, $installPaths, $patchStatuses)
92
        );
93
        
94
        $this->outputOrphans($output, $groups);
95
96
        $output->writeln(
97
            $groups ? '<error>Orphans found!</error>' : '<info>Validation completed successfully</info>'
98
        );
99
        
100
        return (int)(bool)$groups;
101
    }
102
103
    private function createSourcesEnablerConfig($localOnly)
104
    {
105
        $configDefaults = new \Vaimo\ComposerPatches\Config\Defaults();
106
107
        $defaultValues = $configDefaults->getPatcherConfig();
108
        
109
        if (isset($defaultValues[Config::PATCHER_SOURCES]) && is_array($defaultValues[Config::PATCHER_SOURCES])) {
110
            $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

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