Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
24.30% covered (danger)
24.30%
26 / 107
12.50% covered (danger)
12.50%
1 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReplaceCommand
24.30% covered (danger)
24.30%
26 / 107
12.50% covered (danger)
12.50%
1 / 8
99.03
0.00% covered (danger)
0.00%
0 / 1
 configure
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
1
 execute
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 createConfig
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 enumerateFiles
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 determineChanges
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 performReplacements
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 performReplacementsInProjectFiles
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 addLicenses
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Rename a namespace in files. (in-place renaming)
4 *
5 * strauss replace --from "YourCompany\\Project" --to "BrianHenryIE\\MyProject" --paths "includes,my-plugin.php"
6 */
7
8namespace BrianHenryIE\Strauss\Console\Commands;
9
10use BrianHenryIE\Strauss\Composer\ComposerPackage;
11use BrianHenryIE\Strauss\Composer\Extra\ReplaceConfigInterface;
12use BrianHenryIE\Strauss\Composer\Extra\StraussConfig;
13use BrianHenryIE\Strauss\Files\DiscoveredFiles;
14use BrianHenryIE\Strauss\Helpers\FileSystem;
15use BrianHenryIE\Strauss\Pipeline\ChangeEnumerator;
16use BrianHenryIE\Strauss\Pipeline\FileEnumerator;
17use BrianHenryIE\Strauss\Pipeline\FileSymbolScanner;
18use BrianHenryIE\Strauss\Pipeline\Licenser;
19use BrianHenryIE\Strauss\Pipeline\Prefixer;
20use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
21use Exception;
22use League\Flysystem\Local\LocalFilesystemAdapter;
23use Psr\Log\LoggerAwareTrait;
24use Psr\Log\LogLevel;
25use Symfony\Component\Console\Command\Command;
26use Symfony\Component\Console\Input\InputArgument;
27use Symfony\Component\Console\Input\InputInterface;
28use Symfony\Component\Console\Logger\ConsoleLogger;
29use Symfony\Component\Console\Output\OutputInterface;
30
31class ReplaceCommand extends Command
32{
33    use LoggerAwareTrait;
34
35    /** @var string */
36    protected string $workingDir;
37
38    protected ReplaceConfigInterface $config;
39
40    /** @var Prefixer */
41    protected Prefixer $replacer;
42
43    /** @var ComposerPackage[] */
44    protected array $flatDependencyTree = [];
45
46    /**
47     * ArrayAccess of \BrianHenryIE\Strauss\File objects indexed by their path relative to the output target directory.
48     *
49     * Each object contains the file's relative and absolute paths, the package and autoloaders it came from,
50     * and flags indicating should it / has it been copied / deleted etc.
51     *
52     */
53    protected DiscoveredFiles $discoveredFiles;
54    protected DiscoveredSymbols $discoveredSymbols;
55
56    protected Filesystem $filesystem;
57
58    /**
59     * @return void
60     */
61    protected function configure()
62    {
63        $this->setName('replace');
64        $this->setDescription("Rename a namespace in files.");
65        $this->setHelp('');
66
67        $this->addOption(
68            'from',
69            null,
70            InputArgument::OPTIONAL,
71            'Original namespace'
72        );
73
74        $this->addOption(
75            'to',
76            null,
77            InputArgument::OPTIONAL,
78            'New namespace'
79        );
80
81        $this->addOption(
82            'paths',
83            null,
84            InputArgument::OPTIONAL,
85            'Comma separated list of files and directories to update. Default is the current working directory.',
86            getcwd()
87        );
88
89        // TODO: permissions?
90        $this->filesystem = new Filesystem(
91            new \League\Flysystem\Filesystem(new LocalFilesystemAdapter('/')),
92            getcwd() . '/'
93        );
94    }
95
96    /**
97     * @param InputInterface $input
98     * @param OutputInterface $output
99     *
100     * @see Command::execute()
101     *
102     */
103    protected function execute(InputInterface $input, OutputInterface $output): int
104    {
105        $this->setLogger(
106            new ConsoleLogger(
107                $output,
108                [ LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL ]
109            )
110        );
111
112        $workingDir       = getcwd() . '/';
113        $this->workingDir = $workingDir;
114
115        try {
116            $config = $this->createConfig($input);
117            $this->config = $config;
118
119            // Pipeline
120
121            $this->discoveredSymbols = new DiscoveredSymbols();
122
123            $this->enumerateFiles($config);
124
125            $this->determineChanges($config);
126
127            $this->performReplacements($config);
128
129            $this->performReplacementsInProjectFiles($config);
130
131            $this->addLicenses($config);
132        } catch (Exception $e) {
133            $this->logger->error($e->getMessage());
134
135            return 1;
136        }
137
138        return Command::SUCCESS;
139    }
140
141    protected function createConfig(InputInterface $input): ReplaceConfigInterface
142    {
143        $config = new StraussConfig();
144
145        $from = $input->getOption('from');
146        $to = $input->getOption('to');
147
148        // TODO:
149        $config->setNamespaceReplacementPatterns([$from => $to]);
150
151        $paths = explode(',', $input->getOption('paths'));
152
153        $config->setUpdateCallSites($paths);
154
155        return $config;
156    }
157
158
159    protected function enumerateFiles(ReplaceConfigInterface $config): void
160    {
161        $this->logger->info('Enumerating files...');
162        $relativeUpdateCallSites = $config->getUpdateCallSites();
163        $updateCallSites = array_map(
164            fn($path) => false !== strpos($path, trim($this->workingDir, '/')) ? $path : $this->workingDir . $path,
165            $relativeUpdateCallSites
166        );
167        $fileEnumerator = new FileEnumerator($config, $this->filesystem, $this->logger);
168        $this->discoveredFiles = $fileEnumerator->compileFileListForPaths($updateCallSites);
169    }
170
171    // 4. Determine namespace and classname changes
172    protected function determineChanges(ReplaceConfigInterface $config): void
173    {
174        $this->logger->info('Determining changes...');
175
176        $fileScanner = new FileSymbolScanner(
177            $config,
178            $this->discoveredSymbols,
179            $this->filesystem
180        );
181
182        $fileScanner->findInFiles($this->discoveredFiles);
183
184        $changeEnumerator = new ChangeEnumerator(
185            $config,
186            $this->filesystem
187        );
188        $changeEnumerator->markFilesForExclusion($this->discoveredFiles);
189        $changeEnumerator->determineReplacements($this->discoveredSymbols);
190    }
191
192    // 5. Update namespaces and class names.
193    // Replace references to updated namespaces and classnames throughout the dependencies.
194    protected function performReplacements(ReplaceConfigInterface $config): void
195    {
196        $this->logger->info('Performing replacements...');
197
198        $this->replacer = new Prefixer($config, $this->filesystem, $this->logger);
199
200        $this->replacer->replaceInFiles($this->discoveredSymbols, $this->discoveredFiles->getFiles());
201    }
202
203    protected function performReplacementsInProjectFiles(ReplaceConfigInterface $config): void
204    {
205
206        $relativeCallSitePaths = $this->config->getUpdateCallSites();
207
208        if (empty($relativeCallSitePaths)) {
209            return;
210        }
211
212        $callSitePaths = array_map(
213            fn($path) => false !== strpos($path, trim($this->workingDir, '/')) ? $path : $this->workingDir . $path,
214            $relativeCallSitePaths
215        );
216
217        $projectReplace = new Prefixer($config, $this->filesystem, $this->logger);
218
219        $fileEnumerator = new FileEnumerator(
220            $config,
221            $this->filesystem,
222            $this->logger
223        );
224
225        $phpFilePaths = $fileEnumerator->compileFileListForPaths($callSitePaths);
226
227        // TODO: Warn when a file that was specified is not found (during config validation).
228        // $this->logger->warning('Expected file not found from project autoload: ' . $absolutePath);
229
230        $phpFilesAbsolutePaths = array_map(
231            fn($file) => $file->getSourcePath(),
232            $phpFilePaths->getFiles()
233        );
234
235        $projectReplace->replaceInProjectFiles($this->discoveredSymbols, $phpFilesAbsolutePaths);
236    }
237
238
239    protected function addLicenses(ReplaceConfigInterface $config): void
240    {
241        $this->logger->info('Adding licenses...');
242
243        $username = trim(shell_exec('git config user.name'));
244        $email = trim(shell_exec('git config user.email'));
245
246        if (!empty($username) && !empty($email)) {
247            // e.g. "Brian Henry <BrianHenryIE@gmail.com>".
248            $author = $username . ' <' . $email . '>';
249        } else {
250            // e.g. "brianhenry".
251            $author = get_current_user();
252        }
253
254        // TODO: Update to use DiscoveredFiles
255        $dependencies = $this->flatDependencyTree;
256        $licenser = new Licenser($config, $dependencies, $author, $this->filesystem, $this->logger);
257
258        $licenser->copyLicenses();
259
260        $modifiedFiles = $this->replacer->getModifiedFiles();
261        $licenser->addInformationToUpdatedFiles($modifiedFiles);
262    }
263}