Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like PuliPluginImpl often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use PuliPluginImpl, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
36 | class PuliPluginImpl |
||
37 | { |
||
38 | /** |
||
39 | * The version of the Puli plugin. |
||
40 | */ |
||
41 | const VERSION = '@package_version@'; |
||
42 | |||
43 | /** |
||
44 | * The minimum version of the Puli CLI. |
||
45 | */ |
||
46 | const MIN_CLI_VERSION = '1.0.0-beta10'; |
||
47 | |||
48 | /** |
||
49 | * The maximum version of the Puli CLI. |
||
50 | */ |
||
51 | const MAX_CLI_VERSION = '1.999.99999'; |
||
52 | |||
53 | /** |
||
54 | * The name of the installer. |
||
55 | */ |
||
56 | const INSTALLER_NAME = 'composer'; |
||
57 | |||
58 | /** |
||
59 | * @var Composer |
||
60 | */ |
||
61 | private $composer; |
||
62 | |||
63 | /** |
||
64 | * @var IOInterface |
||
65 | */ |
||
66 | private $io; |
||
67 | |||
68 | /** |
||
69 | * @var Config |
||
70 | */ |
||
71 | private $config; |
||
72 | |||
73 | /** |
||
74 | * @var bool |
||
75 | */ |
||
76 | private $isDev; |
||
77 | |||
78 | /** |
||
79 | * @var PuliRunner |
||
80 | */ |
||
81 | private $puliRunner; |
||
82 | |||
83 | /** |
||
84 | * @var string |
||
85 | */ |
||
86 | private $rootDir; |
||
87 | |||
88 | /** |
||
89 | * @var bool |
||
90 | */ |
||
91 | private $runPreAutoloadDump = true; |
||
92 | |||
93 | /** |
||
94 | * @var bool |
||
95 | */ |
||
96 | private $runPostAutoloadDump = true; |
||
97 | |||
98 | /** |
||
99 | * @var bool |
||
100 | */ |
||
101 | private $runPostInstall = true; |
||
102 | |||
103 | /** |
||
104 | * @var bool |
||
105 | */ |
||
106 | private $initialized = false; |
||
107 | |||
108 | public function __construct(Event $event, PuliRunner $puliRunner = null) |
||
109 | { |
||
110 | $this->composer = $event->getComposer(); |
||
111 | 42 | $this->io = $event->getIO(); |
|
112 | $this->config = $this->composer->getConfig(); |
||
113 | 42 | $this->isDev = $event->isDevMode(); |
|
114 | 42 | $this->puliRunner = $puliRunner; |
|
115 | 42 | $this->rootDir = Path::normalize(getcwd()); |
|
116 | 42 | } |
|
117 | 42 | ||
118 | 42 | public function preAutoloadDump() |
|
119 | { |
||
120 | 42 | // This method is called multiple times. Run it only once. |
|
121 | if (!$this->runPreAutoloadDump) { |
||
122 | return; |
||
123 | } |
||
124 | 42 | ||
125 | $this->runPreAutoloadDump = false; |
||
126 | 42 | ||
127 | 42 | $factoryClass = $this->getConfigKeyFromJsonFile('factory.in.class'); |
|
128 | $factoryFile = $this->getConfigKeyFromJsonFile('factory.in.file'); |
||
129 | 3 | $factoryFile = Path::makeAbsolute($factoryFile, $this->rootDir); |
|
130 | |||
131 | 3 | $autoload = $this->composer->getPackage()->getAutoload(); |
|
132 | 3 | $autoload['classmap'][] = $factoryFile; |
|
133 | |||
134 | $this->composer->getPackage()->setAutoload($autoload); |
||
135 | |||
136 | 3 | if (!file_exists($factoryFile)) { |
|
137 | $filesystem = new Filesystem(); |
||
138 | // Let Composer find the factory class with a temporary stub |
||
139 | |||
140 | 3 | $namespace = explode('\\', ltrim($factoryClass, '\\')); |
|
141 | $className = array_pop($namespace); |
||
142 | |||
143 | 3 | if (count($namespace)) { |
|
144 | 3 | $stub = '<?php namespace '.implode('\\', $namespace).'; class '.$className.' {}'; |
|
145 | } else { |
||
146 | $stub = '<?php class '.$className.' {}'; |
||
147 | } |
||
148 | |||
149 | $filesystem->dumpFile($factoryFile, $stub); |
||
150 | } |
||
151 | 3 | } |
|
152 | |||
153 | 3 | public function postAutoloadDump() |
|
154 | 3 | { |
|
155 | if (!$this->initialized) { |
||
156 | 3 | $this->initialize(); |
|
157 | } |
||
158 | 3 | ||
159 | 2 | // This method is called multiple times. Run it only once. |
|
160 | if (!$this->runPostAutoloadDump) { |
||
161 | return; |
||
162 | 2 | } |
|
163 | 2 | ||
164 | $this->runPostAutoloadDump = false; |
||
165 | 2 | ||
166 | 1 | try { |
|
167 | $factoryClass = $this->getConfigKey('factory.in.class'); |
||
168 | 1 | } catch (PuliRunnerException $e) { |
|
169 | $this->printWarning('Could not load Puli configuration', $e); |
||
170 | |||
171 | 2 | return; |
|
172 | } |
||
173 | 3 | ||
174 | $vendorDir = $this->config->get('vendor-dir'); |
||
175 | 5 | ||
176 | // On TravisCI, $vendorDir is a relative path. Probably an old Composer |
||
177 | 5 | // build or something. Usually, $vendorDir should be absolute already. |
|
178 | 5 | $vendorDir = Path::makeAbsolute($vendorDir, $this->rootDir); |
|
179 | |||
180 | $autoloadFile = $vendorDir.'/autoload.php'; |
||
181 | $this->insertFactoryClassConstant($autoloadFile, $factoryClass); |
||
182 | 5 | $this->setBootstrapFile($autoloadFile); |
|
183 | 1 | } |
|
184 | |||
185 | /** |
||
186 | 5 | * Updates the Puli repository after Composer installations/updates. |
|
187 | */ |
||
188 | public function postInstall() |
||
189 | 5 | { |
|
190 | 1 | if (!$this->initialized) { |
|
191 | 1 | $this->initialize(); |
|
192 | } |
||
193 | 1 | ||
194 | // This method is called multiple times. Run it only once. |
||
195 | if (!$this->runPostInstall) { |
||
196 | 4 | return; |
|
197 | 4 | } |
|
198 | 4 | ||
199 | $this->runPostInstall = false; |
||
200 | |||
201 | $this->io->write('<info>Synchronizing Puli with Composer</info>'); |
||
202 | |||
203 | 34 | $rootPackage = $this->composer->getPackage(); |
|
204 | $composerPackages = $this->loadComposerPackages(); |
||
205 | 34 | $prodPackageNames = $this->filterProdPackageNames($composerPackages, $rootPackage); |
|
206 | 34 | $env = $this->isDev ? PuliPackage::ENV_DEV : PuliPackage::ENV_PROD; |
|
207 | |||
208 | try { |
||
209 | $puliPackages = $this->loadPuliPackages(); |
||
210 | 34 | } catch (PuliRunnerException $e) { |
|
211 | 3 | $this->printWarning('Could not load Puli packages', $e); |
|
212 | |||
213 | return; |
||
214 | 32 | } |
|
215 | |||
216 | 32 | // Don't remove non-existing packages in production environment |
|
217 | // Removed packages could be dev dependencies (i.e. "require-dev" |
||
218 | 32 | // of the root package or "require" of another dev dependency), and |
|
219 | 32 | // we can't find out whether they are since Composer doesn't load them |
|
220 | 32 | if (PuliPackage::ENV_PROD !== $env) { |
|
221 | 32 | $this->removeRemovedPackages($composerPackages, $puliPackages); |
|
222 | } |
||
223 | |||
224 | 32 | $this->installNewPackages($composerPackages, $prodPackageNames, $puliPackages); |
|
225 | 1 | ||
226 | 1 | // Don't print warnings for non-existing packages in production |
|
227 | if (PuliPackage::ENV_PROD !== $env) { |
||
228 | 1 | $this->checkForNotFoundErrors($puliPackages); |
|
229 | } |
||
230 | |||
231 | $this->checkForNotLoadableErrors($puliPackages); |
||
232 | $this->adoptComposerName($puliPackages); |
||
233 | $this->removePuliDir(); |
||
234 | $this->buildPuli(); |
||
235 | 31 | } |
|
236 | 3 | ||
237 | private function initialize() |
||
238 | { |
||
239 | 31 | $this->initialized = true; |
|
240 | |||
241 | // Keep the manually set runner |
||
242 | 31 | if (null === $this->puliRunner) { |
|
243 | 3 | try { |
|
244 | // Add Composer's bin directory in case the "puli" executable is |
||
245 | // installed with Composer |
||
246 | 31 | $this->puliRunner = new PuliRunner($this->config->get('bin-dir')); |
|
247 | 31 | } catch (RuntimeException $e) { |
|
248 | 31 | $this->printWarning('Plugin initialization failed', $e); |
|
249 | 31 | $this->runPreAutoloadDump = false; |
|
250 | 31 | $this->runPostAutoloadDump = false; |
|
251 | $this->runPostInstall = false; |
||
252 | 42 | } |
|
253 | } |
||
254 | 42 | ||
255 | // Use the runner to verify if Puli has the right version |
||
256 | try { |
||
257 | $this->verifyPuliVersion(); |
||
258 | } catch (RuntimeException $e) { |
||
259 | $this->printWarning('Version check failed', $e); |
||
260 | $this->runPreAutoloadDump = false; |
||
261 | 42 | $this->runPostAutoloadDump = false; |
|
262 | $this->runPostInstall = false; |
||
263 | } |
||
264 | 42 | } |
|
265 | |||
266 | /** |
||
267 | * @param PackageInterface[] $composerPackages |
||
268 | * @param bool[] $prodPackageNames |
||
269 | * @param PuliPackage[] $puliPackages |
||
270 | */ |
||
271 | private function installNewPackages(array $composerPackages, array $prodPackageNames, array &$puliPackages) |
||
272 | { |
||
273 | $installationManager = $this->composer->getInstallationManager(); |
||
274 | |||
275 | foreach ($composerPackages as $packageName => $package) { |
||
276 | if ($package instanceof AliasPackage) { |
||
277 | $package = $package->getAliasOf(); |
||
278 | } |
||
279 | 42 | ||
280 | 2 | // We need to normalize the system-dependent paths returned by Composer |
|
281 | 2 | $installPath = Path::normalize($installationManager->getInstallPath($package)); |
|
282 | 2 | $env = isset($prodPackageNames[$packageName]) ? PuliPackage::ENV_PROD : PuliPackage::ENV_DEV; |
|
283 | 2 | ||
284 | 2 | // Skip meta packages |
|
285 | if ('' === $installPath) { |
||
286 | 42 | continue; |
|
287 | } |
||
288 | |||
289 | if (isset($puliPackages[$packageName])) { |
||
290 | $puliPackage = $puliPackages[$packageName]; |
||
291 | |||
292 | // Only proceed if the install path or environment has changed |
||
293 | 31 | if ($installPath === $puliPackage->getInstallPath() && $env === $puliPackage->getEnvironment()) { |
|
294 | continue; |
||
295 | 31 | } |
|
296 | |||
297 | 31 | // Only remove packages installed by Composer |
|
298 | 21 | if (self::INSTALLER_NAME === $puliPackage->getInstallerName()) { |
|
299 | 2 | $this->io->write(sprintf( |
|
300 | 'Reinstalling <info>%s</info> (<comment>%s</comment>) in <comment>%s</comment>', |
||
301 | $packageName, |
||
302 | Path::makeRelative($installPath, $this->rootDir), |
||
303 | 21 | $env |
|
304 | 21 | )); |
|
305 | |||
306 | try { |
||
307 | 21 | $this->removePackage($packageName); |
|
308 | 1 | } catch (PuliRunnerException $e) { |
|
309 | $this->printPackageWarning('Could not remove package "%s" (at "%s")', $packageName, $installPath, $e); |
||
310 | |||
311 | 20 | continue; |
|
312 | 15 | } |
|
313 | } |
||
314 | } else { |
||
315 | 15 | $this->io->write(sprintf( |
|
316 | 14 | 'Installing <info>%s</info> (<comment>%s</comment>) in <comment>%s</comment>', |
|
317 | $packageName, |
||
318 | Path::makeRelative($installPath, $this->rootDir), |
||
319 | $env |
||
320 | 6 | )); |
|
321 | 5 | } |
|
322 | 5 | ||
323 | try { |
||
324 | 5 | $this->installPackage($installPath, $packageName, $env); |
|
325 | } catch (PuliRunnerException $e) { |
||
326 | $this->printPackageWarning('Could not install package "%s" (at "%s")', $packageName, $installPath, $e); |
||
327 | |||
328 | continue; |
||
329 | 5 | } |
|
330 | 1 | ||
331 | 1 | $puliPackages[$packageName] = new PuliPackage( |
|
332 | $packageName, |
||
333 | 6 | self::INSTALLER_NAME, |
|
334 | $installPath, |
||
335 | PuliPackage::STATE_ENABLED, |
||
336 | $env |
||
337 | 6 | ); |
|
338 | 6 | } |
|
339 | } |
||
340 | 6 | ||
341 | /** |
||
342 | * @param PackageInterface[] $composerPackages |
||
343 | * @param PuliPackage[] $puliPackages |
||
344 | */ |
||
345 | private function removeRemovedPackages(array $composerPackages, array &$puliPackages) |
||
346 | 11 | { |
|
347 | 3 | /** @var PuliPackage[] $notFoundPackages */ |
|
348 | 3 | $notFoundPackages = array_filter($puliPackages, function (PuliPackage $package) { |
|
349 | return PuliPackage::STATE_NOT_FOUND === $package->getState() |
||
350 | 3 | && PuliPluginImpl::INSTALLER_NAME === $package->getInstallerName(); |
|
351 | }); |
||
352 | |||
353 | 8 | foreach ($notFoundPackages as $packageName => $package) { |
|
354 | // Check whether package was only moved |
||
355 | 8 | if (isset($composerPackages[$packageName])) { |
|
356 | continue; |
||
357 | 8 | } |
|
358 | |||
359 | $this->io->write(sprintf( |
||
360 | 'Removing <info>%s</info> (<comment>%s</comment>)', |
||
361 | 31 | $packageName, |
|
362 | Path::makeRelative($package->getInstallPath(), $this->rootDir) |
||
363 | )); |
||
364 | |||
365 | try { |
||
366 | $this->removePackage($packageName); |
||
367 | 3 | } catch (PuliRunnerException $e) { |
|
368 | $this->printPackageWarning('Could not remove package "%s" (at "%s")', $packageName, $package->getInstallPath(), $e); |
||
369 | |||
370 | continue; |
||
371 | 3 | } |
|
372 | 3 | ||
373 | 3 | unset($puliPackages[$packageName]); |
|
374 | } |
||
375 | 3 | } |
|
376 | |||
377 | 3 | View Code Duplication | private function checkForNotFoundErrors(array $puliPackages) |
|
|||
378 | 1 | { |
|
379 | /** @var PuliPackage[] $notFoundPackages */ |
||
380 | $notFoundPackages = array_filter($puliPackages, |
||
381 | 2 | function (PuliPackage $package) { |
|
382 | 2 | return PuliPackage::STATE_NOT_FOUND === $package->getState() |
|
383 | && PuliPluginImpl::INSTALLER_NAME === $package->getInstallerName(); |
||
384 | 2 | }); |
|
385 | |||
386 | foreach ($notFoundPackages as $package) { |
||
387 | $this->printPackageWarning( |
||
388 | 2 | 'The package "%s" (at "%s") could not be found', |
|
389 | 1 | $package->getName(), |
|
390 | 1 | $package->getInstallPath() |
|
391 | ); |
||
392 | 1 | } |
|
393 | } |
||
394 | |||
395 | 1 | View Code Duplication | private function checkForNotLoadableErrors(array $puliPackages) |
411 | 2 | ||
412 | 2 | private function adoptComposerName(array $puliPackages) |
|
413 | { |
||
414 | $rootDir = $this->rootDir; |
||
415 | 3 | ||
416 | /** @var PuliPackage[] $rootPackages */ |
||
417 | 31 | $rootPackages = array_filter($puliPackages, function (PuliPackage $package) use ($rootDir) { |
|
418 | return !$package->getInstallerName() && $rootDir === $package->getInstallPath(); |
||
419 | }); |
||
420 | |||
421 | 31 | if (0 === count($rootPackages)) { |
|
422 | 31 | // This should never happen |
|
423 | 31 | $this->printWarning('No root package could be found'); |
|
424 | |||
425 | 31 | return; |
|
426 | 1 | } |
|
427 | 1 | ||
428 | 1 | if (count($rootPackages) > 1) { |
|
429 | 1 | // This should never happen |
|
430 | $this->printWarning('More than one root package was found'); |
||
431 | |||
432 | 31 | return; |
|
433 | } |
||
434 | 31 | ||
435 | /** @var PuliPackage $rootPackage */ |
||
436 | 31 | $rootPackage = reset($rootPackages); |
|
437 | $name = $rootPackage->getName(); |
||
438 | $newName = $this->composer->getPackage()->getName(); |
||
439 | 31 | ||
440 | 31 | // Rename the root package after changing the name in composer.json |
|
441 | 31 | if ($name !== $newName) { |
|
442 | try { |
||
443 | 31 | $this->renamePackage($name, $newName); |
|
444 | } catch (PuliRunnerException $e) { |
||
445 | $this->printWarning(sprintf( |
||
446 | 'Could not rename root package to "%s"', |
||
447 | $newName |
||
448 | ), $e); |
||
449 | } |
||
450 | 31 | } |
|
451 | } |
||
452 | |||
453 | private function insertFactoryClassConstant($autoloadFile, $factoryClass) |
||
479 | |||
480 | private function setBootstrapFile($autoloadFile) |
||
481 | { |
||
482 | $bootstrapFile = $this->getConfigKey('bootstrap-file'); |
||
483 | |||
484 | 4 | // Don't change user-defined bootstrap files |
|
485 | if (!empty($bootstrapFile)) { |
||
486 | 4 | return; |
|
487 | 4 | } |
|
488 | 4 | ||
495 | |||
496 | 4 | /** |
|
497 | * Loads Composer's currently installed packages. |
||
498 | * |
||
499 | 4 | * @return PackageInterface[] The installed packages indexed by their names |
|
500 | 4 | */ |
|
501 | private function loadComposerPackages() |
||
513 | 3 | ||
514 | private function getConfigKeyFromJsonFile($key) |
||
546 | |||
547 | 38 | private function getConfigKey($key) |
|
564 | |||
565 | 32 | private function setConfigKey($key, $value) |
|
572 | |||
573 | /** |
||
574 | 31 | * @return PuliPackage[] |
|
575 | 31 | */ |
|
576 | 31 | private function loadPuliPackages() |
|
603 | |||
604 | 7 | private function installPackage($installPath, $packageName, $env) |
|
614 | |||
615 | 31 | private function removePackage($packageName) |
|
621 | |||
622 | 1 | private function removePuliDir() |
|
639 | 2 | ||
640 | 2 | private function buildPuli() |
|
646 | |||
647 | private function renamePackage($name, $newName) |
||
654 | |||
655 | /** |
||
656 | * @param $message |
||
657 | 10 | * @param Exception|null $exception |
|
658 | 8 | */ |
|
659 | 10 | private function printWarning($message, Exception $exception = null) |
|
679 | |||
680 | private function printPackageWarning($message, $packageName, $installPath, PuliRunnerException $exception = null) |
||
688 | |||
689 | private function filterProdPackageNames(array $composerPackages, PackageInterface $package, array &$result = array()) |
||
712 | |||
713 | private function verifyPuliVersion() |
||
752 | } |
||
753 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.