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