Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
25.24% |
26 / 103 |
|
12.50% |
1 / 8 |
CRAP | |
0.00% |
0 / 1 |
ReplaceCommand | |
25.24% |
26 / 103 |
|
12.50% |
1 / 8 |
95.89 | |
0.00% |
0 / 1 |
configure | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
0.00% |
0 / 19 |
|
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 / 11 |
|
0.00% |
0 / 1 |
2 | |||
performReplacements | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
performReplacementsInProjectFiles | |
0.00% |
0 / 18 |
|
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->enumerateFiles($config); |
122 | |
123 | $this->determineChanges($config); |
124 | |
125 | $this->performReplacements($config); |
126 | |
127 | $this->performReplacementsInProjectFiles($config); |
128 | |
129 | $this->addLicenses($config); |
130 | } catch (Exception $e) { |
131 | $this->logger->error($e->getMessage()); |
132 | |
133 | return 1; |
134 | } |
135 | |
136 | return Command::SUCCESS; |
137 | } |
138 | |
139 | protected function createConfig(InputInterface $input): ReplaceConfigInterface |
140 | { |
141 | $config = new StraussConfig(); |
142 | |
143 | $from = $input->getOption('from'); |
144 | $to = $input->getOption('to'); |
145 | |
146 | // TODO: |
147 | $config->setNamespaceReplacementPatterns([$from => $to]); |
148 | |
149 | $paths = explode(',', $input->getOption('paths')); |
150 | |
151 | $config->setUpdateCallSites($paths); |
152 | |
153 | return $config; |
154 | } |
155 | |
156 | |
157 | protected function enumerateFiles(ReplaceConfigInterface $config): void |
158 | { |
159 | $this->logger->info('Enumerating files...'); |
160 | $relativeUpdateCallSites = $config->getUpdateCallSites(); |
161 | $updateCallSites = array_map( |
162 | fn($path) => false !== strpos($path, trim($this->workingDir, '/')) ? $path : $this->workingDir . $path, |
163 | $relativeUpdateCallSites |
164 | ); |
165 | $fileEnumerator = new FileEnumerator($config, $this->filesystem); |
166 | $this->discoveredFiles = $fileEnumerator->compileFileListForPaths($updateCallSites); |
167 | } |
168 | |
169 | // 4. Determine namespace and classname changes |
170 | protected function determineChanges(ReplaceConfigInterface $config): void |
171 | { |
172 | $this->logger->info('Determining changes...'); |
173 | |
174 | $fileScanner = new FileSymbolScanner( |
175 | $config, |
176 | $this->filesystem |
177 | ); |
178 | |
179 | $this->discoveredSymbols = $fileScanner->findInFiles($this->discoveredFiles); |
180 | |
181 | $changeEnumerator = new ChangeEnumerator( |
182 | $config, |
183 | $this->filesystem |
184 | ); |
185 | $changeEnumerator->determineReplacements($this->discoveredSymbols); |
186 | } |
187 | |
188 | // 5. Update namespaces and class names. |
189 | // Replace references to updated namespaces and classnames throughout the dependencies. |
190 | protected function performReplacements(ReplaceConfigInterface $config): void |
191 | { |
192 | $this->logger->info('Performing replacements...'); |
193 | |
194 | $this->replacer = new Prefixer($config, $this->filesystem, $this->logger); |
195 | |
196 | $this->replacer->replaceInFiles($this->discoveredSymbols, $this->discoveredFiles->getFiles()); |
197 | } |
198 | |
199 | protected function performReplacementsInProjectFiles(ReplaceConfigInterface $config): void |
200 | { |
201 | |
202 | $relativeCallSitePaths = $this->config->getUpdateCallSites(); |
203 | |
204 | if (empty($relativeCallSitePaths)) { |
205 | return; |
206 | } |
207 | |
208 | $callSitePaths = array_map( |
209 | fn($path) => false !== strpos($path, trim($this->workingDir, '/')) ? $path : $this->workingDir . $path, |
210 | $relativeCallSitePaths |
211 | ); |
212 | |
213 | $projectReplace = new Prefixer($config, $this->filesystem, $this->logger); |
214 | |
215 | $fileEnumerator = new FileEnumerator( |
216 | $config, |
217 | $this->filesystem |
218 | ); |
219 | |
220 | $phpFilePaths = $fileEnumerator->compileFileListForPaths($callSitePaths); |
221 | |
222 | // TODO: Warn when a file that was specified is not found (during config validation). |
223 | // $this->logger->warning('Expected file not found from project autoload: ' . $absolutePath); |
224 | |
225 | $phpFilesAbsolutePaths = array_map( |
226 | fn($file) => $file->getSourcePath(), |
227 | $phpFilePaths->getFiles() |
228 | ); |
229 | |
230 | $projectReplace->replaceInProjectFiles($this->discoveredSymbols, $phpFilesAbsolutePaths); |
231 | } |
232 | |
233 | |
234 | protected function addLicenses(ReplaceConfigInterface $config): void |
235 | { |
236 | $this->logger->info('Adding licenses...'); |
237 | |
238 | $username = trim(shell_exec('git config user.name')); |
239 | $email = trim(shell_exec('git config user.email')); |
240 | |
241 | if (!empty($username) && !empty($email)) { |
242 | // e.g. "Brian Henry <BrianHenryIE@gmail.com>". |
243 | $author = $username . ' <' . $email . '>'; |
244 | } else { |
245 | // e.g. "brianhenry". |
246 | $author = get_current_user(); |
247 | } |
248 | |
249 | // TODO: Update to use DiscoveredFiles |
250 | $dependencies = $this->flatDependencyTree; |
251 | $licenser = new Licenser($config, $dependencies, $author, $this->filesystem, $this->logger); |
252 | |
253 | $licenser->copyLicenses(); |
254 | |
255 | $modifiedFiles = $this->replacer->getModifiedFiles(); |
256 | $licenser->addInformationToUpdatedFiles($modifiedFiles); |
257 | } |
258 | } |