Passed
Push — master ( 35b695...9087a9 )
by Alexander
03:19
created

PleasingPageHandler   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 500
Duplicated Lines 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 156
c 10
b 0
f 0
dl 0
loc 500
rs 7.44
wmc 52

22 Methods

Rating   Name   Duplication   Size   Complexity  
A addTables() 0 3 1
A addEditor() 0 3 1
A __construct() 0 4 1
A getEditorcode() 0 3 1
A setPageTitle() 0 3 1
A getProcessTables() 0 25 6
A getExceptionCode() 0 10 2
A setEditor() 0 9 3
A getExceptionFrames() 0 5 1
A getDefaultRouting() 0 17 2
A getEditorAtHref() 0 16 4
A setBrand() 0 3 1
A getDefaultDatabase() 0 9 1
A getDefaultServers() 0 17 1
A getBrand() 0 3 1
A getDefaultContext() 0 11 2
C getEditor() 0 29 14
A getResource() 0 20 4
A contentType() 0 3 1
A collectionVars() 0 37 1
A handle() 0 12 2
A getPageTitle() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PleasingPageHandler 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.

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 PleasingPageHandler, and based on these observations, apply Extract Interface, too.

1
<?php 
2
3
/**
4
 * Lenevor Framework
5
 *
6
 * LICENSE
7
 *
8
 * This source file is subject to the new BSD license that is bundled
9
 * with this package in the file license.md.
10
 * It is also available through the world-wide-web at this URL:
11
 * https://lenevor.com/license
12
 * If you did not receive a copy of the license and are unable to
13
 * obtain it through the world-wide-web, please send an email
14
 * to [email protected] so we can send you a copy immediately.
15
 *
16
 * @package     Lenevor
17
 * @subpackage  Base
18
 * @link        https://lenevor.com
19
 * @copyright   Copyright (c) 2019 - 2023 Alexander Campo <[email protected]>
20
 * @license     https://opensource.org/licenses/BSD-3-Clause New BSD license or see https://lenevor.com/license or see /license.md
21
 */
22
23
namespace Syscodes\Components\Debug\Handlers;
24
25
use Exception;
26
use Traversable;
27
use ErrorException;
28
use RuntimeException;
29
use InvalidArgumentException;
30
use UnexpectedValueException;
31
use Syscodes\Components\Debug\Util\Misc;
32
use Syscodes\Components\Contracts\Debug\Table;
33
use Syscodes\Components\Debug\Util\ArrayTable;
34
use Syscodes\Components\Debug\Util\TemplateHandler;
35
use Syscodes\Components\Debug\FrameHandler\Formatter;  
36
37
/**
38
 * Generates exceptions in mode of graphic interface.
39
 */
40
class PleasingPageHandler extends Handler
41
{
42
	/**
43
	 * The brand main of handler.
44
	 * 
45
	 * @var string $brand
46
	 */
47
	protected $brand = 'Lenevor Debug';
48
49
	/**
50
	 * A string identifier for a known IDE/text editor, or a closure
51
	 * that resolves a string that can be used to open a given file
52
	 * in an editor.
53
	 * 
54
	 * @var mixed $editor
55
	 */
56
	protected $editor;
57
58
	/**
59
	 * A list of known editor strings.
60
	 * 
61
	 * @var array $editors
62
	 */
63
	protected $editors = [
64
		"vscode"   => "vscode://file/%file:%line",
65
		"sublime"  => "subl://open?url=file://%file&line=%line",
66
		"phpstorm" => "phpstorm://open?file://%file&line=%line",
67
		"textmate" => "txmt://open?url=file://%file&line=%line",
68
		"atom"     => "atom://core/open/file?filename=%file&line=%line",
69
	];
70
	
71
	/**
72
	 * The page title main of handler.
73
	 * 
74
	 * @var string $pageTitle
75
	 */
76
	protected $pageTitle = 'Lenevor Debug! There was an error';
77
	
78
	/**
79
	 * Fast lookup cache for known resource locations.
80
	 * 
81
	 * @var array $resourceCache
82
	 */
83
	protected $resourceCache = [];
84
	
85
	/**
86
	 * The path to the directory containing the html error template directories.
87
	 * 
88
	 * @var array $searchPaths
89
	 */
90
	protected $searchPaths = [];
91
92
	/**
93
	 * Gets the table of data.
94
	 * 
95
	 * @var array $tables
96
	 */
97
	protected $tables = [];
98
	
99
	/**
100
	 * The template handler system.
101
	 * 
102
	 * @var string|object $template
103
	 */
104
	protected $template;	
105
	
106
	/**
107
	 * Constructor. The PleasingPageHandler class.
108
	 * 
109
	 * @return void
110
	 */
111
	public function __construct()
112
	{
113
		$this->template      = new TemplateHandler;
114
		$this->searchPaths[] = dirname(__DIR__).DIRECTORY_SEPARATOR.'Resources';
115
	}
116
117
	/**
118
	 * Adds an editor resolver, identified by a string name, and that may be a 
119
	 * string path, or a callable resolver.
120
	 * 
121
	 * @param  string            $identifier
122
	 * @param  string|\Callable  $resolver
0 ignored issues
show
Bug introduced by
The type Callable was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
123
	 * 
124
	 * @return void
125
	 */
126
	public function addEditor($identifier, $resolver): void
127
	{
128
		$this->editors[$identifier] = $resolver;
129
	}
130
131
	/**
132
	 * Adds an entry to the list of tables displayed in the template.
133
	 * The expected data is a simple associative array. Any nested arrays
134
	 * will be flattened with print_r.
135
	 * 
136
	 * @param  \Syscodes\Components\Contracts\Debug\Table  $table
137
	 * 
138
	 * @return void
139
	 */
140
	public function addTables(Table $table): void
141
	{
142
		$this->tables[] = $table;
143
	}
144
	
145
	/**
146
	 * Gathers the variables that will be made available to the view.
147
	 * 
148
	 * @return  array
149
	 */
150
	protected function collectionVars(): array
151
	{
152
		$supervisor = $this->getSupervisor();
153
		$style      = file_get_contents($this->getResource('css/debug.base.css'));
154
		$jscript    = file_get_contents($this->getResource('js/debug.base.js'));
155
		$servers    = array_merge($this->getDefaultServers(), $this->tables);
156
		$routing    = array_merge($this->getDefaultRouting(), $this->tables);
157
		$databases  = array_merge($this->getDefaultDatabase(), $this->tables);
158
		$context    = array_merge($this->getDefaultContext(), $this->tables);
159
		
160
		return [ 
161
			'class' => explode('\\', $supervisor->getExceptionName()),
162
			'stylesheet' => preg_replace('#[\r\n\t ]+#', ' ', $style),
163
			'javascript' => preg_replace('#[\r\n\t ]+#', ' ', $jscript),
164
			'header' => $this->getResource('views/partials/updown/header.php'),
165
			'footer' => $this->getResource('views/partials/updown/footer.php'),
166
			'info_exception' => $this->getResource('views/partials/info/info_exception.php'),
167
			'section_stack_exception' => $this->getResource('views/partials/section_stack_exception.php'),
168
			'section_frame' => $this->getResource('views/partials/section_frame.php'),
169
			'frame_description' => $this->getResource('views/partials/frames/frame_description.php'),
170
			'frame_list' => $this->getResource('views/partials/frames//frame_list.php'),
171
			'section_code' => $this->getResource('views/partials/section_code.php'),
172
			'code_source' => $this->getResource('views/partials/codes/code_source.php'),
173
			'request_info' => $this->getResource('views/partials/request_info.php'),
174
			'navigation' => $this->getResource('views/partials/details/navigation.php'),
175
			'section_detail_context' => $this->getResource('views/partials/details/section_detail_context.php'),
176
			'plain_exception' => Formatter::formatExceptionAsPlainText($this->getSupervisor()),
177
			'handler' => $this,
178
			'handlers' => $this->getDebug()->getHandlers(),
179
			'debug' => $this->getDebug(),
180
			'code' => $this->getExceptionCode(),
181
			'message' => $supervisor->getExceptionMessage(),
182
			'frames' => $this->getExceptionFrames(),
183
			'servers' => $this->getProcessTables($servers),
184
			'routes' => $this->getProcessTables($routing),
185
			'databases' => $this->getProcessTables($databases),
186
			'contexts' => $this->getProcessTables($context),
187
		];
188
	}
189
	
190
	/**
191
	 * The way in which the data sender (usually the server) can tell the recipient
192
	 * (the browser, in general) what type of data is being sent in this case, html format tagged.
193
	 * 
194
	 * @return string
195
	 */
196
	public function contentType(): string
197
	{
198
		return 'text/html;charset=UTF-8';
199
	}
200
201
	/**
202
	 * Gets the brand of project.
203
	 * 
204
	 * @return string
205
	 */
206
	public function getBrand(): string
207
	{
208
		return $this->brand;
209
	}
210
211
	/**
212
	 * Returns the default servers.
213
	 * 
214
	 * @return \Syscodes\Components\Contracts\Debug\Table[]
215
	 */
216
	protected function getDefaultServers()
217
	{
218
		$server = [
219
			'host' => $_SERVER['HTTP_HOST'], 
220
			'user-agent' => $_SERVER['HTTP_USER_AGENT'], 
221
			'accept' => $_SERVER['HTTP_ACCEPT'], 
222
			'accept-language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'], 
223
			'accept-encoding' => $_SERVER['HTTP_ACCEPT_ENCODING'],
224
			'connection' => $_SERVER['HTTP_CONNECTION'],
225
			'upgrade-insecure-requests' => $_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS'], 
226
			'sec-fetch-dest' => $_SERVER['HTTP_SEC_FETCH_DEST'],
227
			'sec-fetch-mode' => $_SERVER['HTTP_SEC_FETCH_MODE'],
228
			'sec-fetch-site' => $_SERVER['HTTP_SEC_FETCH_SITE'],
229
			'sec-fetch-user' => $_SERVER['HTTP_SEC_FETCH_USER'],
230
		];
231
232
		return [new ArrayTable($server)];
233
	}
234
235
	/**
236
	 * Returns the default routing.
237
	 * 
238
	 * @return \Syscodes\Components\Contracts\Debug\Table[]
239
	 */
240
	protected function getDefaultRouting()
241
	{
242
		$action = app('request')->route()->isControllerAction() 
0 ignored issues
show
Bug introduced by
The method route() does not exist on Syscodes\Components\Contracts\Core\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

242
		$action = app('request')->/** @scrutinizer ignore-call */ route()->isControllerAction() 

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
243
		          ? app('request')->route()->parseControllerCallback()[0] 
244
		          : 'Closure';
245
246
		$index = match (true) {
247
			array_key_exists('web', app('router')->getMiddlewareGroups()) => 0,
0 ignored issues
show
Bug introduced by
The method getMiddlewareGroups() does not exist on Syscodes\Components\Contracts\Core\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

247
			array_key_exists('web', app('router')->/** @scrutinizer ignore-call */ getMiddlewareGroups()) => 0,

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
248
			array_key_exists('api', app('router')->getMiddlewareGroups()) => 1,
249
		};
250
251
		$routing = [
252
			'Controller' => $action,
253
			'Middleware' => array_keys(app('router')->getMiddlewareGroups())[$index],
254
		];
255
256
		return [new ArrayTable($routing)];
257
	}
258
259
	/**
260
	 * Returns the default database.
261
	 * 
262
	 * @return \Syscodes\Components\Contracts\Debug\Table[]
263
	 */
264
	protected function getDefaultDatabase()
265
	{
266
		$query = [
267
			'Sql' => null,
268
			'Time' => null,
269
			'Connection name' => null,
270
		];
271
272
		return [new ArrayTable($query)];
273
	}
