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