Passed
Push — master ( f8e3f7...025a78 )
by Sebastian
03:51
created

ConvertHelper_ThrowableInfo   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 526
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 53
eloc 171
c 7
b 0
f 0
dl 0
loc 526
rs 6.96

30 Methods

Rating   Name   Duplication   Size   Complexity  
A setFolderDepth() 0 3 1
A getReferer() 0 3 1
A getPrevious() 0 10 2
A getCode() 0 3 1
A hasDetails() 0 3 1
A fromSerialized() 0 3 1
A getMessage() 0 3 1
A isWebRequest() 0 3 1
A renderErrorMessage() 0 41 5
A getCalls() 0 3 1
A isCommandLine() 0 3 1
A getDate() 0 3 1
A hasPrevious() 0 3 1
A getFolderDepth() 0 8 2
A toString() 0 25 4
A countCalls() 0 3 1
A getContext() 0 3 1
A getDetails() 0 8 2
A validateSerializedData() 0 18 4
A __construct() 0 9 2
A hasCode() 0 3 1
A __toString() 0 3 1
B parseException() 0 50 6
A getDefaultOptions() 0 4 1
A serialize() 0 24 3
A getFinalCall() 0 3 1
A createTypeException() 0 10 1
A getClass() 0 3 1
A parseSerialized() 0 21 3
A fromThrowable() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ConvertHelper_ThrowableInfo 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 ConvertHelper_ThrowableInfo, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace AppUtils;
6
7
use Exception;
8
use Throwable;
9
10
class ConvertHelper_ThrowableInfo implements Interface_Optionable
11
{
12
    use Traits_Optionable;
13
    
14
    const ERROR_NO_PREVIOUS_EXCEPTION = 43301;
15
    const ERROR_INVALID_SERIALIZED_DATA_TYPE = 43302;
16
    
17
    const FORMAT_HTML = 'html';
18
19
    const CONTEXT_COMMAND_LINE = 'cli';
20
    const CONTEXT_WEB = 'web';
21
22
    const SERIALIZED_CODE = 'code';
23
    const SERIALIZED_MESSAGE = 'message';
24
    const SERIALIZED_REFERER = 'referer';
25
    const SERIALIZED_CONTEXT = 'context';
26
    const SERIALIZED_AMOUNT_CALLS = 'amountCalls';
27
    const SERIALIZED_DATE = 'date';
28
    const SERIALIZED_PREVIOUS = 'previous';
29
    const SERIALIZED_CALLS = 'calls';
30
    const SERIALIZED_OPTIONS = 'options';
31
32
    /**
33
    * @var Throwable
34
    */
35
    protected $exception;
36
    
37
   /**
38
    * @var ConvertHelper_ThrowableInfo_Call[]
39
    */
40
    protected $calls = array();
41
    
42
   /**
43
    * @var integer
44
    */
45
    protected $code = 0;
46
    
47
   /**
48
    * @var string
49
    */
50
    protected $message;
51
    
52
   /**
53
    * @var integer
54
    */
55
    protected $callsCount = 0;
56
    
57
   /**
58
    * @var ConvertHelper_ThrowableInfo
59
    */
60
    protected $previous;
61
    
62
   /**
63
    * @var string
64
    */
65
    protected $referer = '';
66
    
67
   /**
68
    * @var Microtime
69
    */
70
    protected $date;
71
    
72
   /**
73
    * @var string
74
    */
75
    protected $context = self::CONTEXT_WEB;
76
77
    /**
78
     * @param array<string,mixed>|Throwable $subject
79
     */
80
    protected function __construct($subject)
81
    {
82
        if(is_array($subject))
83
        {
84
            $this->parseSerialized($subject);
85
        }
86
        else
87
        {
88
            $this->parseException($subject);
89
        }
90
    }
91
    
92
    public static function fromThrowable(Throwable $e) : ConvertHelper_ThrowableInfo
93
    {
94
        return new ConvertHelper_ThrowableInfo($e);
95
    }
96
97
    /**
98
     * @param array<string,mixed> $serialized
99
     * @return ConvertHelper_ThrowableInfo
100
     */
101
    public static function fromSerialized(array $serialized) : ConvertHelper_ThrowableInfo
102
    {
103
        return new ConvertHelper_ThrowableInfo($serialized);
104
    }
105
    
106
    public function getCode() : int
107
    {
108
        return $this->code;
109
    }
110
    
111
    public function getMessage() : string
112
    {
113
        return $this->message;
114
    }
115
116
    public function getDefaultOptions() : array
117
    {
118
        return array(
119
            'folder-depth' => 2
120
        );
121
    }
122
    
123
    public function hasPrevious() : bool
124
    {
125
        return isset($this->previous);
126
    }
127
    
128
   /**
129
    * Retrieves the information on the previous exception.
130
    * 
131
    * NOTE: Throws an exception if there is no previous 
132
    * exception. Use hasPrevious() first to avoid this.
133
    * 
134
    * @throws ConvertHelper_Exception
135
    * @return ConvertHelper_ThrowableInfo
136
    * @see ConvertHelper_ThrowableInfo::ERROR_NO_PREVIOUS_EXCEPTION
137
    */
138
    public function getPrevious() : ConvertHelper_ThrowableInfo
139
    {
140
        if(isset($this->previous)) {
141
            return $this->previous;
142
        }
143
        
144
        throw new ConvertHelper_Exception(
145
            'Cannot get previous exception info: none available.',
146
            'Always use hasPrevious() before using getPrevious() to avoid this error.',
147
            self::ERROR_NO_PREVIOUS_EXCEPTION
148
        );
149
    }
150
    
151
    public function hasCode() : bool
152
    {
153
        return !empty($this->code);
154
    }
155
    
156
   /**
157
    * Improved text-only exception trace.
158
    */
159
    public function toString() : string
160
    {
161
        $calls = $this->getCalls();
162
        
163
        $string = 'Exception';
164
        
165
        if($this->hasCode()) {
166
            $string .= ' #'.$this->code;
167
        }
168
        
169
        $string .= ': '.$this->getMessage().PHP_EOL;
170
        
171
        foreach($calls as $call) 
172
        {
173
            $string .= $call->toString().PHP_EOL;
174
        }
175
        
176
        if($this->hasPrevious())
177
        {
178
            $string .= PHP_EOL.PHP_EOL.
179
            'Previous error:'.PHP_EOL.PHP_EOL.
180
            $this->previous->toString();
181
        }
182
        
183
        return $string;
184
    }
185
    
186
   /**
187
    * Retrieves the URL of the page in which the exception
188
    * was thrown, if applicable: in CLI context, this will
189
    * return an empty string.
190
    * 
191
    * @return string
192
    */
193
    public function getReferer() : string
194
    {
195
        return $this->referer;
196
    }
197
    
198
   /**
199
    * Whether the exception occurred in a command line context.
200
    * @return bool
201
    */
202
    public function isCommandLine() : bool
203
    {
204
        return $this->getContext() === self::CONTEXT_COMMAND_LINE;
205
    }
206
    
207
   /**
208
    * Whether the exception occurred during an http request.
209
    * @return bool
210
    */
211
    public function isWebRequest() : bool
212
    {
213
        return $this->getContext() === self::CONTEXT_WEB;
214
    }
215
    
216
   /**
217
    * Retrieves the context identifier, i.e. if the exception
218
    * occurred in a command line context or regular web request.
219
    * 
220
    * @return string
221
    * 
222
    * @see ConvertHelper_ThrowableInfo::isCommandLine()
223
    * @see ConvertHelper_ThrowableInfo::isWebRequest()
224
    * @see ConvertHelper_ThrowableInfo::CONTEXT_COMMAND_LINE
225
    * @see ConvertHelper_ThrowableInfo::CONTEXT_WEB
226
    */
227
    public function getContext() : string
228
    {
229
        return $this->context;
230
    }
231
    
232
   /**
233
    * Retrieves the date of the exception, and approximate time:
234
    * since exceptions do not store time, this is captured the 
235
    * moment the ThrowableInfo is created.
236
    * 
237
    * @return Microtime
238
    */
239
    public function getDate() : Microtime
240
    {
241
        return $this->date;
242
    }
243
    
244
   /**
245
    * Serializes all information on the exception to an
246
    * associative array. This can be saved (file, database, 
247
    * session...), and later be restored into a throwable
248
    * info instance using the fromSerialized() method.
249
    * 
250
    * @return array<string,mixed>
251
    * @see ConvertHelper_ThrowableInfo::fromSerialized()
252
    */
253
    public function serialize() : array
254
    {
255
        $result = array(
256
            self::SERIALIZED_MESSAGE => $this->getMessage(),
257
            self::SERIALIZED_CODE => $this->getCode(),
258
            self::SERIALIZED_DATE => $this->date->getISODate(),
259
            self::SERIALIZED_REFERER => $this->getReferer(),
260
            self::SERIALIZED_CONTEXT => $this->getContext(),
261
            self::SERIALIZED_AMOUNT_CALLS => $this->callsCount,
262
            self::SERIALIZED_OPTIONS => $this->getOptions(),
263
            self::SERIALIZED_CALLS => array(),
264
            self::SERIALIZED_PREVIOUS => null,
265
        );
266
        
267
        if($this->hasPrevious()) {
268
            $result[self::SERIALIZED_PREVIOUS] =  $this->previous->serialize();
269
        }
270
        
271
        foreach($this->calls as $call)
272
        {
273
            $result[self::SERIALIZED_CALLS][] = $call->serialize();
274
        }
275
        
276
        return $result;
277
    }
278
279
   /**
280
    * Sets the maximum folder depth to show in the 
281
    * file paths, to avoid them being too long.
282
    * 
283
    * @param int $depth
284
    * @return ConvertHelper_ThrowableInfo
285
    */
286
    public function setFolderDepth(int $depth) : ConvertHelper_ThrowableInfo
287
    {
288
        return $this->setOption('folder-depth', $depth);
289
    }
290
    
291
   /**
292
    * Retrieves the current folder depth option value.
293
    * 
294
    * @return int
295
    * @see ConvertHelper_ThrowableInfo::setFolderDepth()
296
    */
297
    public function getFolderDepth() : int
298
    {
299
        $depth = $this->getOption('folder-depth');
300
        if(!empty($depth)) {
301
            return $depth;
302
        }
303
        
304
        return 2;
305
    }
306
    
307
   /**
308
    * Retrieves all function calls that led to the error,
309
    * ordered from latest to earliest (the first one in
310
    * the stack is actually the last call).
311
    *
312
    * @return ConvertHelper_ThrowableInfo_Call[]
313
    */
314
    public function getCalls()
315
    {
316
        return $this->calls;
317
    }
318
319
    /**
320
     * Retrieves the last call that led to the error.
321
     *
322
     * @return ConvertHelper_ThrowableInfo_Call
323
     */
324
    public function getFinalCall() : ConvertHelper_ThrowableInfo_Call
325
    {
326
        return $this->calls[0];
327
    }
328
    
329
   /**
330
    * Returns the amount of function and method calls in the stack trace.
331
    * @return int
332
    */
333
    public function countCalls() : int
334
    {
335
        return $this->callsCount;
336
    }
337
338
339
    /**
340
     * @param array<string,mixed> $serialized
341
     * @throws Exception
342
     */
343
    protected function parseSerialized(array $serialized) : void
344
    {
345
        $this->validateSerializedData($serialized);
346
347
        $this->date = new Microtime($serialized[self::SERIALIZED_DATE]);
348
        $this->code = $serialized[self::SERIALIZED_CODE];
349
        $this->message = $serialized[self::SERIALIZED_MESSAGE];
350
        $this->referer = $serialized[self::SERIALIZED_REFERER];
351
        $this->context = $serialized[self::SERIALIZED_CONTEXT];
352
        $this->callsCount = $serialized[self::SERIALIZED_AMOUNT_CALLS];
353
        
354
        $this->setOptions($serialized[self::SERIALIZED_OPTIONS]);
355
        
356
        if(!empty($serialized[self::SERIALIZED_PREVIOUS]))
357
        {
358
            $this->previous = ConvertHelper_ThrowableInfo::fromSerialized($serialized[self::SERIALIZED_PREVIOUS]);
359
        }
360
        
361
        foreach($serialized[self::SERIALIZED_CALLS] as $def)
362
        {
363
            $this->calls[] = ConvertHelper_ThrowableInfo_Call::fromSerialized($this, $def);
364
        }
365
    }
366
    
367
    protected function parseException(Throwable $e) : void
368
    {
369
        $this->date = new Microtime();
370
        $this->message = $e->getMessage();
371
372
        $code = $e->getCode();
373
374
        if(is_integer($code))
0 ignored issues
show
introduced by
The condition is_integer($code) is always true.
Loading history...
375
        {
376
            $this->code = $code;
377
        }
378
        else
379
        {
380
            $this->message = 'Original error code: ['.$code.']. ';
381
        }
382
383
        if(!isset($_REQUEST['REQUEST_URI'])) {
384
            $this->context = self::CONTEXT_COMMAND_LINE;
385
        }
386
        
387
        $previous = $e->getPrevious();
388
        if(!empty($previous)) {
389
            $this->previous = ConvertHelper::throwable2info($previous);
390
        }
391
        
392
        if(isset($_SERVER['REQUEST_URI'])) {
393
            $this->referer = $_SERVER['REQUEST_URI'];
394
        }
395
        
396
        $trace = $e->getTrace();
397
        
398
        // add the origin file as entry
399
        array_unshift($trace, array(
400
            'file' => $e->getFile(),
401
            'line' => $e->getLine()
402
        ));
403
        
404
        $idx = 1;
405
        
406
        foreach($trace as $entry)
407
        {
408
            $this->calls[] = ConvertHelper_ThrowableInfo_Call::fromTrace($this, $idx, $entry);
409
            
410
            $idx++;
411
        }
412
        
413
        // we want the last function call first
414
        $this->calls = array_reverse($this->calls, false);
415
        
416
        $this->callsCount = count($this->calls);
417
    }
418
419
    /**
420
     * Retrieves the class name of the exception.
421
     *
422
     * @return string
423
     */
424
    public function getClass() : string
425
    {
426
        return get_class($this->exception);
427
    }
428
429
    /**
430
     * Converts the exception's information into a human-
431
     * readable string containing the exception's essentials.
432
     *
433
     * It includes any previous exceptions as well, recursively.
434
     *
435
     * @param bool $withDeveloperInfo Whether to include developer-specific info
436
     *                                when available (which may include sensitive
437
     *                                information).
438
     * @return string
439
     * @throws ConvertHelper_Exception
440
     */
441
    public function renderErrorMessage(bool $withDeveloperInfo=false) : string
442
    {
443
        $finalCall = $this->getFinalCall();
444
445
        $message = sb()
446
            ->t('A %1$s exception occurred.', $this->getClass())
447
            ->eol()
448
            ->t('Code:')
449
            ->add($this->getCode())
450
            ->t('Message:')
451
            ->add($this->getMessage());
452
453
        if($withDeveloperInfo)
454
        {
455
            $message
456
            ->eol()
457
            ->t('Final call:')
458
            ->add($finalCall->toString());
459
        }
460
461
        if($withDeveloperInfo && $this->hasDetails())
462
        {
463
            $message
464
                ->t('Developer details:')
465
                ->eol()
466
                ->add($this->getDetails());
467
        }
468
469
        $previous = $this->getPrevious();
470
471
        if($previous !== null)
472
        {
473
            $message
474
                ->eol()
475
                ->eol()
476
                ->t('Previous exception:')
477
                ->eol()
478
                ->add($previous->renderErrorMessage($withDeveloperInfo));
479
        }
480
481
        return (string)$message;
482
    }
483
484
    public function getDetails() : string
485
    {
486
        if($this->exception instanceof BaseException)
487
        {
488
            return $this->exception->getDetails();
489
        }
490
491
        return '';
492
    }
493
494
    public function hasDetails() : bool
495
    {
496
        return $this->exception instanceof BaseException;
497
    }
498
    
499
    public function __toString()
500
    {
501
        return $this->toString();
502
    }
503
504
    private function validateSerializedData(array $serialized)
505
    {
506
        $keys = array(
507
            self::SERIALIZED_CODE => 'integer',
508
            self::SERIALIZED_MESSAGE => 'string',
509
            self::SERIALIZED_DATE => 'string',
510
            self::SERIALIZED_REFERER => 'string',
511
            self::SERIALIZED_CONTEXT => 'string',
512
            self::SERIALIZED_AMOUNT_CALLS => 'integer',
513
            self::SERIALIZED_OPTIONS => 'array',
514
            self::SERIALIZED_CALLS => 'array'
515
        );
516
517
        foreach($keys as $key => $type)
518
        {
519
            if(!isset($serialized[$key]) || gettype($serialized[$key]) !== $type)
520
            {
521
                throw $this->createTypeException($key, $type);
522
            }
523
        }
524
    }
525
526
    private function createTypeException(string $keyName, string  $expectedType) : ConvertHelper_Exception
527
    {
528
        return new ConvertHelper_Exception(
529
            'Invalid serialized throwable key',
530
            sprintf(
531
                'The key [%s] does not have the expected data type [%s].',
532
                $keyName,
533
                $expectedType
534
            ),
535
            self::ERROR_INVALID_SERIALIZED_DATA_TYPE
536
        );
537
    }
538
}
539