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 InitCommand 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 InitCommand, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
20 | class InitCommand extends AbstractGenerateCommand { |
||
21 | |||
22 | use QuestionHelperTrait; |
||
23 | |||
24 | private $gitConfig; |
||
25 | 20 | ||
26 | 20 | protected function configure() { |
|
27 | 20 | $this |
|
28 | 20 | ->setName('init') |
|
29 | 20 | ->setDescription('Initializes composer.json with keeko related values') |
|
30 | 20 | ->addOption( |
|
31 | 20 | 'name', |
|
32 | 20 | '', |
|
33 | InputOption::VALUE_REQUIRED, |
||
34 | 20 | 'Name of the package' |
|
35 | 20 | ) |
|
36 | 20 | ->addOption( |
|
37 | 20 | 'description', |
|
38 | 20 | 'd', |
|
39 | InputOption::VALUE_OPTIONAL, |
||
40 | 20 | 'Description of the package' |
|
41 | 20 | ) |
|
42 | 20 | ->addOption( |
|
43 | 20 | 'author', |
|
44 | 20 | '', |
|
45 | InputOption::VALUE_OPTIONAL, |
||
46 | 20 | 'Author name of the package' |
|
47 | 20 | ) |
|
48 | 20 | ->addOption( |
|
49 | 20 | 'type', |
|
50 | 20 | 't', |
|
51 | InputOption::VALUE_REQUIRED, |
||
52 | 20 | 'The type of the package (app|module)' |
|
53 | 20 | ) |
|
54 | 20 | ->addOption( |
|
55 | 20 | 'namespace', |
|
56 | 20 | 'ns', |
|
57 | InputOption::VALUE_OPTIONAL, |
||
58 | 20 | 'The package\'s namespace for the src/ folder (If ommited, the package name is used)' |
|
59 | 20 | ) |
|
60 | 20 | ->addOption( |
|
61 | 20 | 'license', |
|
62 | 20 | 'l', |
|
63 | InputOption::VALUE_OPTIONAL, |
||
64 | 20 | 'License of the package' |
|
65 | 20 | ) |
|
66 | 20 | ->addOption( |
|
67 | 20 | 'title', |
|
68 | 20 | '', |
|
69 | 20 | InputOption::VALUE_OPTIONAL, |
|
70 | 'The package\'s title (If ommited, second part of the package name is used)', |
||
71 | 20 | null |
|
72 | 20 | ) |
|
73 | 20 | ->addOption( |
|
74 | 20 | 'classname', |
|
75 | 20 | 'c', |
|
76 | 20 | InputOption::VALUE_OPTIONAL, |
|
77 | 'The main class name (If ommited, there is a default handler)', |
||
78 | 20 | null |
|
79 | ) |
||
80 | // ->addOption( |
||
81 | // 'default-action', |
||
82 | // '', |
||
83 | // InputOption::VALUE_OPTIONAL, |
||
84 | // 'The module\'s default action' |
||
85 | 20 | // ) |
|
86 | 20 | ->addOption( |
|
87 | 20 | 'force', |
|
88 | 20 | 'f', |
|
89 | InputOption::VALUE_NONE, |
||
90 | 20 | 'Allows to overwrite existing values' |
|
91 | 20 | ) |
|
92 | 20 | ; |
|
93 | 20 | ||
94 | 20 | $this->configureGlobalOptions(); |
|
95 | } |
||
96 | 20 | ||
97 | protected function initialize(InputInterface $input, OutputInterface $output) { |
||
98 | parent::initialize($input, $output); |
||
99 | 20 | } |
|
100 | 20 | ||
101 | protected function interact(InputInterface $input, OutputInterface $output) { |
||
|
|||
102 | 3 | $force = $input->getOption('force'); |
|
103 | 3 | $formatter = $this->getHelperSet()->get('formatter'); |
|
104 | 3 | $output->writeln([ |
|
105 | '', |
||
106 | $formatter->formatBlock('Welcome to the Keeko initializer', 'bg=blue;fg=white', true), |
||
107 | '' |
||
108 | ]); |
||
109 | $output->writeln([ |
||
110 | '', |
||
111 | 'This command will guide you through creating your Keeko composer package.', |
||
112 | '', |
||
113 | ]); |
||
114 | |||
115 | $name = $this->getPackageName(); |
||
116 | $askName = $name === null; |
||
117 | if ($name === null) { |
||
118 | $git = $this->getGitConfig(); |
||
119 | $cwd = realpath("."); |
||
120 | $name = basename($cwd); |
||
121 | $name = preg_replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $name); |
||
122 | $name = strtolower($name); |
||
123 | $localName = $this->package->getFullName(); |
||
124 | if (!empty($localName)) { |
||
125 | $name = $this->package->getFullName(); |
||
126 | } else if (isset($git['github.user'])) { |
||
127 | $name = $git['github.user'] . '/' . $name; |
||
128 | } elseif (!empty($_SERVER['USERNAME'])) { |
||
129 | $name = $_SERVER['USERNAME'] . '/' . $name; |
||
130 | } elseif (get_current_user()) { |
||
131 | $name = get_current_user() . '/' . $name; |
||
132 | } else { |
||
133 | // package names must be in the format foo/bar |
||
134 | $name = $name . '/' . $name; |
||
135 | } |
||
136 | } else { |
||
137 | $this->validateName($name); |
||
138 | } |
||
139 | |||
140 | // asking for the name |
||
141 | View Code Duplication | if ($askName || $force) { |
|
142 | $name = $this->askQuestion(new Question('Package name (<vendor>/<name>)', $name)); |
||
143 | $this->validateName($name); |
||
144 | $input->setOption('name', $name); |
||
145 | } |
||
146 | |||
147 | // asking for a description |
||
148 | $desc = $this->getPackageDescription(); |
||
149 | View Code Duplication | if ($desc === null || $force) { |
|
150 | $desc = $this->askQuestion(new Question('Description', $desc)); |
||
151 | $input->setOption('description', $desc); |
||
152 | } |
||
153 | |||
154 | // asking for the author |
||
155 | if ($this->package->getAuthors()->isEmpty() || $force) { |
||
156 | $author = $input->getOption('author'); |
||
157 | if ($author === null && isset($git['user.name'])) { |
||
158 | $author = $git['user.name']; |
||
159 | |||
160 | if (isset($git['user.email'])) { |
||
161 | $author = sprintf('%s <%s>', $git['user.name'], $git['user.email']); |
||
162 | } |
||
163 | } |
||
164 | |||
165 | $author = $this->askQuestion(new Question('Author', $author)); |
||
166 | $input->setOption('author', $author); |
||
167 | } |
||
168 | |||
169 | // asking for the package type |
||
170 | $type = $this->getPackageType(); |
||
171 | if ($type === null || $force) { |
||
172 | $types = ['module', 'app']; |
||
173 | $question = new Question('Package type (module|app)', $type); |
||
174 | $question->setAutocompleterValues($types); |
||
175 | $question->setValidator(function ($answer) use ($types) { |
||
176 | if (!in_array($answer, $types)) { |
||
177 | throw new \RuntimeException('The name of the type should be one of: ' . |
||
178 | implode(',', $types)); |
||
179 | } |
||
180 | return $answer; |
||
181 | }); |
||
182 | $question->setMaxAttempts(2); |
||
183 | $type = $this->askQuestion($question); |
||
184 | } |
||
185 | $input->setOption('type', $type); |
||
186 | |||
187 | // asking for the license |
||
188 | $license = $this->getPackageLicense(); |
||
189 | View Code Duplication | if ($license === null || $force) { |
|
190 | $license = $this->askQuestion(new Question('License', $license)); |
||
191 | $input->setOption('license', $license); |
||
192 | } |
||
193 | |||
194 | // asking for namespace |
||
195 | var_dump($this->hasAutoload()); |
||
196 | // if (!$this->hasAutoload() || $force) { |
||
197 | // $namespace = $input->getOption('namespace'); |
||
198 | // if ($namespace === null) { |
||
199 | // $namespace = str_replace('/', '\\', $name); |
||
200 | // } |
||
201 | // $namespace = $this->askQuestion(new Question('Namespace for src/', $namespace)); |
||
202 | // $input->setOption('namespace', $namespace); |
||
203 | // } |
||
204 | |||
205 | // |
||
206 | // KEEKO values |
||
207 | $output->writeln([ |
||
208 | '', |
||
209 | 'Information for Keeko ' . ucfirst($type), |
||
210 | '' |
||
211 | ]); |
||
212 | |||
213 | // ask for the title |
||
214 | $title = $this->getPackageTitle(); |
||
215 | View Code Duplication | if ($title === null || $force) { |
|
216 | $title = $this->askQuestion(new Question('Title', $title)); |
||
217 | $input->setOption('title', $title); |
||
218 | } |
||
219 | |||
220 | // ask for the class |
||
221 | $classname = $this->getPackageClass(); |
||
222 | if ($classname === null || $force) { |
||
223 | $classname = $this->askQuestion(new Question('Class', $classname)); |
||
224 | $input->setOption('classname', $classname); |
||
225 | } |
||
226 | |||
227 | // // -- module |
||
228 | // if ($type === 'module') { |
||
229 | // ask for the default action |
||
230 | // $defaultAction = $this->getPackageDefaultAction(); |
||
231 | // $defaultAction = $this->askQuestion(new Question('Default Action', $defaultAction)); |
||
232 | // $input->setOption('default-action', $defaultAction); |
||
233 | // } |
||
234 | } |
||
235 | |||
236 | protected function execute(InputInterface $input, OutputInterface $output) { |
||
237 | $this->generatePackage(); |
||
238 | 3 | $this->generateCode(); |
|
239 | 3 | } |
|
240 | 3 | ||
241 | 3 | private function generatePackage() { |
|
242 | $input = $this->io->getInput(); |
||
243 | 3 | $force = $input->getOption('force'); |
|
244 | 3 | ||
245 | // name |
||
246 | $localName = $this->package->getFullName(); |
||
247 | 3 | if (empty($localName) && $input->getOption('name') === null) { |
|
248 | 3 | throw new \RuntimeException('No name for the package given'); |
|
249 | } |
||
250 | |||
251 | if (($force || empty($localName)) && ($name = $input->getOption('name')) !== null) { |
||
252 | 3 | $this->validateName($name); |
|
253 | 3 | $this->package->setFullName($name); |
|
254 | 3 | } |
|
255 | 3 | ||
256 | // description |
||
257 | if (($desc = $input->getOption('description')) !== null) { |
||
258 | 3 | $this->package->setDescription($desc); |
|
259 | 3 | } |
|
260 | 3 | ||
261 | // type |
||
262 | if (($type = $input->getOption('type')) !== null) { |
||
263 | 3 | if (in_array($type, ['app', 'module'])) { |
|
264 | 3 | $this->package->setType('keeko-' . $type); |
|
265 | 3 | } |
|
266 | 3 | } |
|
267 | 3 | ||
268 | // license |
||
269 | if (($license = $input->getOption('license')) !== null) { |
||
270 | 3 | $this->package->setLicense($license); |
|
271 | 3 | } |
|
272 | 3 | ||
273 | // author |
||
274 | if (($author = $input->getOption('author')) !== null |
||
275 | 3 | && ($this->package->getAuthors()->isEmpty() || $force)) { |
|
276 | 3 | list($name, $email) = sscanf($author, '%s <%s>'); |
|
277 | 3 | ||
278 | $author = new AuthorSchema(); |
||
279 | 3 | $author->setName($name); |
|
280 | 3 | ||
281 | if (substr($email, -1) == '>') { |
||
282 | 3 | $email = substr($email, 0, -1); |
|
283 | } |
||
284 | $author->setEmail($email); |
||
285 | 3 | ||
286 | $this->package->getAuthors()->add($author); |
||
287 | 3 | } |
|
288 | 3 | ||
289 | // autoload |
||
290 | if (!$this->hasAutoload()) { |
||
291 | 3 | $namespace = $input->getOption('namespace'); |
|
292 | 3 | if ($namespace === null) { |
|
293 | 3 | $namespace = str_replace('/', '\\', $this->package->getFullName()); |
|
294 | 2 | } |
|
295 | 2 | if (substr($namespace, -2) !== '\\') { |
|
296 | 3 | $namespace .= '\\'; |
|
297 | 3 | } |
|
298 | 3 | $this->setAutoload($namespace); |
|
299 | 3 | } |
|
300 | 3 | ||
301 | $this->manageDependencies(); |
||
302 | 3 | ||
303 | // KEEKO |
||
304 | if ($type === null) { |
||
305 | $type = $this->getPackageType(); |
||
306 | } |
||
307 | 3 | ||
308 | 3 | // title |
|
309 | 3 | $keeko = $this->packageService->getKeeko()->getKeekoPackage($type); |
|
310 | 3 | if (($title = $this->getPackageTitle()) !== null) { |
|
311 | $keeko->setTitle($title); |
||
312 | } |
||
313 | 3 | ||
314 | 3 | // class |
|
315 | 3 | if (($classname = $this->getPackageClass()) !== null) { |
|
316 | $keeko->setClass($classname); |
||
317 | } |
||
318 | 3 | ||
319 | // // additions for keeko-module |
||
320 | // if ($keeko instanceof ModuleSchema) { |
||
321 | // // default-action |
||
322 | // // if (($defaultAction = $this->getPackageDefaultAction()) !== null) { |
||
323 | // // $keeko->setDefaultAction($defaultAction); |
||
324 | // // } |
||
325 | // } |
||
326 | |||
327 | $this->packageService->savePackage($this->package); |
||
328 | } |
||
329 | |||
330 | private function manageDependencies() { |
||
331 | // add require statements |
||
332 | 1 | $require = $this->package->getRequire(); |
|
333 | |||
334 | 3 | if (!$require->has('php')) { |
|
335 | 3 | $require->set('php', '>=5.4'); |
|
336 | } |
||
337 | 3 | ||
338 | if (!$require->has('keeko/composer-installer')) { |
||
339 | 3 | $require->set('keeko/composer-installer', '*'); |
|
340 | } |
||
341 | 3 | ||
342 | 3 | // add require dev statements |
|
343 | 3 | $requireDev = $this->package->getRequireDev(); |
|
344 | $requireDev->set('keeko/core', 'dev-master'); |
||
345 | 3 | $requireDev->set('composer/composer', '@dev'); |
|
346 | 3 | $requireDev->set('propel/propel', '@dev'); |
|
347 | 3 | $requireDev->set('puli/composer-plugin', '@beta'); |
|
348 | } |
||
349 | |||
350 | 3 | private function generateCode() { |
|
351 | 3 | $class = $this->generateClass(); |
|
352 | 3 | $type = $this->getPackageType(); |
|
353 | 3 | ||
354 | 3 | switch ($type) { |
|
355 | case 'app': |
||
356 | 3 | $this->handleAppClass($class); |
|
357 | 3 | break; |
|
358 | 3 | ||
359 | case 'module': |
||
360 | $this->handleModuleClass($class); |
||
361 | 3 | break; |
|
362 | 2 | } |
|
363 | 2 | ||
364 | $this->codegenService->dumpStruct($class, true); |
||
365 | 1 | } |
|
366 | 1 | ||
367 | 1 | private function generateClass() { |
|
368 | $input = $this->io->getInput(); |
||
369 | $type = $this->getPackageType(); |
||
370 | 3 | $package = $this->package->getKeeko()->getKeekoPackage($type); |
|
371 | 3 | $fqcn = str_replace('\\', '/', $package->getClass()); |
|
372 | $classname = basename($fqcn); |
||
373 | 3 | $filename = $this->project->getRootPath() . '/src/' . $classname . '.php'; |
|
374 | 3 | $fqcn = str_replace('/', '\\', $fqcn); |
|
375 | 3 | ||
376 | 3 | if (!file_exists($filename) || $input->getOption('force')) { |
|
377 | 3 | $class = PhpClass::create($fqcn); |
|
378 | 3 | $class->setDescription($package->getTitle()); |
|
379 | 3 | ||
380 | $docblock = $class->getDocblock(); |
||
381 | 3 | $docblock->appendTag(new LicenseTag($this->package->getLicense())); |
|
382 | 3 | $this->codegenService->addAuthors($class, $this->package); |
|
383 | 3 | } else { |
|
384 | $class = PhpClass::fromFile($filename); |
||
385 | 3 | } |
|
386 | 3 | ||
387 | 3 | return $class; |
|
388 | 3 | } |
|
389 | |||
390 | private function handleAppClass(PhpClass $class) { |
||
391 | // set parent |
||
392 | $class->setParentClassName('AbstractApplication'); |
||
393 | $class->addUseStatement('keeko\\core\\package\\AbstractApplication'); |
||
394 | 3 | ||
395 | // method: run(Request $request, $path) |
||
396 | View Code Duplication | if (!$class->hasMethod('run')) { |
|
397 | 2 | $class->setMethod(PhpMethod::create('run') |
|
398 | ->addParameter(PhpParameter::create('request')->setType('Request')) |
||
399 | 2 | ->addParameter(PhpParameter::create('path')->setType('string')) |
|
400 | 2 | ); |
|
401 | $class->addUseStatement('Symfony\\Component\\HttpFoundation\\Request'); |
||
402 | } |
||
403 | 2 | } |
|
404 | 2 | ||
405 | 2 | private function handleModuleClass(PhpClass $class) { |
|
406 | 2 | // set parent |
|
407 | 2 | $class->setParentClassName('AbstractModule'); |
|
408 | 2 | $class->addUseStatement('keeko\\core\\package\\AbstractModule'); |
|
409 | 2 | ||
410 | 2 | // method: install() |
|
411 | if (!$class->hasMethod('install')) { |
||
412 | 1 | $class->setMethod(PhpMethod::create('install')); |
|
413 | } |
||
414 | 1 | ||
415 | 1 | // method: uninstall() |
|
416 | if (!$class->hasMethod('uninstall')) { |
||
417 | $class->setMethod(PhpMethod::create('uninstall')); |
||
418 | 1 | } |
|
419 | 1 | ||
420 | 1 | // method: update($from, $to) |
|
421 | View Code Duplication | if (!$class->hasMethod('update')) { |
|
422 | $class->setMethod(PhpMethod::create('update') |
||
423 | 1 | ->addParameter(PhpParameter::create('from')->setType('mixed')) |
|
424 | 1 | ->addParameter(PhpParameter::create('to')->setType('mixed')) |
|
425 | 1 | ); |
|
426 | } |
||
427 | } |
||
428 | 1 | ||
429 | 1 | private function getPackageKeeko($type) { |
|
430 | 1 | $keeko = $this->package->getKeeko(); |
|
431 | 1 | $pkg = $keeko->getKeekoPackage($type); |
|
432 | 1 | ||
433 | 1 | if ($pkg == null) { |
|
434 | 1 | throw new \Exception(sprintf('Unknown package type <%s>', $type)); |
|
435 | } |
||
436 | 3 | ||
437 | 3 | return $pkg; |
|
438 | 3 | } |
|
439 | |||
440 | 3 | // private function getPackageSlug() { |
|
441 | // $type = $this->getPackageType(); |
||
442 | // if ($type !== 'module') { |
||
443 | // return; |
||
444 | 3 | // } |
|
445 | |||
446 | // $input = $this->io->getInput(); |
||
447 | // $keeko = $this->getPackageKeeko('module'); |
||
448 | // $pkgSlug = $keeko->getSlug(); |
||
449 | // $slug = $input->getOption('slug'); |
||
450 | // $slug = $slug === null && !empty($pkgSlug) ? $pkgSlug : $slug; |
||
451 | |||
452 | // // fallback to default value |
||
453 | // if ($slug === null) { |
||
454 | // $slug = str_replace('/', '.', $this->package->getFullName()); |
||
455 | // } |
||
456 | |||
457 | // return $slug; |
||
458 | // } |
||
459 | |||
460 | // private function getPackageDefaultAction() { |
||
461 | // $type = $this->getPackageType(); |
||
462 | // if ($type !== 'module') { |
||
463 | // return; |
||
464 | // } |
||
465 | |||
466 | // $input = $this->getInput(); |
||
467 | // $keeko = $this->getPackageKeeko('module'); |
||
468 | // $defaultAction = $input->getOption('default-action'); |
||
469 | // $defaultAction = $defaultAction === null && isset($keeko['default-action']) ? $keeko['default-action'] : $defaultAction; |
||
470 | |||
471 | // return $defaultAction; |
||
472 | // } |
||
473 | |||
474 | private function getPackageTitle() { |
||
475 | $input = $this->io->getInput(); |
||
476 | $type = $this->getPackageType(); |
||
477 | $keeko = $this->getPackageKeeko($type); |
||
478 | $pkgTitle = $keeko === null ? null : $keeko->getTitle(); |
||
479 | $title = $input->getOption('title'); |
||
480 | $title = $title === null && !empty($pkgTitle) ? $pkgTitle : $title; |
||
481 | 3 | ||
482 | 3 | // fallback to default value |
|
483 | 3 | if ($title === null) { |
|
484 | 3 | $title = ucwords(str_replace('/', ' ', $input->getOption('name'))); |
|
485 | 3 | } |
|
486 | 3 | ||
487 | 3 | return $title; |
|
488 | } |
||
489 | |||
490 | 3 | private function getPackageClass() { |
|
491 | $input = $this->io->getInput(); |
||
492 | $type = $this->getPackageType(); |
||
493 | $keeko = $this->getPackageKeeko($type); |
||
494 | 3 | $pkgClass = $keeko === null ? null : $keeko->getClass(); |
|
495 | $classname = $input->getOption('classname'); |
||
496 | $classname = $classname === null && !empty($pkgClass) ? $pkgClass : $classname; |
||
497 | 3 | ||
498 | 3 | // default value |
|
499 | 3 | if ($classname === null) { |
|
500 | 3 | $pkgName = $this->package->getFullName(); |
|
501 | 3 | $parts = explode('/', $pkgName); |
|
502 | 3 | $ns = $input->getOption('namespace'); |
|
503 | 3 | $namespace = !empty($ns) ? $ns : str_replace('/', '\\', $pkgName); |
|
504 | $classname = $namespace . '\\' . ucfirst($parts[1]); |
||
505 | |||
506 | 3 | // suffix |
|
507 | 3 | if ($type === 'module') { |
|
508 | 3 | $classname .= 'Module'; |
|
509 | 3 | } else if ($type === 'app') { |
|
510 | 3 | $classname .= 'Application'; |
|
511 | 3 | } |
|
512 | } |
||
513 | |||
514 | 3 | return $classname; |
|
515 | 1 | } |
|
516 | 3 | ||
517 | 2 | View Code Duplication | private function getPackageType() { |
518 | 2 | $input = $this->io->getInput(); |
|
519 | 3 | $type = $input->getOption('type'); |
|
520 | $pkgType = $this->package->getType(); |
||
521 | 3 | return $type === null && !empty($pkgType) |
|
522 | ? str_replace('keeko-', '', $pkgType) |
||
523 | : $type; |
||
524 | 3 | } |
|
525 | 3 | ||
526 | 3 | View Code Duplication | private function getPackageName() { |
527 | 3 | $input = $this->io->getInput(); |
|
528 | 3 | $name = $input->getOption('name'); |
|
529 | 3 | $pkgName = $this->package->getFullName(); |
|
530 | 3 | return $name === null && !empty($pkgName) ? $pkgName : $name; |
|
531 | } |
||
532 | |||
533 | View Code Duplication | private function getPackageDescription() { |
|
539 | |||
540 | View Code Duplication | private function getPackageLicense() { |
|
546 | |||
547 | 3 | private function hasAutoload() { |
|
548 | 3 | return NamespaceResolver::getNamespace('src', $this->package); |
|
549 | } |
||
550 | 3 | ||
551 | private function validateName($name) { |
||
558 | |||
559 | 3 | private function setAutoload($namespace) { |
|
569 | 3 | ||
570 | 3 | protected function getGitConfig() { |
|
589 | |||
590 | } |
||
591 |
Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable: