1
|
|
|
<?php |
2
|
|
|
namespace Nkey\Caribu\Mvc; |
3
|
|
|
|
4
|
|
|
use Generics\Socket\InvalidUrlException; |
5
|
|
|
use \Generics\GenericsException; |
6
|
|
|
use \Nkey\Caribu\Mvc\Controller\AbstractController; |
7
|
|
|
use \Nkey\Caribu\Mvc\Controller\ControllerException; |
8
|
|
|
use \Nkey\Caribu\Mvc\Controller\Request; |
9
|
|
|
use \Nkey\Caribu\Mvc\View\ViewException; |
10
|
|
|
use \Nkey\Caribu\Mvc\View\View; |
11
|
|
|
use \Psr\Log\LoggerAwareInterface; |
12
|
|
|
use \Psr\Log\LoggerAwareTrait; |
13
|
|
|
use \Psr\Log\NullLogger; |
14
|
|
|
use Nkey\Caribu\Mvc\Router\AbstractRouter; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* The MVC Application main class |
18
|
|
|
* |
19
|
|
|
* This class provides all functions to route the request to |
20
|
|
|
* responsible controller and action. If no controller/action |
21
|
|
|
* could be found to match the request, the error controller |
22
|
|
|
* will be triggered. |
23
|
|
|
* |
24
|
|
|
* To work correctly all available controllers and views must |
25
|
|
|
* be registered before the request can be routed. |
26
|
|
|
* |
27
|
|
|
* The routing and rendering process will be performed by |
28
|
|
|
* calling the Application::serve() function |
29
|
|
|
* |
30
|
|
|
* @author Maik Greubel <[email protected]> |
31
|
|
|
* |
32
|
|
|
* This file is part of Caribu MVC package |
33
|
|
|
*/ |
34
|
|
|
final class Application implements LoggerAwareInterface |
35
|
|
|
{ |
36
|
|
|
use LoggerAwareTrait; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Retrieve the logging instance |
40
|
|
|
* |
41
|
|
|
* @return \Psr\Log\LoggerInterface The logging instance |
42
|
|
|
*/ |
43
|
20 |
|
private function getLogger() |
44
|
|
|
{ |
45
|
20 |
|
return $this->logger; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* List of controllers |
50
|
|
|
* |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
private $controllers = null; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* List of views |
57
|
|
|
* |
58
|
|
|
* @var array |
59
|
|
|
*/ |
60
|
|
|
private $views = null; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* List of view controls |
64
|
|
|
* |
65
|
|
|
* @var array |
66
|
|
|
*/ |
67
|
|
|
private $viewControls = null; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* The default controller |
71
|
|
|
* |
72
|
|
|
* @var string |
73
|
|
|
*/ |
74
|
|
|
private $defaultController = 'Index'; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* The default action |
78
|
|
|
* |
79
|
|
|
* @var string |
80
|
|
|
*/ |
81
|
|
|
private $defaultAction = 'index'; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Singleton instance |
85
|
|
|
* |
86
|
|
|
* @var \Nkey\Caribu\Mvc\Application |
87
|
|
|
*/ |
88
|
|
|
private static $instance = null; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Default headers to send to client |
92
|
|
|
* |
93
|
|
|
* @var array |
94
|
|
|
*/ |
95
|
|
|
private $defaultHeaders = array(); |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* The client request headers to override |
99
|
|
|
* |
100
|
|
|
* @var array |
101
|
|
|
*/ |
102
|
|
|
private $overridenClientHeaders = array(); |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Additional css files to include in view |
106
|
|
|
* |
107
|
|
|
* @var array |
108
|
|
|
*/ |
109
|
|
|
private $cssFiles = array(); |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Additional javascript files to include in view |
113
|
|
|
* |
114
|
|
|
* @var array |
115
|
|
|
*/ |
116
|
|
|
private $jsFiles = array(); |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Router object |
120
|
|
|
* @var AbstractRouter |
121
|
|
|
*/ |
122
|
|
|
private $router; |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Get application instance |
126
|
|
|
* |
127
|
|
|
* @return \Nkey\Caribu\Mvc\Application |
128
|
|
|
*/ |
129
|
28 |
|
public static function getInstance() |
130
|
|
|
{ |
131
|
28 |
|
if (null === self::$instance) { |
132
|
1 |
|
self::$instance = new self(); |
133
|
|
|
} |
134
|
28 |
|
return self::$instance; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Singleton constructor |
139
|
|
|
*/ |
140
|
1 |
|
private function __construct() |
141
|
|
|
{ |
142
|
1 |
|
$this->setUp(); |
143
|
1 |
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Set up the default values |
147
|
|
|
* |
148
|
|
|
* @return Application Current application instance |
149
|
|
|
*/ |
150
|
28 |
|
public function setUp() |
151
|
|
|
{ |
152
|
28 |
|
$this->controllers = array(); |
153
|
28 |
|
$this->views = array(); |
154
|
28 |
|
$this->viewControls = array(); |
155
|
28 |
|
$this->setDefaults(); |
156
|
28 |
|
$this->init(); |
157
|
28 |
|
$this->setLogger(new NullLogger()); |
158
|
|
|
|
159
|
28 |
|
return $this; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Singleton instance |
164
|
|
|
*/ |
165
|
1 |
|
public function __clone() |
166
|
|
|
{ |
167
|
1 |
|
throw new GenericsException("Cloning is prohibited"); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Init the application |
172
|
|
|
* |
173
|
|
|
* Register internally needed controller and view |
174
|
|
|
*/ |
175
|
28 |
|
public function init() |
176
|
|
|
{ |
177
|
28 |
|
$this->registerController(\Nkey\Caribu\Mvc\Controller\ErrorController::class); |
178
|
28 |
|
$this->registerView(\Nkey\Caribu\Mvc\View\DefaultView::class); |
179
|
28 |
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Set the default controller and action |
183
|
|
|
* |
184
|
|
|
* @param string $defaultController |
185
|
|
|
* The default controller name if nothing is provided by request |
186
|
|
|
* @param string $defaultAction |
187
|
|
|
* The default action name if nothing is provided by request |
188
|
|
|
* @return Application Current application instance |
189
|
|
|
*/ |
190
|
28 |
|
public function setDefaults($defaultController = 'Index', $defaultAction = 'index') |
191
|
|
|
{ |
192
|
28 |
|
$this->defaultController = $defaultController; |
193
|
28 |
|
$this->defaultAction = $defaultAction; |
194
|
|
|
|
195
|
28 |
|
return $this; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* Register a new view |
200
|
|
|
* |
201
|
|
|
* @param string $view |
202
|
|
|
* The view class |
203
|
|
|
* @param int $order |
204
|
|
|
* Override the default order given by view class |
205
|
|
|
* @param string $applicationName |
206
|
|
|
* The application name where the view will be available in |
207
|
|
|
* |
208
|
|
|
* @throws ViewException |
209
|
|
|
* |
210
|
|
|
* @return Application Current application instance |
211
|
|
|
*/ |
212
|
28 |
|
public function registerView($view, $order = null, $applicationName = 'default') |
213
|
|
|
{ |
214
|
28 |
|
if (! class_exists($view)) { |
215
|
1 |
|
throw new ViewException("No such view class {view} found", array( |
216
|
1 |
|
'view' => $view |
217
|
|
|
)); |
218
|
|
|
} |
219
|
|
|
|
220
|
28 |
|
$v = new $view(); |
221
|
28 |
|
if (! $v instanceof View) { |
222
|
1 |
|
throw new ViewException("View {view} is not in application scope", array( |
223
|
1 |
|
'view' => $view |
224
|
|
|
)); |
225
|
|
|
} |
226
|
28 |
|
$viewOrder = $v->getOrder(); |
227
|
28 |
|
if (null !== $order) { |
228
|
1 |
|
$viewOrder = intval($order); |
229
|
|
|
} |
230
|
|
|
|
231
|
28 |
|
$settings = $v->getViewSettings(); |
232
|
28 |
|
$this->views[$applicationName][$viewOrder][$settings->getViewSimpleName()] = $settings; |
233
|
|
|
|
234
|
28 |
|
return $this; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Register a view control |
239
|
|
|
* |
240
|
|
|
* @param string $controlIdentifier |
241
|
|
|
* The identifier under which the control will be registered |
242
|
|
|
* @param string $controlClass |
243
|
|
|
* The class of control |
244
|
|
|
* |
245
|
|
|
* @return Application Current application instance |
246
|
|
|
*/ |
247
|
4 |
|
public function registerViewControl($controlIdentifier, $controlClass) |
248
|
|
|
{ |
249
|
4 |
|
$this->viewControls[$controlIdentifier] = $controlClass; |
250
|
4 |
|
return $this; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Unregister a given view |
255
|
|
|
* |
256
|
|
|
* @param string $view |
257
|
|
|
* The view to unregister |
258
|
|
|
* @param string $applicationName |
259
|
|
|
* Optional application name where the view is registered |
260
|
|
|
* |
261
|
|
|
* @return Application Current application instance |
262
|
|
|
*/ |
263
|
1 |
|
public function unregisterView($view, $order, $applicationName = 'default') |
264
|
|
|
{ |
265
|
1 |
|
if (isset($this->views[$applicationName][$order][$view])) { |
266
|
1 |
|
unset($this->views[$applicationName][$order][$view]); |
267
|
|
|
} |
268
|
1 |
|
return $this; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Get the best view for request |
273
|
|
|
* |
274
|
|
|
* @param Request $request |
275
|
|
|
* The request to get best view for |
276
|
|
|
* |
277
|
|
|
* @return View The view best matched for the request |
278
|
|
|
* |
279
|
|
|
* @throws ViewException |
280
|
|
|
*/ |
281
|
20 |
|
private function getViewBestMatch(Request $request, $applicationName) |
282
|
|
|
{ |
283
|
20 |
|
$best = null; |
284
|
|
|
|
285
|
20 |
|
if (count($this->views[$applicationName]) > 0) { |
286
|
20 |
|
foreach ($this->views[$applicationName] as $orderLevel => $views) { |
287
|
20 |
|
foreach ($views as $view) { |
288
|
19 |
|
assert($view instanceof View); |
289
|
19 |
|
if ($view->matchBoth($request->getController(), $request->getAction())) { |
290
|
19 |
|
$best[$orderLevel] = $view; |
291
|
20 |
|
continue 2; |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
} |
295
|
|
|
} |
296
|
20 |
|
if (null == $best) { |
297
|
1 |
|
throw new ViewException("No view found for request"); |
298
|
|
|
} |
299
|
|
|
|
300
|
19 |
|
if (count($best) > 1) { |
301
|
1 |
|
krsort($best); |
302
|
|
|
} |
303
|
|
|
|
304
|
19 |
|
return reset($best); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Register a new controller class |
309
|
|
|
* |
310
|
|
|
* @param string $controller |
311
|
|
|
* The full qualified name of controller class to register |
312
|
|
|
* @param string $applicationName |
313
|
|
|
* Optional name of application where controller will be registered in |
314
|
|
|
* |
315
|
|
|
* @return Application Current application instance |
316
|
|
|
* |
317
|
|
|
* @throws ControllerException |
318
|
|
|
*/ |
319
|
28 |
|
public function registerController($controller, $applicationName = 'default') |
320
|
|
|
{ |
321
|
28 |
|
if ( !$controller instanceof \Nkey\Caribu\Mvc\Controller\AbstractController ) { |
322
|
28 |
|
if (! class_exists($controller)) { |
323
|
1 |
|
throw new ControllerException("No such controller class {controller} found", array( |
324
|
1 |
|
'controller' => $controller |
325
|
|
|
)); |
326
|
|
|
} |
327
|
28 |
|
$c = new $controller(); |
328
|
28 |
|
if (! ($c instanceof AbstractController)) { |
329
|
1 |
|
throw new ControllerException("Controller {controller} is not in application scope", array( |
330
|
28 |
|
'controller' => $controller |
331
|
|
|
)); |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
else { |
335
|
2 |
|
$c = $controller; |
336
|
|
|
} |
337
|
28 |
|
$settings = $c->getControllerSettings(); |
338
|
28 |
|
$this->controllers[$applicationName][$settings->getControllerSimpleName()] = $settings; |
339
|
|
|
|
340
|
28 |
|
return $this; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Start the application |
345
|
|
|
* |
346
|
|
|
* @param string $applicationName |
347
|
|
|
* Optional application name to service the request for |
348
|
|
|
* @param array $serverVars |
349
|
|
|
* The server variables provided by sapi |
350
|
|
|
* @param Request $request |
351
|
|
|
* Optional previous generated request object |
352
|
|
|
* @param boolean $send |
353
|
|
|
* Optional whether to send the output directly to client |
354
|
|
|
* |
355
|
|
|
* @throws ControllerException |
356
|
|
|
* @throws InvalidUrlException |
357
|
|
|
*/ |
358
|
20 |
|
public function serve($applicationName = 'default', $serverVars = array(), Request $request = null, $send = true) |
359
|
|
|
{ |
360
|
20 |
|
if (null === $request) { |
361
|
1 |
|
$request = Request::parseFromServerRequest($serverVars, $this->defaultController, $this->defaultAction); |
362
|
|
|
} |
363
|
|
|
|
364
|
20 |
|
foreach ($this->overridenClientHeaders as $headerName => $headerValue) { |
365
|
|
|
$request->setParam($headerName, $headerValue); |
366
|
|
|
} |
367
|
|
|
|
368
|
20 |
|
$controller = $request->getController(); |
369
|
20 |
|
$action = $request->getAction(); |
370
|
|
|
|
371
|
20 |
|
$this->getLogger()->debug("[{remote}] Requested controller is {controller} and action is {action}", array( |
372
|
20 |
|
'remote' => $request->getRemoteHost(), |
373
|
20 |
|
'controller' => $controller, |
374
|
20 |
|
'action' => $action |
375
|
|
|
)); |
376
|
|
|
|
377
|
20 |
|
if ( null != $this->router && $this->router->hasRoute($action) ) { |
378
|
1 |
|
$controllerInstance = $this->router->route($action, $request); |
379
|
1 |
|
$action = $request->getAction(); |
380
|
|
|
} |
381
|
|
|
else { |
382
|
19 |
View Code Duplication |
if (! isset($this->controllers[$applicationName][$controller])) { |
|
|
|
|
383
|
2 |
|
$this->getLogger()->error("[{remote}] No such controller {controller}", array( |
384
|
2 |
|
'remote' => $request->getRemoteHost(), |
385
|
2 |
|
'controller' => $controller |
386
|
|
|
)); |
387
|
2 |
|
$controller = 'Error'; |
388
|
2 |
|
$action = 'error'; |
389
|
|
|
} |
390
|
|
|
|
391
|
19 |
|
$controllerInstance = $this->controllers[$applicationName][$controller]; |
392
|
19 |
|
assert($controllerInstance instanceof AbstractController); |
393
|
19 |
View Code Duplication |
if (! $controllerInstance->hasAction($action)) { |
|
|
|
|
394
|
2 |
|
$this->getLogger()->error("[{remote}] No such action {action}", array( |
395
|
2 |
|
'remote' => $request->getRemoteHost(), |
396
|
2 |
|
'action' => $action |
397
|
|
|
)); |
398
|
2 |
|
$controllerInstance = $this->controllers[$applicationName]['Error']; |
399
|
2 |
|
$action = 'error'; |
400
|
|
|
} |
401
|
|
|
|
402
|
19 |
|
$this->getLogger()->debug("[{remote}] Routing request to {controller}:{action}", array( |
403
|
19 |
|
'remote' => $request->getRemoteHost(), |
404
|
19 |
|
'controller' => $controller, |
405
|
19 |
|
'action' => $action |
406
|
|
|
)); |
407
|
|
|
} |
408
|
|
|
|
409
|
20 |
|
$view = $this->getViewBestMatch($request, $applicationName); |
410
|
|
|
|
411
|
19 |
|
$view->setCssFiles($this->cssFiles); |
412
|
19 |
|
$view->setJsFiles($this->jsFiles); |
413
|
|
|
|
414
|
19 |
|
foreach ($this->viewControls as $controlIdentifier => $controlClass) { |
415
|
4 |
|
$view->registerControl($controlClass, $controlIdentifier); |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
try { |
419
|
19 |
|
$response = $controllerInstance->call($action, $request, $view); |
420
|
1 |
|
} catch (\Exception $ex) { |
421
|
1 |
|
$controllerInstance = $this->controllers[$applicationName]['Error']; |
422
|
1 |
|
$action = 'exception'; |
423
|
1 |
|
$request->setException($ex); |
424
|
1 |
|
$response = $controllerInstance->call($action, $request, $view); |
425
|
|
|
|
426
|
1 |
|
$outputBuffer = ob_get_clean(); |
427
|
1 |
|
if (strlen($outputBuffer)) { |
428
|
|
|
$response->appendBody($outputBuffer); |
429
|
|
|
} |
430
|
|
|
} |
431
|
|
|
|
432
|
19 |
|
$responseCode = $response->getHttpCode(); |
433
|
19 |
|
$responseType = sprintf('%s;%s', $response->getType(), $response->getEncoding()); |
434
|
19 |
|
$responseContent = strval($response); |
435
|
19 |
|
$responseLen = strlen($responseContent); |
436
|
|
|
|
437
|
19 |
|
$this->getLogger()->debug("[{remote}] Response is type of {type}, length of {length} and code {code}", array( |
438
|
19 |
|
'remote' => $request->getRemoteHost(), |
439
|
19 |
|
'type' => $responseType, |
440
|
19 |
|
'length' => $responseLen, |
441
|
19 |
|
'code' => $responseCode |
442
|
|
|
)); |
443
|
|
|
|
444
|
19 |
|
if ($send) { |
445
|
|
|
header(sprintf("%s", $responseCode)); |
446
|
|
|
header(sprintf("Content-Length: %d", $responseLen)); |
447
|
|
|
header(sprintf("Content-Type: %s", $responseType)); |
448
|
|
|
|
449
|
|
|
foreach ($this->defaultHeaders as $headerName => $headerValue) { |
450
|
|
|
header(sprintf("%s: %s", $headerName, $headerValue)); |
451
|
|
|
} |
452
|
|
|
foreach ($response->getAdditionalHeaders() as $headerName => $headerValue) { |
453
|
|
|
header(sprintf("%s: %s", $headerName, $headerValue)); |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
echo $responseContent; |
457
|
|
|
} |
458
|
|
|
|
459
|
19 |
|
return $response; |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
/** |
463
|
|
|
* Register a new Router |
464
|
|
|
* |
465
|
|
|
* @param AbstractRouter $router |
466
|
|
|
* @return Application the current application instance |
467
|
|
|
*/ |
468
|
2 |
|
public function registerRouter(AbstractRouter $router) |
469
|
|
|
{ |
470
|
2 |
|
$this->router = $router; |
471
|
2 |
|
$this->router->setApplication($this); |
472
|
2 |
|
return $this; |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
/** |
476
|
|
|
* Enable session handling |
477
|
|
|
* |
478
|
|
|
* @return Application The current application instance |
479
|
|
|
*/ |
480
|
|
|
public function enableSession() |
481
|
|
|
{ |
482
|
|
|
session_start(); |
483
|
|
|
|
484
|
|
|
return $this; |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Retrieve the default controller name |
489
|
|
|
* |
490
|
|
|
* @return string The name of default controller |
491
|
|
|
*/ |
492
|
|
|
public function getDefaultController() |
493
|
|
|
{ |
494
|
|
|
return $this->defaultController; |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
/** |
498
|
|
|
* Retrieve the default action name |
499
|
|
|
* |
500
|
|
|
* @return string The name of default action |
501
|
|
|
*/ |
502
|
|
|
public function getDefaultAction() |
503
|
|
|
{ |
504
|
|
|
return $this->defaultAction; |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
/** |
508
|
|
|
* Add a new header to specific value. |
509
|
|
|
* |
510
|
|
|
* Existing header will be overriden. |
511
|
|
|
* |
512
|
|
|
* @param string $name |
513
|
|
|
* The header identifier |
514
|
|
|
* @param string $value |
515
|
|
|
* The value to set |
516
|
|
|
* |
517
|
|
|
* @return Application The current application instance |
518
|
|
|
*/ |
519
|
|
|
public function addHeader($name, $value) |
520
|
|
|
{ |
521
|
|
|
$this->defaultHeaders[$name] = $value; |
522
|
|
|
return $this; |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
/** |
526
|
|
|
* Add a header to overide a client request header |
527
|
|
|
* |
528
|
|
|
* @param string $name |
529
|
|
|
* The header name to override |
530
|
|
|
* @param string $value |
531
|
|
|
* The value to override |
532
|
|
|
* |
533
|
|
|
* @return Application The current application instance |
534
|
|
|
*/ |
535
|
|
|
public function addOverridenClientHeader($name, $value) |
536
|
|
|
{ |
537
|
|
|
$this->overridenClientHeaders[$name] = $value; |
538
|
|
|
return $this; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Add an uri for an additional javascript file |
543
|
|
|
* |
544
|
|
|
* @param string $file |
545
|
|
|
* |
546
|
|
|
* @return Application the current application instance |
547
|
|
|
*/ |
548
|
|
|
public function addJsFile($file) |
549
|
|
|
{ |
550
|
|
|
$this->jsFiles[] = $file; |
551
|
|
|
return $this; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* Add an uri for an additional css file |
556
|
|
|
* |
557
|
|
|
* @param string $file |
558
|
|
|
* |
559
|
|
|
* @return Application the current application instance |
560
|
|
|
*/ |
561
|
|
|
public function addCssFile($file) |
562
|
|
|
{ |
563
|
|
|
$this->cssFiles[] = $file; |
564
|
|
|
return $this; |
565
|
|
|
} |
566
|
|
|
} |
567
|
|
|
|
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.