Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
13.73% covered (danger)
13.73%
35 / 255
10.00% covered (danger)
10.00%
2 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
DependenciesCommand
13.73% covered (danger)
13.73%
35 / 255
10.00% covered (danger)
10.00%
2 / 20
965.29
0.00% covered (danger)
0.00%
0 / 1
 configure
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 execute
19.23% covered (danger)
19.23%
5 / 26
0.00% covered (danger)
0.00%
0 / 1
4.11
 loadProjectComposerPackage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 loadConfigFromComposerJson
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 updateConfigFromCli
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 buildDependencyList
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 enumerateFiles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 enumeratePsr4Namespaces
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 enumerateAutoloadedFiles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 scanFilesForSymbols
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 markSymbolsForRenaming
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 determineChanges
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 analyseFilesToCopy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 copyFiles
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 performReplacements
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 performReplacementsInProjectFiles
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 addLicenses
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 generateAutoloader
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 generateAliasesFile
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 cleanUp
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace BrianHenryIE\Strauss\Console\Commands;
4
5use BrianHenryIE\Strauss\Composer\ComposerPackage;
6use BrianHenryIE\Strauss\Composer\ProjectComposerPackage;
7use BrianHenryIE\Strauss\Files\DiscoveredFiles;
8use BrianHenryIE\Strauss\Files\File;
9use BrianHenryIE\Strauss\Pipeline\Aliases\Aliases;
10use BrianHenryIE\Strauss\Pipeline\Autoload;
11use BrianHenryIE\Strauss\Pipeline\Autoload\VendorComposerAutoload;
12use BrianHenryIE\Strauss\Pipeline\AutoloadedFilesEnumerator;
13use BrianHenryIE\Strauss\Pipeline\ChangeEnumerator;
14use BrianHenryIE\Strauss\Pipeline\Cleanup\Cleanup;
15use BrianHenryIE\Strauss\Pipeline\Cleanup\InstalledJson;
16use BrianHenryIE\Strauss\Pipeline\Copier;
17use BrianHenryIE\Strauss\Pipeline\DependenciesEnumerator;
18use BrianHenryIE\Strauss\Pipeline\FileCopyScanner;
19use BrianHenryIE\Strauss\Pipeline\FileEnumerator;
20use BrianHenryIE\Strauss\Pipeline\FileSymbolScanner;
21use BrianHenryIE\Strauss\Pipeline\Licenser;
22use BrianHenryIE\Strauss\Pipeline\MarkSymbolsForRenaming;
23use BrianHenryIE\Strauss\Pipeline\Prefixer;
24use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
25use BrianHenryIE\Strauss\Types\NamespaceSymbol;
26use Composer\Factory;
27use Exception;
28use Symfony\Component\Console\Command\Command;
29use Symfony\Component\Console\Input\InputArgument;
30use Symfony\Component\Console\Input\InputInterface;
31use Symfony\Component\Console\Output\OutputInterface;
32
33class DependenciesCommand extends AbstractRenamespacerCommand
34{
35    /** @var Prefixer */
36    protected Prefixer $replacer;
37
38    protected DependenciesEnumerator $dependenciesEnumerator;
39
40    /** @var array<string,ComposerPackage> */
41    protected array $flatDependencyTree = [];
42
43    /**
44     * ArrayAccess of \BrianHenryIE\Strauss\File objects indexed by their path relative to the output target directory.
45     *
46     * Each object contains the file's relative and absolute paths, the package and autoloaders it came from,
47     * and flags indicating should it / has it been copied / deleted etc.
48     *
49     */
50    protected DiscoveredFiles $discoveredFiles;
51    protected DiscoveredSymbols $discoveredSymbols;
52
53    /**
54     * Set name and description, add CLI arguments, call parent class to add dry-run, verbosity options.
55     *
56     * @used-by \Symfony\Component\Console\Command\Command::__construct
57     * @override {@see \Symfony\Component\Console\Command\Command::configure()} empty method.
58     *
59     * @return void
60     */
61    protected function configure()
62    {
63        $this->setName('dependencies');
64        $this->setDescription("Copy composer's `require` and prefix their namespace and classnames.");
65        $this->setHelp('');
66
67        $this->addOption(
68            'updateCallSites',
69            null,
70            InputArgument::OPTIONAL,
71            'Should replacements also be performed in project files? true|list,of,paths|false'
72        );
73
74        $this->addOption(
75            'deleteVendorPackages',
76            null,
77            4,
78            'Should original packages be deleted after copying? true|false',
79            false
80        );
81        // Is there a nicer way to add aliases?
82        $this->addOption(
83            'delete_vendor_packages',
84            null,
85            4,
86            '',
87            false
88        );
89
90        parent::configure();
91    }
92
93    /**
94     * @param InputInterface $input
95     * @param OutputInterface $output
96     *
97     * @return int
98     * @see Command::execute()
99     *
100     */
101    protected function execute(InputInterface $input, OutputInterface $output): int
102    {
103        try {
104            $this->logger->notice('Starting... '/** version */); // + PHP version
105
106            $this->loadProjectComposerPackage();
107            $this->loadConfigFromComposerJson();
108            $this->updateConfigFromCli($input);
109
110            parent::execute($input, $output);
111
112            $this->buildDependencyList();
113
114            $this->enumerateFiles();
115
116            $this->discoveredSymbols = new DiscoveredSymbols();
117
118            $this->enumeratePsr4Namespaces();
119            $this->enumerateAutoloadedFiles();
120            $this->scanFilesForSymbols();
121            $this->analyseFilesToCopy();
122            $this->markSymbolsForRenaming();
123            $this->determineChanges();
124            $this->copyFiles();
125
126            $this->performReplacements();
127
128            $this->performReplacementsInProjectFiles();
129
130            $this->addLicenses();
131
132            $this->cleanUp();
133
134            $this->generateAutoloader();
135
136            // After files have been deleted, we may need aliases.
137            $this->generateAliasesFile();
138
139            $this->logger->notice('Done');
140        } catch (Exception $e) {
141            $this->logger->error($e->getMessage());
142            return Command::FAILURE;
143        }
144
145        return Command::SUCCESS;
146    }
147
148    /**
149     * Load the project's composer package using the current working directory.
150     *
151     * @throws Exception
152     */
153    protected function loadProjectComposerPackage(): void
154    {
155        $this->logger->notice('Loading package...');
156
157        $composerFilePath = $this->filesystem->normalize($this->workingDir . Factory::getComposerFile());
158        $defaultComposerFilePath = $this->filesystem->normalize($this->workingDir . 'composer.json');
159        if ($composerFilePath !== $defaultComposerFilePath) {
160            $this->logger->info('Using: ' . $composerFilePath);
161        }
162
163        $this->projectComposerPackage = new ProjectComposerPackage('/'.$composerFilePath);
164
165        // TODO: Print the config that Strauss is using.
166        // Maybe even highlight what is default config and what is custom config.
167    }
168
169    /**
170     * Load Strauss config from the project's composer.json.
171     */
172    protected function loadConfigFromComposerJson(): void
173    {
174        $this->logger->notice('Loading composer.json config...');
175
176        $this->config = $this->projectComposerPackage->getStraussConfig();
177    }
178
179    protected function updateConfigFromCli(InputInterface $input): void
180    {
181        $this->logger->notice('Loading cli config...');
182
183        $this->config->updateFromCli($input);
184    }
185
186    /**
187     * 2. Built flat list of packages and dependencies.
188     *
189     * 2.1 Initiate getting dependencies for the project composer.json.
190     *
191     * @see DependenciesCommand::flatDependencyTree
192     */
193    protected function buildDependencyList(): void
194    {
195        $this->logger->notice('Building dependency list...');
196
197        $this->dependenciesEnumerator = new DependenciesEnumerator(
198            $this->config,
199            $this->filesystem,
200            $this->logger
201        );
202        $this->flatDependencyTree = $this->dependenciesEnumerator->getAllDependencies();
203
204        $this->config->setPackagesToCopy(
205            array_filter($this->flatDependencyTree, function ($dependency) {
206                return !in_array($dependency, $this->config->getExcludePackagesFromCopy());
207            },
208            ARRAY_FILTER_USE_KEY)
209        );
210
211        $this->config->setPackagesToPrefix(
212            array_filter($this->flatDependencyTree, function ($dependency) {
213                return !in_array($dependency, $this->config->getExcludePackagesFromPrefixing());
214            },
215            ARRAY_FILTER_USE_KEY)
216        );
217
218        foreach ($this->flatDependencyTree as $dependency) {
219            // Sort of duplicating the logic above.
220            $dependency->setCopy(
221                !in_array($dependency, $this->config->getExcludePackagesFromCopy())
222            );
223
224            if ($this->config->isDeleteVendorPackages()) {
225                $dependency->setDelete(true);
226            }
227        }
228
229        // TODO: Print the dependency tree that Strauss has determined.
230    }
231
232
233    protected function enumerateFiles(): void
234    {
235        $this->logger->notice('Enumerating files...');
236
237        $fileEnumerator = new FileEnumerator(
238            $this->config,
239            $this->filesystem,
240            $this->logger
241        );
242
243        $this->discoveredFiles = $fileEnumerator->compileFileListForDependencies($this->flatDependencyTree);
244    }
245
246    /**
247     * TODO: currently this must run after ::determineChanges() so the discoveredSymbols object exists,
248     * but logically it should run first.
249     */
250    protected function enumeratePsr4Namespaces(): void
251    {
252        foreach ($this->config->getPackagesToPrefix() as $package) {
253            $autoloadKey = $package->getAutoload();
254            if (! isset($autoloadKey['psr-4'])) {
255                continue;
256            }
257
258            $psr4autoloadKey = $autoloadKey['psr-4'];
259            $namespaces = array_keys($psr4autoloadKey);
260
261            $file = new File($package->getPackageAbsolutePath() . 'composer.json', '../composer.json');
262
263            foreach ($namespaces as $namespace) {
264                // TODO: log.
265                $symbol = new NamespaceSymbol(
266                    trim($namespace, '\\'),
267                    $file,
268                    '\\',
269                    $package
270                );
271                // TODO: respect all config options.
272//              $symbol->setReplacement($this->config->getNamespacePrefix() . '\\' . trim($namespace, '\\'));
273                $this->discoveredSymbols->add($symbol);
274            }
275        }
276    }
277
278    protected function enumerateAutoloadedFiles(): void
279    {
280        $this->logger->notice('Enumerating autoload files...');
281
282        $autoloadFilesEnumerator = new AutoloadedFilesEnumerator(
283            $this->config,
284            $this->filesystem,
285            $this->logger
286        );
287        $autoloadFilesEnumerator->scanForAutoloadedFiles($this->flatDependencyTree);
288    }
289
290    protected function scanFilesForSymbols(): void
291    {
292        $this->logger->notice('Scanning files...');
293
294        $fileSymbolScanner = new FileSymbolScanner(
295            $this->config,
296            $this->discoveredSymbols,
297            $this->filesystem,
298            $this->logger
299        );
300
301        $fileSymbolScanner->findInFiles($this->discoveredFiles);
302    }
303
304    protected function markSymbolsForRenaming(): void
305    {
306
307        $markSymbolsForRenaming = new MarkSymbolsForRenaming(
308            $this->config,
309            $this->filesystem,
310            $this->logger
311        );
312
313        $markSymbolsForRenaming->scanSymbols($this->discoveredSymbols);
314    }
315
316    protected function determineChanges(): void
317    {
318        $this->logger->notice('Determining changes...');
319
320        $changeEnumerator = new ChangeEnumerator(
321            $this->config,
322            $this->logger
323        );
324        $changeEnumerator->determineReplacements($this->discoveredSymbols);
325    }
326
327    protected function analyseFilesToCopy(): void
328    {
329        (new FileCopyScanner($this->config, $this->filesystem, $this->logger))->scanFiles($this->discoveredFiles);
330    }
331
332    protected function copyFiles(): void
333    {
334
335        if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
336            // Nothing to do.
337            return;
338        }
339
340        $this->logger->notice('Copying files...');
341
342        $copier = new Copier(
343            $this->discoveredFiles,
344            $this->config,
345            $this->filesystem,
346            $this->logger
347        );
348
349
350        $copier->prepareTarget();
351        $copier->copy();
352
353        foreach ($this->flatDependencyTree as $package) {
354            if ($package->isCopy()) {
355                $package->setDidCopy(true);
356            }
357        }
358
359        $installedJson = new InstalledJson(
360            $this->config,
361            $this->filesystem,
362            $this->logger
363        );
364        $installedJson->copyInstalledJson();
365    }
366
367
368    // 5. Update namespaces and class names.
369    // Replace references to updated namespaces and classnames throughout the dependencies.
370    protected function performReplacements(): void
371    {
372        $this->logger->notice('Performing replacements...');
373
374        $this->replacer = new Prefixer(
375            $this->config,
376            $this->filesystem,
377            $this->logger
378        );
379
380        $this->replacer->replaceInFiles(
381            $this->discoveredSymbols,
382            $this->discoveredFiles->getFiles()
383        );
384    }
385
386    protected function performReplacementsInProjectFiles(): void
387    {
388        // TODO: this doesn't do tests?!
389        $relativeCallSitePaths =
390            $this->config->getUpdateCallSites()
391            ?? $this->projectComposerPackage->getFlatAutoloadKey();
392
393        if (empty($relativeCallSitePaths)) {
394            return;
395        }
396
397        $callSitePaths = array_map(
398            fn($path) => $this->workingDir . $path,
399            $relativeCallSitePaths
400        );
401
402        $projectReplace = new Prefixer(
403            $this->config,
404            $this->filesystem,
405            $this->logger
406        );
407
408        $fileEnumerator = new FileEnumerator(
409            $this->config,
410            $this->filesystem,
411            $this->logger
412        );
413
414        $projectFiles = $fileEnumerator->compileFileListForPaths($callSitePaths);
415
416        $phpFiles = array_filter(
417            $projectFiles->getFiles(),
418            fn($file) => $file->isPhpFile()
419        );
420
421        $phpFilesAbsolutePaths = array_map(
422            fn($file) => $file->getSourcePath(),
423            $phpFiles
424        );
425
426        // TODO: Warn when a file that was specified is not found
427        // $this->logger->warning('Expected file not found from project autoload: ' . $absolutePath);
428
429        $projectReplace->replaceInProjectFiles($this->discoveredSymbols, $phpFilesAbsolutePaths);
430    }
431
432    protected function addLicenses(): void
433    {
434        $this->logger->notice('Adding licenses...');
435
436        $author = $this->projectComposerPackage->getAuthor();
437
438        $dependencies = $this->flatDependencyTree;
439
440        $licenser = new Licenser(
441            $this->config,
442            $dependencies,
443            $author,
444            $this->filesystem,
445            $this->logger
446        );
447
448        $licenser->copyLicenses();
449
450        $modifiedFiles = $this->replacer->getModifiedFiles();
451        $licenser->addInformationToUpdatedFiles($modifiedFiles);
452    }
453
454    /**
455     * 6. Generate autoloader.
456     */
457    protected function generateAutoloader(): void
458    {
459        if (isset($this->projectComposerPackage->getAutoload()['classmap'])
460            && in_array(
461                $this->config->getTargetDirectory(),
462                $this->projectComposerPackage->getAutoload()['classmap'],
463                true
464            )
465        ) {
466            $this->logger->notice('Skipping autoloader generation as target directory is in Composer classmap. Run `composer dump-autoload`.');
467            return;
468        }
469
470        $this->logger->notice('Generating autoloader...');
471
472        $allFilesAutoloaders = $this->dependenciesEnumerator->getAllFilesAutoloaders();
473        $filesAutoloaders = array();
474        foreach ($allFilesAutoloaders as $packageName => $packageFilesAutoloader) {
475            if (in_array($packageName, $this->config->getExcludePackagesFromCopy())) {
476                continue;
477            }
478            $filesAutoloaders[$packageName] = $packageFilesAutoloader;
479        }
480
481        $classmap = new Autoload(
482            $this->config,
483            $filesAutoloaders,
484            $this->filesystem,
485            $this->logger
486        );
487
488        $classmap->generate($this->flatDependencyTree, $this->discoveredSymbols);
489    }
490
491    /**
492     * When namespaces are prefixed which are used by both require and require-dev dependencies,
493     * the require-dev dependencies need class aliases specified to point to the new class names/namespaces.
494     */
495    protected function generateAliasesFile(): void
496    {
497        if (!$this->config->isCreateAliases()) {
498            return;
499        }
500
501        $this->logger->notice('Generating aliases file...');
502
503        $aliases = new Aliases(
504            $this->config,
505            $this->filesystem,
506            $this->logger
507        );
508        $aliases->writeAliasesFileForSymbols($this->discoveredSymbols);
509
510        $vendorComposerAutoload = new VendorComposerAutoload(
511            $this->config,
512            $this->filesystem,
513            $this->logger
514        );
515        $vendorComposerAutoload->addAliasesFileToComposer();
516        $vendorComposerAutoload->addVendorPrefixedAutoloadToVendorAutoload();
517    }
518
519    /**
520     * 7.
521     * Delete source files if desired.
522     * Delete empty directories in destination.
523     */
524    protected function cleanUp(): void
525    {
526
527        $this->logger->notice('Cleaning up...');
528
529        $cleanup = new Cleanup(
530            $this->config,
531            $this->filesystem,
532            $this->logger
533        );
534
535        // This will check the config to check should it delete or not.
536        $cleanup->deleteFiles($this->flatDependencyTree, $this->discoveredFiles);
537
538        $cleanup->cleanupVendorInstalledJson($this->flatDependencyTree, $this->discoveredSymbols);
539        if ($this->config->isDeleteVendorFiles() || $this->config->isDeleteVendorPackages()) {
540            // Rebuild the autoloader after cleanup.
541            // This is needed because cleanup may have deleted files that were in the autoloader.
542            $cleanup->rebuildVendorAutoloader();
543        }
544    }
545}