274
275
	/**
276
	 * Returns the default context data.
277
	 * 
278
	 * @return \Syscodes\Components\Contracts\Debug\Table[]
279
	 */
280
	protected function getDefaultContext()
281
	{
282
		$context = [
283
			'Php Version' => PHP_VERSION,
284
			'Lenevor Version' => app()->version(),
285
			'Lenevor Locale' => config('app.locale'),
286
			'App Debug' => (1 == env('APP_DEBUG') ? 'true' : 'false'),
287
			'App Env' => env('APP_ENV'),
288
		];
289
290
		return [new ArrayTable($context)];
291
	}
292
293
	/**
294
	 * Get the code of the exception that is currently being handled.
295
	 * 
296
	 * @return string
297
	 */
298
	protected function getExceptionCode()
299
	{
300
		$exception = $this->getException();
301
		$code      = $exception->getCode();
302
303
		if ($exception instanceof ErrorException) {
304
			$code = Misc::translateErrorCode($exception->getSeverity());
305
		}
306
307
		return (string) $code;
308
	}
309
310
	/**
311
	 * Get the stack trace frames of the exception that is currently being handled.
312
	 * 
313
	 * @return \Syscodes\Components\Debug\Engine\Supervisor;
0 ignored issues
show
Bug introduced by
The type Syscodes\Components\Debug\Engine\Supervisor was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
314
	 */
315
	protected function getExceptionFrames()
316
	{
317
		$frames = $this->getSupervisor()->getFrames();
318
		
319
		return $frames;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $frames returns the type array which is incompatible with the documented return type Syscodes\Components\Debug\Engine\Supervisor.
Loading history...
320
	}
321
	
322
	/**
323
	 * Gets the page title web.
324
	 * 
325
	 * @return string
326
	 */
327
	public function getPageTitle(): string
328
	{
329
		return $this->pageTitle;
330
	}
331
332
	/**
333
	 * Processes an array of tables making sure everything is all right.
334
	 * 
335
	 * @param  \Syscodes\Components\Contracts\Debug\Table[]  $tables
336
	 * 
337
	 * @return array
338
	 */
339
	protected function getProcessTables(array $tables): array
340
	{
341
		$processTables = [];
342
343
		foreach ($tables as $table) {
344
			if ( ! $table instanceof Table) {
345
				continue;
346
			}
347
			
348
			$label = $table->getLabel();
349
350
			try {
351
				$data = (array) $table->getData();
352
353
				if ( ! (is_array($data) || $data instanceof Traversable)) {
354
					$data = [];
355
				}
356
			} catch (Exception $e) {
357
				$data = [];
358
			}
359
360
			$processTables[$label] = $data;
361
		}
362
363
		return $processTables;
364
	}
365
366
	/**
367
	 * Finds a resource, by its relative path, in all available search paths.
368
	 *
369
	 * @param  string  $resource
370
	 * 
371
	 * @return string
372
	 * 
373
	 * @throws \RuntimeException
374
	 */
375
	protected function getResource($resource)
376
	{
377
		if (isset($this->resourceCache[$resource])) {
378
			return $this->resourceCache[$resource];
379
		}
380
381
		foreach ($this->searchPaths as $path) {
382
			$fullPath = $path.DIRECTORY_SEPARATOR.$resource;
383
384
			if (is_file($fullPath)) {
385
				// Cache:
386
				$this->resourceCache[$resource] = $fullPath;
387
388
				return $fullPath;
389
			}
390
		}
391
392
		throw new RuntimeException( 
393
				"Could not find resource '{$resource}' in any resource paths.". 
394
				"(searched: ".join(", ", $this->searchPaths).")");
395
	}
396
	
397
	/**
398
	 * Given an exception and status code will display the error to the client.
399
	 * 
400
	 * @return int|null
401
	 */
402
	public function handle()
