Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.46% covered (danger)
19.46%
29 / 149
11.76% covered (danger)
11.76%
2 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Compose
19.46% covered (danger)
19.46%
29 / 149
11.76% covered (danger)
11.76%
2 / 17
500.14
0.00% covered (danger)
0.00%
0 / 1
 configure
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 execute
48.00% covered (danger)
48.00%
12 / 25
0.00% covered (danger)
0.00%
0 / 1
2.56
 loadProjectComposerPackage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 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 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 enumerateFiles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 copyFiles
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 determineChanges
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 performReplacements
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 performReplacementsInComposerFiles
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 performReplacementsInProjectFiles
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 writeClassAliasMap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addLicenses
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 generateAutoloader
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 generateClassAliasList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 cleanUp
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BrianHenryIE\Strauss\Console\Commands;
4
5use BrianHenryIE\Strauss\ChangeEnumerator;
6use BrianHenryIE\Strauss\FileScanner;
7use BrianHenryIE\Strauss\Autoload;
8use BrianHenryIE\Strauss\Cleanup;
9use BrianHenryIE\Strauss\Composer\ComposerPackage;
10use BrianHenryIE\Strauss\Composer\ProjectComposerPackage;
11use BrianHenryIE\Strauss\Copier;
12use BrianHenryIE\Strauss\DependenciesEnumerator;
13use BrianHenryIE\Strauss\DiscoveredFiles;
14use BrianHenryIE\Strauss\DiscoveredSymbols;
15use BrianHenryIE\Strauss\FileEnumerator;
16use BrianHenryIE\Strauss\Licenser;
17use BrianHenryIE\Strauss\Prefixer;
18use BrianHenryIE\Strauss\Composer\Extra\StraussConfig;
19use Exception;
20use Psr\Log\LoggerAwareTrait;
21use Psr\Log\LogLevel;
22use Symfony\Component\Console\Command\Command;
23use Symfony\Component\Console\Input\InputArgument;
24use Symfony\Component\Console\Input\InputInterface;
25use Symfony\Component\Console\Logger\ConsoleLogger;
26use Symfony\Component\Console\Output\OutputInterface;
27
28class Compose extends Command
29{
30    use LoggerAwareTrait;
31
32    /** @var string */
33    protected string $workingDir;
34
35    /** @var StraussConfig */
36    protected StraussConfig $config;
37
38    protected ProjectComposerPackage $projectComposerPackage;
39
40    /** @var Prefixer */
41    protected Prefixer $replacer;
42
43    protected DependenciesEnumerator $dependenciesEnumerator;
44
45    /** @var ComposerPackage[] */
46    protected array $flatDependencyTree = [];
47
48    /**
49     * ArrayAccess of \BrianHenryIE\Strauss\File objects indexed by their path relative to the output target directory.
50     *
51     * Each object contains the file's relative and absolute paths, the package and autoloaders it came from,
52     * and flags indicating should it / has it been copied / deleted etc.
53     *
54     */
55    protected DiscoveredFiles $discoveredFiles;
56    protected DiscoveredSymbols $discoveredSymbols;
57
58    /**
59     * @return void
60     */
61    protected function configure()
62    {
63        $this->setName('compose');
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            InputArgument::OPTIONAL,
78            'Should original packages be deleted after copying? true|false'
79        );
80    }
81
82    /**
83     * @param InputInterface $input
84     * @param OutputInterface $output
85     *
86     * @return int
87     * @see Command::execute()
88     *
89     */
90    protected function execute(InputInterface $input, OutputInterface $output): int
91    {
92        $this->setLogger(
93            new ConsoleLogger(
94                $output,
95                [ LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL ]
96            )
97        );
98
99        $workingDir       = getcwd() . DIRECTORY_SEPARATOR;
100        $this->workingDir = $workingDir;
101
102        try {
103            $this->loadProjectComposerPackage();
104            $this->loadConfigFromComposerJson();
105            $this->updateConfigFromCli($input);
106
107            $this->buildDependencyList();
108
109            $this->enumerateFiles();
110
111            $this->copyFiles();
112
113            $this->determineChanges();
114
115            $this->performReplacements();
116
117            $this->performReplacementsInComposerFiles();
118
119            $this->performReplacementsInProjectFiles();
120
121            $this->addLicenses();
122
123            $this->generateAutoloader();
124
125            $this->cleanUp();
126        } catch (Exception $e) {
127            $this->logger->error($e->getMessage());
128
129            return 1;
130        }
131
132        return Command::SUCCESS;
133    }
134
135
136    /**
137     * 1. Load the composer.json.
138     *
139     * @throws Exception
140     */
141    protected function loadProjectComposerPackage(): void
142    {
143        $this->logger->info('Loading package...');
144
145        $this->projectComposerPackage = new ProjectComposerPackage($this->workingDir);
146
147        // TODO: Print the config that Strauss is using.
148        // Maybe even highlight what is default config and what is custom config.
149    }
150
151    protected function loadConfigFromComposerJson(): void
152    {
153        $this->logger->info('Loading composer.json config...');
154
155        $this->config = $this->projectComposerPackage->getStraussConfig();
156    }
157
158    protected function updateConfigFromCli(InputInterface $input): void
159    {
160        $this->logger->info('Loading cli config...');
161
162        $this->config->updateFromCli($input);
163    }
164
165    /**
166     * 2. Built flat list of packages and dependencies.
167     *
168     * 2.1 Initiate getting dependencies for the project composer.json.
169     *
170     * @see Compose::flatDependencyTree
171     */
172    protected function buildDependencyList(): void
173    {
174        $this->logger->info('Building dependency list...');
175
176        $this->dependenciesEnumerator = new DependenciesEnumerator(
177            $this->workingDir,
178            $this->config
179        );
180        $this->flatDependencyTree = $this->dependenciesEnumerator->getAllDependencies();
181
182        // TODO: Print the dependency tree that Strauss has determined.
183    }
184
185    protected function enumerateFiles(): void
186    {
187        $this->logger->info('Enumerating files...');
188
189        $fileEnumerator = new FileEnumerator(
190            $this->flatDependencyTree,
191            $this->workingDir,
192            $this->config
193        );
194
195        $this->discoveredFiles = $fileEnumerator->compileFileList();
196    }
197
198    // 3. Copy autoloaded files for each
199    protected function copyFiles(): void
200    {
201        if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
202            // Nothing to do.
203            return;
204        }
205
206        $this->logger->info('Copying files...');
207
208        $copier = new Copier(
209            $this->discoveredFiles,
210            $this->workingDir,
211            $this->config
212        );
213
214        $copier->prepareTarget();
215        $copier->copy();
216    }
217
218    // 4. Determine namespace and classname changes
219    protected function determineChanges(): void
220    {
221        $this->logger->info('Determining changes...');
222
223        $fileScanner = new FileScanner($this->config);
224
225        $this->discoveredSymbols = $fileScanner->findInFiles($this->discoveredFiles);
226
227        $changeEnumerator = new ChangeEnumerator(
228            $this->config,
229            $this->workingDir
230        );
231        $changeEnumerator->determineReplacements($this->discoveredSymbols);
232    }
233
234    // 5. Update namespaces and class names.
235    // Replace references to updated namespaces and classnames throughout the dependencies.
236    protected function performReplacements(): void
237    {
238        $this->logger->info('Performing replacements...');
239
240        $this->replacer = new Prefixer($this->config, $this->workingDir);
241
242        $phpFiles = $this->discoveredFiles->getPhpFilesAndDependencyList();
243
244        $this->replacer->replaceInFiles($this->discoveredSymbols, $phpFiles);
245    }
246
247    protected function performReplacementsInComposerFiles(): void
248    {
249        if ($this->config->getTargetDirectory() !== $this->config->getVendorDirectory()) {
250            // Nothing to do.
251            return;
252        }
253
254        $projectReplace = new Prefixer($this->config, $this->workingDir);
255
256        $fileEnumerator = new FileEnumerator(
257            $this->flatDependencyTree,
258            $this->workingDir,
259            $this->config
260        );
261
262        $composerPhpFileRelativePaths = $fileEnumerator->findFilesInDirectory(
263            $this->workingDir,
264            $this->config->getVendorDirectory() . 'composer'
265        );
266
267        $projectReplace->replaceInProjectFiles($this->discoveredSymbols, $composerPhpFileRelativePaths);
268    }
269
270    protected function performReplacementsInProjectFiles(): void
271    {
272
273        $callSitePaths =
274            $this->config->getUpdateCallSites()
275            ?? $this->projectComposerPackage->getFlatAutoloadKey();
276
277        if (empty($callSitePaths)) {
278            return;
279        }
280
281        $projectReplace = new Prefixer($this->config, $this->workingDir);
282
283        $fileEnumerator = new FileEnumerator(
284            $this->flatDependencyTree,
285            $this->workingDir,
286            $this->config
287        );
288
289        $phpFilesRelativePaths = [];
290        foreach ($callSitePaths as $relativePath) {
291            $absolutePath = $this->workingDir . $relativePath;
292            if (is_dir($absolutePath)) {
293                $phpFilesRelativePaths = array_merge($phpFilesRelativePaths, $fileEnumerator->findFilesInDirectory($this->workingDir, $relativePath));
294            } elseif (is_readable($absolutePath)) {
295                $phpFilesRelativePaths[] = $relativePath;
296            } else {
297                $this->logger->warning('Expected file not found from project autoload: ' . $absolutePath);
298            }
299        }
300
301        $projectReplace->replaceInProjectFiles($this->discoveredSymbols, $phpFilesRelativePaths);
302    }
303
304    protected function writeClassAliasMap(): void
305    {
306    }
307
308    protected function addLicenses(): void
309    {
310        $this->logger->info('Adding licenses...');
311
312        $author = $this->projectComposerPackage->getAuthor();
313
314        $dependencies = $this->flatDependencyTree;
315
316        $licenser = new Licenser($this->config, $this->workingDir, $dependencies, $author);
317
318        $licenser->copyLicenses();
319
320        $modifiedFiles = $this->replacer->getModifiedFiles();
321        $licenser->addInformationToUpdatedFiles($modifiedFiles);
322    }
323
324    /**
325     * 6. Generate autoloader.
326     */
327    protected function generateAutoloader(): void
328    {
329        if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
330            $this->logger->info('Skipping autoloader generation as target directory is vendor directory.');
331            return;
332        }
333        if (isset($this->projectComposerPackage->getAutoload()['classmap'])
334            && in_array(
335                $this->config->getTargetDirectory(),
336                $this->projectComposerPackage->getAutoload()['classmap'],
337                true
338            )
339        ) {
340            $this->logger->info('Skipping autoloader generation as target directory is in Composer classmap. Run `composer dump-autoload`.');
341            return;
342        }
343
344        $this->logger->info('Generating autoloader...');
345
346        $allFilesAutoloaders = $this->dependenciesEnumerator->getAllFilesAutoloaders();
347        $filesAutoloaders = array();
348        foreach ($allFilesAutoloaders as $packageName => $packageFilesAutoloader) {
349            if (in_array($packageName, $this->config->getExcludePackagesFromCopy())) {
350                continue;
351            }
352            $filesAutoloaders[$packageName] = $packageFilesAutoloader;
353        }
354
355        $classmap = new Autoload($this->config, $this->workingDir, $filesAutoloaders);
356
357        $classmap->generate();
358    }
359
360    /**
361     * When namespaces are prefixed which are used by by require and require-dev dependencies,
362     * the require-dev dependencies need class aliases specified to point to the new class names/namespaces.
363     */
364    protected function generateClassAliasList(): void
365    {
366    }
367
368    /**
369     * 7.
370     * Delete source files if desired.
371     * Delete empty directories in destination.
372     */
373    protected function cleanUp(): void
374    {
375        if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
376            // Nothing to do.
377            return;
378        }
379
380        $this->logger->info('Cleaning up...');
381
382        $cleanup = new Cleanup($this->config, $this->workingDir);
383
384        $sourceFiles = array_keys($this->discoveredFiles->getAllFilesAndDependencyList());
385
386        // TODO: For files autoloaders, delete the contents of the file, not the file itself.
387
388        // This will check the config to check should it delete or not.
389        $cleanup->cleanup($sourceFiles);
390    }
391}