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 SetupCommand 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 SetupCommand, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
16 | class SetupCommand extends Command |
||
17 | { |
||
18 | private $templating; |
||
19 | private $rootDir = null; |
||
20 | |||
21 | 30 | public function __construct($name = null) |
|
22 | { |
||
23 | 30 | parent::__construct($name); |
|
24 | |||
25 | 30 | $this->templating = new PhpEngine( |
|
26 | 30 | new TemplateNameParser(), |
|
27 | 30 | new FilesystemLoader(array(__DIR__.'/../Resources/blueprints/%name%')) |
|
28 | ); |
||
29 | 30 | } |
|
30 | |||
31 | 30 | public function setRootDir($path) |
|
35 | |||
36 | 30 | protected function configure() |
|
37 | { |
||
38 | $this |
||
39 | 30 | ->setName('rj_frontend:setup') |
|
40 | 30 | ->setDescription('Generate the configuration for the asset pipeline') |
|
41 | 30 | ->addOption( |
|
42 | 30 | 'dry-run', |
|
43 | 30 | null, |
|
44 | 30 | InputOption::VALUE_NONE, |
|
45 | 30 | 'Output which commands would have been run instead of running them' |
|
46 | ) |
||
47 | 30 | ->addOption( |
|
48 | 30 | 'force', |
|
49 | 30 | null, |
|
50 | 30 | InputOption::VALUE_NONE, |
|
51 | 30 | 'Force execution' |
|
52 | ) |
||
53 | 30 | ->addOption( |
|
54 | 30 | 'src-dir', |
|
55 | 30 | null, |
|
56 | 30 | InputOption::VALUE_REQUIRED, |
|
57 | 30 | 'Path to the directory containing the source assets [e.g. '.$this->getDefaultOption('src-dir').']' |
|
58 | ) |
||
59 | 30 | ->addOption( |
|
60 | 30 | 'dest-dir', |
|
61 | 30 | null, |
|
62 | 30 | InputOption::VALUE_REQUIRED, |
|
63 | 30 | 'Path to the directory containing the compiled assets [e.g. '.$this->getDefaultOption('dest-dir').']' |
|
64 | ) |
||
65 | 30 | ->addOption( |
|
66 | 30 | 'pipeline', |
|
67 | 30 | null, |
|
68 | 30 | InputOption::VALUE_REQUIRED, |
|
69 | 30 | 'Asset pipeline to use [only gulp is available at the moment]' |
|
70 | ) |
||
71 | 30 | ->addOption( |
|
72 | 30 | 'csspre', |
|
73 | 30 | null, |
|
74 | 30 | InputOption::VALUE_REQUIRED, |
|
75 | 30 | 'CSS preprocessor to use [sass, less or none]' |
|
76 | ) |
||
77 | 30 | ->addOption( |
|
78 | 30 | 'coffee', |
|
79 | 30 | null, |
|
80 | 30 | InputOption::VALUE_REQUIRED, |
|
81 | 30 | 'Use the CoffeeScript compiler [true or false]' |
|
82 | ) |
||
83 | ; |
||
84 | 30 | } |
|
85 | |||
86 | 4 | protected function interact(InputInterface $input, OutputInterface $output) |
|
87 | { |
||
88 | 4 | $simpleOptionHelper = new SimpleOptionHelper($this, $input, $output); |
|
89 | 4 | $choiceOptionHelper = new ChoiceOptionHelper($this, $input, $output); |
|
90 | |||
91 | $simpleOptionHelper |
||
92 | 4 | ->setDefaultValue($this->getDefaultOption('src-dir')) |
|
93 | 4 | ->setOption( |
|
94 | 4 | 'src-dir', |
|
95 | 4 | 'Path to the directory containing the source assets [default is '.$this->getDefaultOption('src-dir').']' |
|
96 | ) |
||
97 | ; |
||
98 | |||
99 | $simpleOptionHelper |
||
100 | 4 | ->setDefaultValue($this->getDefaultOption('dest-dir')) |
|
101 | 4 | ->setOption( |
|
102 | 4 | 'dest-dir', |
|
103 | 4 | 'Path to the directory containing the compiled assets [default is '.$this->getDefaultOption('dest-dir').']' |
|
104 | ) |
||
105 | ; |
||
106 | |||
107 | $choiceOptionHelper |
||
108 | 4 | ->setAllowedValues(array('gulp')) |
|
109 | 4 | ->setErrorMessage('%s is not a supported asset pipeline') |
|
110 | 4 | ->setOption( |
|
111 | 4 | 'pipeline', |
|
112 | 4 | 'Asset pipeline to use [only gulp is available at the moment]' |
|
113 | ) |
||
114 | ; |
||
115 | |||
116 | $choiceOptionHelper |
||
117 | 4 | ->setAllowedValues(array('sass', 'less', 'none')) |
|
118 | 4 | ->setErrorMessage('%s is not a supported CSS preprocessor') |
|
119 | 4 | ->setOption( |
|
120 | 4 | 'csspre', |
|
121 | 4 | 'CSS preprocessor to use [default is '.$this->getDefaultOption('csspre').']' |
|
122 | ) |
||
123 | ; |
||
124 | |||
125 | $choiceOptionHelper |
||
126 | 4 | ->setAllowedValues(array('false', 'true')) |
|
127 | 4 | ->setErrorMessage('%s is not a supported value for --coffee. Use either true or false') |
|
128 | 4 | ->setOption( |
|
129 | 4 | 'coffee', |
|
130 | 4 | 'Whether to use the CoffeeScript compiler [default is '.$this->getDefaultOption('coffee').']' |
|
131 | ) |
||
132 | ; |
||
133 | |||
134 | 4 | $output->writeln(''); |
|
135 | 4 | } |
|
136 | |||
137 | 30 | protected function execute(InputInterface $input, OutputInterface $output) |
|
138 | { |
||
139 | 30 | $this->processOptions($input); |
|
140 | |||
141 | 30 | $output->writeln('<info>Selected options are:</info>'); |
|
142 | 30 | $output->writeln('src-dir: '.$input->getOption('src-dir')); |
|
143 | 30 | $output->writeln('dest-dir: '.$input->getOption('dest-dir')); |
|
144 | 30 | $output->writeln('pipeline: '.$input->getOption('pipeline')); |
|
145 | 30 | $output->writeln('csspre: '.$input->getOption('csspre')); |
|
146 | 30 | $output->writeln('coffee: '.($input->getOption('coffee') ? 'true' : 'false')); |
|
147 | |||
148 | 30 | if (!preg_match('|web/.+|', $input->getOption('dest-dir'))) { |
|
149 | 6 | throw new \InvalidArgumentException("'dest-dir' must be a directory under web/"); |
|
150 | } |
||
151 | |||
152 | 24 | $output->writeln(''); |
|
153 | 24 | $this->createSourceTree($input, $output); |
|
154 | 24 | $this->createBuildFile($input, $output); |
|
155 | 24 | $this->createPackageJson($input, $output); |
|
156 | 24 | $this->createBowerJson($input, $output); |
|
157 | |||
158 | 24 | $output->writeln(''); |
|
159 | 24 | $this->runInstallCommand($input, $output); |
|
160 | 24 | } |
|
161 | |||
162 | 24 | private function runInstallCommand($input, $output) |
|
163 | { |
||
164 | 24 | if ($input->getOption('dry-run')) { |
|
165 | 12 | return $output->writeln('<info>Would have installed npm and bower dependencies</info>'); |
|
166 | } |
||
167 | |||
168 | 12 | $this->getApplication()->find('rj_frontend:install') |
|
169 | 12 | ->run(new ArrayInput(array('command' => 'rj_frontend:install')), $output); |
|
170 | 12 | } |
|
171 | |||
172 | 24 | private function createSourceTree($input, $output) |
|
173 | { |
||
174 | 24 | $blueprints = __DIR__.'/../Resources/blueprints'; |
|
175 | 24 | $dryRun = $input->getOption('dry-run'); |
|
176 | 24 | $base = $input->getOption('src-dir'); |
|
177 | |||
178 | 24 | $output->writeln($dryRun |
|
179 | 12 | ? '<info>Would have created directory tree for source assets:</info>' |
|
180 | 24 | : '<info>Creating directory tree for source assets:</info>' |
|
181 | ); |
||
182 | |||
183 | 24 | $blueprintDir = "$blueprints/images"; |
|
184 | 24 | $this->createDirFromBlueprint($input, $output, $blueprintDir, "$base/images"); |
|
185 | |||
186 | 24 | $blueprintDir = "$blueprints/stylesheets/".$input->getOption('csspre'); |
|
187 | 24 | $this->createDirFromBlueprint($input, $output, $blueprintDir, "$base/stylesheets"); |
|
188 | |||
189 | 24 | $blueprintDir = "$blueprints/scripts/"; |
|
190 | 24 | $blueprintDir .= $input->getOption('coffee') ? 'coffee' : 'js'; |
|
191 | 24 | $this->createDirFromBlueprint($input, $output, $blueprintDir, "$base/scripts"); |
|
192 | |||
193 | 24 | $output->writeln(''); |
|
194 | 24 | } |
|
195 | |||
196 | 24 | View Code Duplication | private function createBuildFile($input, $output) |
|
|||
197 | { |
||
198 | $files = array( |
||
199 | 24 | 'gulp' => 'gulp/gulpfile.js', |
|
200 | ); |
||
201 | |||
202 | 24 | $this->createFileFromTemplate($input, $output, 'pipelines/'.$files[$input->getOption('pipeline')]); |
|
203 | 24 | } |
|
204 | |||
205 | 24 | View Code Duplication | private function createPackageJson($input, $output) |
206 | { |
||
207 | $files = array( |
||
208 | 24 | 'gulp' => 'gulp/package.json', |
|
209 | ); |
||
210 | |||
211 | 24 | $this->createFileFromTemplate($input, $output, 'pipelines/'.$files[$input->getOption('pipeline')]); |
|
212 | 24 | } |
|
213 | |||
214 | 24 | private function createBowerJson($input, $output) |
|
218 | |||
219 | 24 | private function createDirFromBlueprint($input, $output, $blueprintDir, $targetDir) |
|
220 | { |
||
221 | 24 | $dryRun = $input->getOption('dry-run'); |
|
222 | |||
223 | 24 | if (!$dryRun && !file_exists($targetDir)) { |
|
224 | 12 | mkdir($targetDir, 0777, true); |
|
225 | } |
||
226 | |||
227 | 24 | foreach (preg_grep('/^\.?\w+/', scandir($blueprintDir)) as $entry) { |
|
228 | 24 | $target = $entry; |
|
229 | |||
230 | 24 | $isPhpTemplate = substr($entry, strrpos($entry, '.')) === '.php'; |
|
231 | 24 | if ($isPhpTemplate) { |
|
232 | 24 | $entry = str_replace('.php', '', $entry); |
|
233 | 24 | $target = str_replace('.php', '', $target); |
|
234 | } |
||
235 | |||
236 | 24 | $entry = $blueprintDir.'/'.$entry; |
|
237 | 24 | $target = $targetDir.'/'.$target; |
|
238 | |||
239 | 24 | if (!$dryRun) { |
|
240 | 12 | if ($isPhpTemplate) { |
|
241 | 12 | $this->renderTemplate($input, $output, $entry, $target); |
|
242 | } else { |
||
243 | 12 | if (file_exists($target) && !$input->getOption('force')) { |
|
244 | $output->writeln( |
||
245 | "<error>$target already exists. Run this command with --force to overwrite</error> |
||
246 | "); |
||
247 | |||
248 | continue; |
||
249 | } |
||
250 | |||
251 | 12 | copy($entry, $target); |
|
252 | } |
||
253 | } |
||
254 | |||
255 | 24 | $output->writeln($target); |
|
256 | } |
||
257 | 24 | } |
|
258 | |||
259 | 24 | private function createFileFromTemplate($input, $output, $file) |
|
279 | |||
280 | 12 | private function renderTemplate($input, $output, $file, $target) |
|
281 | { |
||
282 | 12 | if (file_exists($target) && !$input->getOption('force')) { |
|
283 | 2 | return $output->writeln( |
|
284 | "<error>$target already exists. Run this command with --force to overwrite</error> |
||
285 | 2 | "); |
|
286 | } |
||
287 | |||
288 | 12 | switch ($input->getOption('csspre')) { |
|
310 | |||
311 | 30 | private function processOptions($input) |
|
327 | |||
328 | 30 | private function getDefaultOption($name) |
|
340 | } |
||
341 |
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.