Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
14.50% covered (danger)
14.50%
38 / 262
5.00% covered (danger)
5.00%
1 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
DependenciesCommand
14.50% covered (danger)
14.50%
38 / 262
5.00% covered (danger)
5.00%
1 / 20
940.42
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
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 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 / 25
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->makeAbsolute(
158            $this->filesystem->normalizePath(
159                $this->workingDir . '/' . Factory::getComposerFile()
160            )
161        );
162        $defaultComposerFilePath = $this->filesystem->makeAbsolute($this->workingDir . '/composer.json');
163        if ($composerFilePath !== $defaultComposerFilePath) {
164            $this->logger->info('Using: ' . $composerFilePath);
165        }
166
167        $this->projectComposerPackage = new ProjectComposerPackage($composerFilePath);
168
169        // TODO: Print the config that Strauss is using.
170        // Maybe even highlight what is default config and what is custom config.
171    }
172
173    /**
174     * Load Strauss config from the project's composer.json.
175     */
176    protected function loadConfigFromComposerJson(): void
177    {
178        $this->logger->notice('Loading composer.json config...');
179
180        $this->config = $this->projectComposerPackage->getStraussConfig();
181    }
182
183    protected function updateConfigFromCli(InputInterface $input): void
184    {
185        $this->logger->notice('Loading cli config...');
186
187        $this->config->updateFromCli($input);
188    }
189
190    /**
191     * 2. Built flat list of packages and dependencies.
192     *
193     * 2.1 Initiate getting dependencies for the project composer.json.
194     *
195     * @see DependenciesCommand::flatDependencyTree
196     */
197    protected function buildDependencyList(): void
198    {
199        $this->logger->notice('Building dependency list...');
200
201        $this->dependenciesEnumerator = new DependenciesEnumerator(
202            $this->config,
203            $this->filesystem,
204            $this->logger
205        );
206        $this->flatDependencyTree = $this->dependenciesEnumerator->getAllDependencies();
207
208        $this->config->setPackagesToCopy(
209            array_filter($this->flatDependencyTree, function ($dependency) {
210                return !in_array($dependency, $this->config->getExcludePackagesFromCopy());
211            },
212            ARRAY_FILTER_USE_KEY)
213        );
214
215        $this->config->setPackagesToPrefix(
216            array_filter($this->flatDependencyTree, function ($dependency) {
217                return !in_array($dependency, $this->config->getExcludePackagesFromPrefixing());
218            },
219            ARRAY_FILTER_USE_KEY)
220        );
221
222        foreach ($this->flatDependencyTree as $dependency) {
223            // Sort of duplicating the logic above.
224            $dependency->setCopy(
225                !in_array($dependency->getPackageName(), $this->config->getExcludePackagesFromCopy())
226            );
227
228            if ($this->config->isDeleteVendorPackages()) {
229                $dependency->setDelete(true);
230            }
231        }
232
233        // TODO: Print the dependency tree that Strauss has determined.
234    }
235
236
237    protected function enumerateFiles(): void
238    {
239        $this->logger->notice('Enumerating files...');
240
241        $fileEnumerator = new FileEnumerator(
242            $this->config,
243            $this->filesystem,
244            $this->logger
245        );
246
247        $this->discoveredFiles = $fileEnumerator->compileFileListForDependencies($this->flatDependencyTree);
248    }
249
250    /**
251     * TODO: currently this must run after ::determineChanges() so the discoveredSymbols object exists,
252     * but logically it should run first.
253     */
254    protected function enumeratePsr4Namespaces(): void
255    {
256        foreach ($this->config->getPackagesToPrefix() as $package) {
257            $autoloadKey = $package->getAutoload();
258            if (! isset($autoloadKey['psr-4'])) {
259                continue;
260            }
261
262            $psr4autoloadKey = $autoloadKey['psr-4'];
263            $namespaces = array_keys($psr4autoloadKey);
264
265            $file = new File($package->getPackageAbsolutePath() . '/composer.json', '/../composer.json');
266
267            foreach ($namespaces as $namespace) {
268                // TODO: log.
269                $symbol = new NamespaceSymbol(
270                    trim($namespace, '\\'),
271                    $file,
272                    '\\',
273                    $package
274                );
275                // TODO: respect all config options.
276//              $symbol->setReplacement($this->config->getNamespacePrefix() . '\\' . trim($namespace, '\\'));
277                $this->discoveredSymbols->add($symbol);
278            }
279        }
280    }
281
282    protected function enumerateAutoloadedFiles(): void
283    {
284        $this->logger->notice('Enumerating autoload files...');
285
286        $autoloadFilesEnumerator = new AutoloadedFilesEnumerator(
287            $this->config,
288            $this->filesystem,
289            $this->logger
290        );
291        $autoloadFilesEnumerator->scanForAutoloadedFiles($this->flatDependencyTree);
292    }
293
294    protected function scanFilesForSymbols(): void
295    {
296        $this->logger->notice('Scanning files...');
297
298        $fileSymbolScanner = new FileSymbolScanner(
299            $this->config,
300            $this->discoveredSymbols,
301            $this->filesystem,
302            $this->logger
303        );
304
305        $fileSymbolScanner->findInFiles($this->discoveredFiles);
306    }
307
308    protected function markSymbolsForRenaming(): void
309    {
310
311        $markSymbolsForRenaming = new MarkSymbolsForRenaming(
312            $this->config,
313            $this->filesystem,
314            $this->logger
315        );
316
317        $markSymbolsForRenaming->scanSymbols($this->discoveredSymbols);
318    }
319
320    protected function determineChanges(): void
321    {
322        $this->logger->notice('Determining changes...');
323
324        $changeEnumerator = new ChangeEnumerator(
325            $this->config,
326            $this->logger
327        );
328        $changeEnumerator->determineReplacements($this->discoveredSymbols);
329    }
330
331    protected function analyseFilesToCopy(): void
332    {
333        (new FileCopyScanner($this->config, $this->filesystem, $this->logger))->scanFiles($this->discoveredFiles);
334    }
335
336    protected function copyFiles(): void
337    {
338
339        if ($this->config->isTargetDirectoryVendor()) {
340            // Nothing to do.
341            return;
342        }
343
344        $this->logger->notice('Copying files...');
345
346        $copier = new Copier(
347            $this->discoveredFiles,
348            $this->config,
349            $this->filesystem,
350            $this->logger
351        );
352
353
354        $copier->prepareTarget();
355        $copier->copy();
356
357        foreach ($this->flatDependencyTree as $package) {
358            if ($package->isCopy()) {
359                $package->setDidCopy(true);
360            }
361        }
362
363        $installedJson = new InstalledJson(
364            $this->config,
365            $this->filesystem,
366            $this->logger
367        );
368        $installedJson->copyInstalledJson();
369    }
370
371
372    // 5. Update namespaces and class names.
373    // Replace references to updated namespaces and classnames throughout the dependencies.
374    protected function performReplacements(): void
375    {
376        $this->logger->notice('Performing replacements...');
377
378        $this->replacer = new Prefixer(
379            $this->config,
380            $this->filesystem,
381            $this->logger
382        );
383
384        $this->replacer->replaceInFiles(
385            $this->discoveredSymbols,
386            $this->discoveredFiles->getFiles()
387        );
388    }
389
390    protected function performReplacementsInProjectFiles(): void
391    {
392        // TODO: this doesn't do tests?!
393        $relativeCallSitePaths =
394            $this->config->getUpdateCallSites()
395            ?? $this->projectComposerPackage->getFlatAutoloadKey();
396
397        if (empty($relativeCallSitePaths)) {
398            return;
399        }
400
401        $callSitePaths = array_map(
402            fn($path) => $this->workingDir . '/' . $path,
403            $relativeCallSitePaths
404        );
405
406        $projectReplace = new Prefixer(
407            $this->config,
408            $this->filesystem,
409            $this->logger
410        );
411
412        $fileEnumerator = new FileEnumerator(
413            $this->config,
414            $this->filesystem,
415            $this->logger
416        );
417
418        $projectFiles = $fileEnumerator->compileFileListForPaths($callSitePaths);
419
420        $phpFiles = array_filter(
421            $projectFiles->getFiles(),
422            fn($file) => $file->isPhpFile()
423        );
424
425        $phpFilesAbsolutePaths = array_map(
426            fn($file) => $file->getSourcePath(),
427            $phpFiles
428        );
429
430        // TODO: Warn when a file that was specified is not found
431        // $this->logger->warning('Expected file not found from project autoload: ' . $absolutePath);
432
433        $projectReplace->replaceInProjectFiles($this->discoveredSymbols, $phpFilesAbsolutePaths);
434    }
435
436    protected function addLicenses(): void
437    {
438        $this->logger->notice('Adding licenses...');
439
440        $author = $this->projectComposerPackage->getAuthor();
441
442        $dependencies = $this->flatDependencyTree;
443
444        $licenser = new Licenser(
445            $this->config,
446            $dependencies,
447            $author,
448            $this->filesystem,
449            $this->logger
450        );
451
452        $licenser->copyLicenses();
453
454        $modifiedFiles = $this->replacer->getModifiedFiles();
455        $licenser->addInformationToUpdatedFiles($modifiedFiles);
456    }
457
458    /**
459     * 6. Generate autoloader.
460     */
461    protected function generateAutoloader(): void
462    {
463        if (isset($this->projectComposerPackage->getAutoload()['classmap'])
464            && in_array(
465                $this->config->getAbsoluteTargetDirectory(),
466                array_map(
467                    fn(string $entry) => trim($entry, '\\/'),
468                    $this->projectComposerPackage->getAutoload()['classmap']
469                ),
470                true
471            )
472        ) {
473            $this->logger->notice('Skipping autoloader generation as target directory is in Composer classmap. Run `composer dump-autoload`.');
474            return;
475        }
476
477        $this->logger->notice('Generating autoloader...');
478
479        $allFilesAutoloaders = $this->dependenciesEnumerator->getAllFilesAutoloaders();
480        $filesAutoloaders = array();
481        foreach ($allFilesAutoloaders as $packageName => $packageFilesAutoloader) {
482            if (in_array($packageName, $this->config->getExcludePackagesFromCopy())) {
483                continue;
484            }
485            $filesAutoloaders[$packageName] = $packageFilesAutoloader;
486        }
487
488        $classmap = new Autoload(
489            $this->config,
490            $filesAutoloaders,
491            $this->filesystem,
492            $this->logger
493        );
494
495        $classmap->generate($this->flatDependencyTree, $this->discoveredSymbols);
496    }
497
498    /**
499     * When namespaces are prefixed which are used by both require and require-dev dependencies,
500     * the require-dev dependencies need class aliases specified to point to the new class names/namespaces.
501     */
502    protected function generateAliasesFile(): void
503    {
504        if (!$this->config->isCreateAliases()) {
505            return;
506        }
507
508        $this->logger->notice('Generating aliases file...');
509
510        $aliases = new Aliases(
511            $this->config,
512            $this->filesystem,
513            $this->logger
514        );
515        $aliases->writeAliasesFileForSymbols($this->discoveredSymbols);
516
517        $vendorComposerAutoload = new VendorComposerAutoload(
518            $this->config,
519            $this->filesystem,
520            $this->logger
521        );
522        $vendorComposerAutoload->addAliasesFileToComposer();
523        $vendorComposerAutoload->addVendorPrefixedAutoloadToVendorAutoload();
524    }
525
526    /**
527     * 7.
528     * Delete source files if desired.
529     * Delete empty directories in destination.
530     */
531    protected function cleanUp(): void
532    {
533
534        $this->logger->notice('Cleaning up...');
535
536        $cleanup = new Cleanup(
537            $this->config,
538            $this->filesystem,
539            $this->logger
540        );
541
542        // This will check the config to check should it delete or not.
543        $cleanup->deleteFiles($this->flatDependencyTree, $this->discoveredFiles);
544
545        $cleanup->cleanupVendorInstalledJson($this->flatDependencyTree, $this->discoveredSymbols);
546        if ($this->config->isDeleteVendorFiles() || $this->config->isDeleteVendorPackages()) {
547            // Rebuild the autoloader after cleanup.
548            // This is needed because cleanup may have deleted files that were in the autoloader.
549            $cleanup->rebuildVendorAutoloader();
550        }
551    }
552}