Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
24.30% |
26 / 107 |
|
12.50% |
1 / 8 |
CRAP | |
0.00% |
0 / 1 |
ReplaceCommand | |
24.30% |
26 / 107 |
|
12.50% |
1 / 8 |
99.03 | |
0.00% |
0 / 1 |
configure | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 | |||
createConfig | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
enumerateFiles | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
determineChanges | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
performReplacements | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
performReplacementsInProjectFiles | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
addLicenses | |
0.00% |
0 / 11 |
|
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 | |
8 | namespace BrianHenryIE\Strauss\Console\Commands; |
9 | |
10 | use BrianHenryIE\Strauss\Composer\ComposerPackage; |
11 | use BrianHenryIE\Strauss\Composer\Extra\ReplaceConfigInterface; |
12 | use BrianHenryIE\Strauss\Composer\Extra\StraussConfig; |
13 | use BrianHenryIE\Strauss\Files\DiscoveredFiles; |
14 | use BrianHenryIE\Strauss\Helpers\FileSystem; |
15 | use BrianHenryIE\Strauss\Pipeline\ChangeEnumerator; |
16 | use BrianHenryIE\Strauss\Pipeline\FileEnumerator; |
17 | use BrianHenryIE\Strauss\Pipeline\FileSymbolScanner; |
18 | use BrianHenryIE\Strauss\Pipeline\Licenser; |
19 | use BrianHenryIE\Strauss\Pipeline\Prefixer; |
20 | use BrianHenryIE\Strauss\Types\DiscoveredSymbols; |
21 | use Exception; |
22 | use League\Flysystem\Local\LocalFilesystemAdapter; |
23 | use Psr\Log\LoggerAwareTrait; |
24 | use Psr\Log\LogLevel; |
25 | use Symfony\Component\Console\Command\Command; |
26 | use Symfony\Component\Console\Input\InputArgument; |
27 | use Symfony\Component\Console\Input\InputInterface; |
28 | use Symfony\Component\Console\Logger\ConsoleLogger; |
29 | use Symfony\Component\Console\Output\OutputInterface; |
30 | |
31 | class 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 | } |