Passed
Push — master ( 5153e5...af085a )
by Sebastian
05:42 queued 30s
created

ConvertHelper_ThrowableInfo   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 47
eloc 137
c 5
b 0
f 0
dl 0
loc 468
rs 8.64

28 Methods

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

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