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:
1 | <?php |
||
77 | class PlayStory_Command extends BaseCommand implements CliSignalHandler |
||
78 | { |
||
79 | /** |
||
80 | * should we let background processes survive when we shutdown? |
||
81 | * @var boolean |
||
82 | */ |
||
83 | protected $persistProcesses = false; |
||
84 | |||
85 | // we need to track this for handling CTRL-C |
||
86 | protected $st; |
||
87 | |||
88 | // we track this for convenience |
||
89 | protected $output; |
||
90 | |||
91 | // our list of players to execute |
||
92 | protected $playerList; |
||
93 | |||
94 | // our injected data / services |
||
95 | // needed for when user presses CTRL+C |
||
96 | protected $injectables; |
||
97 | |||
98 | /** |
||
99 | * the environment that we have loaded |
||
100 | * |
||
101 | * @var string |
||
102 | */ |
||
103 | protected $envName; |
||
104 | |||
105 | public function __construct($injectables) |
||
106 | { |
||
107 | // call our parent |
||
108 | parent::__construct($injectables); |
||
109 | |||
110 | // define the command |
||
111 | $this->setName('play-story'); |
||
112 | $this->setShortDescription('play a story, or a list of stories'); |
||
113 | $this->setLongDescription( |
||
114 | "Use this command to play a single story, or a list of stories defined in a JSON file." |
||
115 | .PHP_EOL |
||
116 | ); |
||
117 | $this->setArgsList(array( |
||
118 | "[<story.php|list.json>]" => "run a story, or a list of stories" |
||
119 | )); |
||
120 | |||
121 | // the switches that this command supports |
||
122 | $this->setSwitches(array( |
||
123 | new PlayStory_LogJsonSwitch(), |
||
124 | new PlayStory_LogJUnitSwitch(), |
||
125 | new PlayStory_LogTapSwitch(), |
||
126 | )); |
||
127 | |||
128 | // add in the features that this command relies on |
||
129 | $this->addFeature(new Feature_ConsoleSupport); |
||
130 | $this->addFeature(new Feature_VerboseSupport); |
||
131 | $this->addFeature(new Feature_ColorSupport); |
||
132 | $this->addFeature(new Feature_DeviceSupport); |
||
133 | $this->addFeature(new Feature_TestEnvironmentConfigSupport); |
||
134 | $this->addFeature(new Feature_SystemUnderTestConfigSupport); |
||
135 | $this->addFeature(new Feature_LocalhostSupport); |
||
136 | $this->addFeature(new Feature_ActiveConfigSupport); |
||
137 | $this->addFeature(new Feature_DefinesSupport); |
||
138 | $this->addFeature(new Feature_PhaseLoaderSupport); |
||
139 | $this->addFeature(new Feature_ProseLoaderSupport); |
||
140 | $this->addFeature(new Feature_PersistReuseTargetSupport); |
||
141 | $this->addFeature(new Feature_PersistDeviceSupport); |
||
142 | $this->addFeature(new Feature_PersistProcessesSupport); |
||
143 | $this->addFeature(new Feature_TestUsersSupport); |
||
144 | $this->addFeature(new Feature_WarnDeprecatedSupport); |
||
145 | $this->addFeature(new Feature_LogInternalEventsSupport); |
||
146 | |||
147 | // now setup all of the switches that we support |
||
148 | $this->addFeatureSwitches(); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * |
||
153 | * @param CliEngine $engine |
||
154 | * @param array $params |
||
155 | * @param Injectables|null $injectables |
||
156 | * @return integer |
||
157 | */ |
||
158 | View Code Duplication | public function processCommand(CliEngine $engine, $params = array(), $injectables = null) |
|
159 | { |
||
160 | // we need to wrap our code to catch old-style PHP errors |
||
161 | $legacyHandler = new Legacy_ErrorHandler(); |
||
162 | |||
163 | // run our code |
||
164 | try { |
||
165 | $returnCode = $legacyHandler->run([$this, 'processInsideLegacyHandler'], [$engine, $params, $injectables]); |
||
166 | return $returnCode; |
||
167 | } |
||
168 | catch (Exception $e) { |
||
169 | $injectables->output->logCliError($e->getMessage()); |
||
170 | $engine->options->dev = true; |
||
171 | if (isset($engine->options->dev) && $engine->options->dev) { |
||
172 | $injectables->output->logCliError("Stack trace is:\n\n" . $e->getTraceAsString()); |
||
173 | } |
||
174 | |||
175 | // stop the browser if available |
||
176 | if (isset($this->st)) { |
||
177 | $this->st->stopDevice(); |
||
178 | } |
||
179 | |||
180 | // tell the calling process that things did not end well |
||
181 | exit(1); |
||
|
|||
182 | } |
||
183 | } |
||
184 | |||
185 | public function processInsideLegacyHandler(CliEngine $engine, $params = array(), $injectables = null) |
||
186 | { |
||
187 | // the order we do things: |
||
188 | // |
||
189 | // 1. build up the config we're going to use |
||
190 | // a. storyplayer.json (already done) |
||
191 | // c. any additional config file |
||
192 | // c. test-environment config file |
||
193 | // d. per-device config file |
||
194 | // |
||
195 | // 2. override from the command-line |
||
196 | // a. -D switches |
||
197 | // b. persistent processes |
||
198 | // |
||
199 | // 3. build up the list of stories to run |
||
200 | // a. test environment setup |
||
201 | // b. one or more stories |
||
202 | // c. test environment teardown |
||
203 | // |
||
204 | // 4. setup any remaining services |
||
205 | // a. phase loading |
||
206 | // b. prose loading |
||
207 | // c. report loader |
||
208 | // |
||
209 | // 5. setup the output channels |
||
210 | // a. the console (i.e. --dev mode) |
||
211 | // b. report-to-file plugins |
||
212 | |||
213 | // process the common functionality |
||
214 | $this->initFeaturesBeforeModulesAvailable($engine); |
||
215 | |||
216 | // now it is safe to create our shorthand |
||
217 | $runtimeConfig = $injectables->getRuntimeConfig(); |
||
218 | $runtimeConfigManager = $injectables->getRuntimeConfigManager(); |
||
219 | $output = $injectables->output; |
||
220 | |||
221 | // save the output for use in other methods |
||
222 | $this->output = $output; |
||
223 | |||
224 | // setup reporting modules |
||
225 | $this->initReporting($engine, $injectables); |
||
226 | |||
227 | // at this point, all of the services / data held in $injectables |
||
228 | // has been initialised and is ready for use |
||
229 | // |
||
230 | // what's left is the stuff that needs initialising in phases |
||
231 | // or $st |
||
232 | |||
233 | // create a new StoryTeller object |
||
234 | $st = new StoryTeller($injectables); |
||
235 | |||
236 | // remember our $st object, as we'll need it for our |
||
237 | // shutdown function |
||
238 | $this->st = $st; |
||
239 | |||
240 | // now that we have $st, we can initialise any feature that |
||
241 | // wants to use our modules |
||
242 | $this->initFeaturesAfterModulesAvailable($st, $engine, $injectables); |
||
243 | |||
244 | // install signal handling, now that $this->st is defined |
||
245 | // |
||
246 | // we wouldn't want signal handling called out of order :) |
||
247 | $this->initSignalHandling($injectables); |
||
248 | |||
249 | // build our list of players to run |
||
250 | $this->initPlayerList($engine, $injectables, $params); |
||
251 | |||
252 | // let's keep score :) |
||
253 | $startTime = microtime(true); |
||
254 | |||
255 | // and we're ready to tell the world that we're here |
||
256 | $output->startStoryplayer( |
||
257 | $engine->getAppVersion(), |
||
258 | $engine->getAppUrl(), |
||
259 | $engine->getAppCopyright(), |
||
260 | $engine->getAppLicense() |
||
261 | ); |
||
262 | |||
263 | // $this->playerList contains one or more things to play |
||
264 | // |
||
265 | // let's play each of them in order |
||
266 | foreach ($this->playerList as $player) |
||
267 | { |
||
268 | // execute each player in turn |
||
269 | // |
||
270 | // they may also have their own list of nested players |
||
271 | $player->play($st, $injectables); |
||
272 | |||
273 | // make sure the test device has been stopped |
||
274 | // (it may have been persisted by the story) |
||
275 | // |
||
276 | // we do not allow the test device to persist between |
||
277 | // top-level players |
||
278 | $st->stopDevice(); |
||
279 | } |
||
280 | |||
281 | // write out any changed runtime config to disk |
||
282 | $runtimeConfigManager->saveRuntimeConfig($runtimeConfig, $output); |
||
283 | |||
284 | // how long did that take? |
||
285 | $duration = microtime(true) - $startTime; |
||
286 | |||
287 | // tell the output plugins that we're all done |
||
288 | $retval = $output->endStoryplayer($duration); |
||
289 | |||
290 | // all done |
||
291 | return $retval; |
||
292 | } |
||
293 | |||
294 | // ================================================================== |
||
295 | // |
||
296 | // the individual initX() methods |
||
297 | // |
||
298 | // these are processed *after* the objects defined in the |
||
299 | // CommonFunctionalitySupport trait have been initialised |
||
300 | // |
||
301 | // ------------------------------------------------------------------ |
||
302 | |||
303 | /** |
||
304 | * |
||
305 | * @param CliEngine $engine |
||
306 | * @param Injectables $injectables |
||
307 | * @return void |
||
308 | */ |
||
309 | protected function initReporting(CliEngine $engine, Injectables $injectables) |
||
310 | { |
||
311 | // are there any reporting modules to be loaded? |
||
312 | if (!isset($engine->options->reports)) { |
||
313 | // no |
||
314 | return; |
||
315 | } |
||
316 | |||
317 | // setup the reports that have been requested |
||
318 | $injectables->initReportLoaderSupport($injectables); |
||
319 | foreach ($engine->options->reports as $reportName => $reportFilename) |
||
320 | { |
||
321 | try { |
||
322 | $report = $injectables->reportLoader->loadReport($reportName, [ 'filename' => $reportFilename]); |
||
323 | } |
||
324 | catch (E4xx_NoSuchReport $e) { |
||
325 | $injectables->output->logCliError("no such report '{$reportName}'"); |
||
326 | exit(1); |
||
327 | } |
||
328 | $injectables->output->usePluginInSlot($report, $reportName); |
||
329 | } |
||
330 | |||
331 | // all done |
||
332 | } |
||
333 | |||
334 | /** |
||
335 | * |
||
336 | * @param Injectables $injectables |
||
337 | * @return void |
||
338 | */ |
||
339 | View Code Duplication | protected function initSignalHandling(Injectables $injectables) |
|
340 | { |
||
341 | // we need to remember the injectables, for when we handle CTRL+C |
||
342 | $this->injectables = $injectables; |
||
343 | |||
344 | // setup signal handling |
||
345 | pcntl_signal(SIGTERM, array($this, 'sigtermHandler')); |
||
346 | pcntl_signal(SIGINT , array($this, 'sigtermHandler')); |
||
347 | } |
||
348 | |||
349 | /** |
||
350 | * |
||
351 | * @param CliEngine $cliEngine |
||
352 | * @param Injectables $injectables |
||
353 | * @param array $cliParams |
||
354 | * @return void |
||
355 | */ |
||
356 | protected function initPlayerList(CliEngine $cliEngine, Injectables $injectables, $cliParams) |
||
357 | { |
||
358 | // our list of stories to play |
||
359 | $this->playerList = []; |
||
360 | |||
361 | // do we have any parameters at this point? |
||
362 | if (empty($cliParams)) { |
||
363 | $msg = "no stories listed on the command-line." . PHP_EOL . PHP_EOL |
||
364 | . "see 'storyplayer help play-story' for required params" . PHP_EOL; |
||
365 | $this->output->logCliError($msg); |
||
366 | exit(1); |
||
367 | } |
||
368 | |||
369 | // keep track of the stories to play |
||
370 | $storiesToPlay = []; |
||
371 | |||
372 | foreach ($cliParams as $cliParam) { |
||
373 | // figure out what to do? |
||
374 | if (is_dir($cliParam)) { |
||
375 | $storiesToPlay = array_merge($storiesToPlay, $this->addStoriesFromFolder($cliEngine, $injectables, $cliParam)); |
||
376 | } |
||
377 | else if (is_file($cliParam)) { |
||
378 | // are we loading a story, or a list of stories? |
||
379 | $paramParts = explode('.', $cliParams[0]); |
||
380 | $paramSuffix = end($paramParts); |
||
381 | |||
382 | switch ($paramSuffix) { |
||
383 | case 'php': |
||
384 | $storiesToPlay = array_merge($storiesToPlay, $this->addStoryFromFile($cliEngine, $injectables, $cliParam)); |
||
385 | break; |
||
386 | |||
387 | default: |
||
388 | $this->output->logCliError("unsupported story file '{$cliParam}'"); |
||
389 | exit(1); |
||
390 | } |
||
391 | } |
||
392 | else { |
||
393 | // if we get here, we've no idea what to do |
||
394 | $this->output->logCliError("no such file: '{$cliParam}'"); |
||
395 | exit(1); |
||
396 | } |
||
397 | } |
||
398 | |||
399 | // did we find any stories to play? |
||
400 | if (count($storiesToPlay) == 0) { |
||
401 | $this->output->logCliError("no stories to play :("); |
||
402 | exit(1); |
||
403 | } |
||
404 | |||
405 | // wrap all of the stories in a TestEnvironment |
||
406 | $this->playerList[] = new TestEnvironment_Player($storiesToPlay, $injectables); |
||
407 | } |
||
408 | |||
409 | // ================================================================== |
||
410 | // |
||
411 | // Story loading |
||
412 | // |
||
413 | // ------------------------------------------------------------------ |
||
414 | |||
415 | protected function addStoryFromFile(CliEngine $engine, Injectables $injectables, $storyFile) |
||
416 | { |
||
417 | // warn the user if the story file doesn't end in 'Story.php' |
||
418 | // |
||
419 | // this is because Storyplayer will ignore the file if you |
||
420 | // point Storyplayer at a folder instead of a specific file |
||
421 | if (substr($storyFile, -9) != 'Story.php') { |
||
422 | $msg = "your story should end in 'Story.php', but it does not" . PHP_EOL; |
||
423 | $this->output->logCliWarning($msg); |
||
424 | } |
||
425 | |||
426 | // these are the players we want to execute for the story |
||
427 | $return = [ |
||
428 | new Story_Player($storyFile, $injectables), |
||
429 | ]; |
||
430 | |||
431 | // all done |
||
432 | return $return; |
||
433 | } |
||
434 | |||
435 | protected function addStoriesFromFolder(CliEngine $engine, Injectables $injectables, $folder) |
||
457 | |||
458 | protected function findStoriesInFolder($folder) |
||
459 | { |
||
460 | // use the SPL to do the heavy lifting |
||
461 | $dirIter = new RecursiveDirectoryIterator($folder); |
||
462 | $recIter = new RecursiveIteratorIterator($dirIter); |
||
463 | $regIter = new RegexIterator($recIter, '/^.+Story\.php$/i', RegexIterator::GET_MATCH); |
||
477 | |||
478 | // ================================================================== |
||
479 | // |
||
480 | // SIGNAL handling |
||
481 | // |
||
482 | // ------------------------------------------------------------------ |
||
483 | |||
484 | /** |
||
485 | * |
||
486 | * @param integer $signo |
||
487 | * @return void |
||
488 | */ |
||
489 | View Code Duplication | public function sigtermHandler($signo) |
|
518 | |||
519 | // ================================================================== |
||
520 | // |
||
521 | // legacy code goes here |
||
522 | // |
||
523 | // everything below here is old code that needs stripping out |
||
524 | // before we release v1.6 |
||
525 | // |
||
526 | // ------------------------------------------------------------------ |
||
527 | |||
528 | protected function summariseStoryList($storyResults) |
||
542 | |||
543 | } |
||
544 |
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exit
expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.