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