403
	{	
404
		$templatePath = $this->getResource('views/debug.layout.php');
405
406
		$vars = $this->collectionVars();
407
		
408
		if (empty($vars['message'])) $vars['message'] = __('exception.noMessage');
409
		
410
		$this->template->setVariables($vars);
411
		$this->template->render($templatePath);
412
		
413
		return Handler::QUIT;
414
	}
415
416
	/**
417
	 * Set the editor to use to open referenced files, by a string identifier or callable
418
	 * that will be executed for every file reference. Should return a string.
419
	 * 
420
	 * @example  $debug->setEditor(function($file, $line) { return "file:///{$file}"; });
421
	 * @example  $debug->setEditor('vscode');
422
	 * 
423
	 * @param  string  $editor
424
	 * 
425
	 * @return void
426
	 * 
427
	 * @throws \InvalidArgumentException
428
	 */
429
	public function setEditor($editor)
430
	{
431
		if ( ! is_callable($editor) && ! isset($this->editors[$editor])) {
432
			throw new InvalidArgumentException("Unknown editor identifier: [{$editor}]. Known editors: " .
433
				implode(', ', array_keys($this->editors))
434
			);
435
		}
436
437
		$this->editor = $editor;
438
	}
439
440
	/**
441
	 * Given a string file path, and an integer file line,
442
	 * executes the editor resolver and returns.
443
	 * 
444
	 * @param  string  $file
445
	 * @param  int	   $line
446
	 * 
447
	 * @return string|bool
448
	 * 
449
	 * @throws \UnexpectedValueException
450
	 */
451
	public function getEditorAtHref($file, $line)
452
	{
453
		$editor = $this->getEditor($file, $line);
454
455
		if (empty($editor))	{
456
			return false;
457
		}
458
459
		if ( ! isset($editor['url']) || ! is_string($editor['url'])) {
460
			throw new UnexpectedValueException(__METHOD__.'should always resolve to a string or a valid editor array');
461
		}
462
463
		$editor['url'] = str_replace("%file", rawurldecode($file), $editor['url']);
464
		$editor['url'] = str_replace("%line", rawurldecode($line), $editor['url']);
465
466
		return $editor['url'];
467
	}
468
469
	/**
470
	 * The editor must be a valid callable function/closure.
471
	 * 
472
	 * @param  string  $file
473
	 * @param  int	   $line
474
	 * 
475
	 * @return array
476
	 */
477
	protected function getEditor($file, $line): array
478
	{
479
		if ( ! $this->editor || ( ! is_string($this->editor) && ! is_callable($this->editor))) {
480
			return [];
481
		}
482
483
		if (is_string($this->editor) && isset($this->editors[$this->editor]) && ! is_callable($this->editors[$this->editor])) {
484
			return ['url' => $this->editors[$this->editor]];
485
		}
486
487
		if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) {
488
			if (is_callable($this->editor)) {
489
				$callback = call_user_func($this->editor, $file, $line);
490
			} else {
491
				$callback = call_user_func($this->editors[$this->editor], $file, $line);
492
			}
493
494
			if (empty($callback)) {
495
				return [];
496
			}
497
498
			if (is_string($callback)) {
499
				return ['url' => $callback];
500
			}
501
			
502
			return ['url' => isset($callback['url']) ? $callback['url'] : $callback];
503
		}
504
		
505
		return [];
506
	}
507
508
	/**
509
	 * Registered the editor.
510
	 * 
511
	 * @return string
512
	 */
513
	public function getEditorcode(): string
514
	{
515
		return $this->editor;
516
	}
517
	
518
	/**
519
	 * Sets the brand of project.
520
	 * 
521
	 * @param  string  $brand
522
	 * 
523
	 * @return void
524
	 */
525
	public function setBrand($brand): void
526
	{
527
		$this->brand = (string) $brand;
528
	}
529
	
530
	/**
531
	 * Sets the page title web.
532
	 * 
533
	 * @param  string  $title
534
	 * 
535
	 * @return void
536
	 */
537
	public function setPageTitle($title): void
538
	{
539
		$this->pageTitle = (string) $title;
540
	}
541
}