Completed
Push — master ( 3fe54f...10b459 )
by Basil
02:11
created

ErrorHandlerTrait   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 11
dl 0
loc 273
rs 8.96
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A transferMessage() 0 8 1
A isExceptionWhitelisted() 0 12 3
A apiServerSendData() 0 12 1
A renderException() 0 8 3
F getExceptionArray() 0 67 19
A buildTrace() 0 9 2
C buildTraceItem() 0 54 14

How to fix   Complexity   

Complex Class

Complex classes like ErrorHandlerTrait 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

1
<?php
2
3
namespace luya\traits;
4
5
use Yii;
6
use yii\helpers\Json;
7
use Curl\Curl;
8
use luya\helpers\Url;
9
use luya\helpers\ObjectHelper;
10
use luya\helpers\ArrayHelper;
11
use yii\web\Application;
12
use yii\web\HttpException;
13
use yii\base\Exception;
14
use yii\base\ErrorException;
15
use luya\Boot;
16
use luya\exceptions\WhitelistedException;
17
18
/**
19
 * ErrorHandler trait to extend the renderException method with an api call if enabled.
20
 *
21
 * @author Basil Suter <[email protected]>
22
 * @since 1.0.0
23
 */
24
trait ErrorHandlerTrait
25
{
26
    /**
27
     * @var string The url of the error api without trailing slash. Make sure you have installed the error api
28
     * module on the requested api url (https://luya.io/guide/module/luyadev---luya-module-errorapi).
29
     *
30
     * An example when using the erroapi module, the url could look like this `https://luya.io/errorapi`.
31
     */
32
    public $api;
33
34
    /**
35
     * @var boolean Enable the transfer of exceptions to the defined `$api` server.
36
     */
37
    public $transferException = false;
38
39
    /**
40
     * @var \Curl\Curl|null The curl object from the last error api call.
41
     * @since 1.0.5
42
     */
43
    public $lastTransferCall;
44
    
45
    /**
46
     * @var array An array of exceptions which are whitelisted and therefore NOT TRANSFERED. Whitelisted exception
47
     * are basically expected application logic which does not need to report informations to the developer, as the
48
     * application works as expect.
49
     * @since 1.0.5
50
     */
51
    public $whitelist = [
52
        'yii\base\InvalidRouteException',
53
        'yii\web\NotFoundHttpException',
54
        'yii\web\ForbiddenHttpException',
55
        'yii\web\MethodNotAllowedHttpException',
56
        'yii\web\UnauthorizedHttpException',
57
        'yii\web\BadRequestHttpException',
58
    ];
59
    
60
    /**
61
     * @var array
62
     * @since 1.0.6
63
     */
64
    public $sensitiveKeys = ['password', 'pwd', 'pass', 'passwort', 'pw', 'token', 'hash', 'authorization'];
65
    
66
    /**
67
     * Send a custom message to the api server event its not related to an exception.
68
     *
69
     * Sometimes you just want to pass informations from your application, this method allows you to transfer
70
     * a message to the error api server.
71
     *
72
     * Example of sending a message
73
     *
74
     * ```php
75
     * Yii::$app->errorHandler->transferMessage('Something went wrong here!', __FILE__, __LINE__);
76
     * ```
77
     *
78
     * @param string $message The message you want to send to the error api server.
79
     * @param string $file The you are currently send the message (use __FILE__)
80
     * @param string $line The line you want to submit (use __LINE__)
81
     * @return bool|null
82
     */
83
    public function transferMessage($message, $file = __FILE__, $line = __LINE__)
84
    {
85
        return $this->apiServerSendData($this->getExceptionArray([
86
            'message' => $message,
87
            'file' => $file,
88
            'line' => $line,
89
        ]));
90
    }
91
92
    /**
93
     * Returns whether a given exception is whitelisted or not.
94
     * 
95
     * If an exception is whitelisted or in the liste of whitelisted exception
96
     * the exception content won't be transmitted to the error api.
97
     *
98
     * @param mixed $exception
99
     * @return boolean Returns true if whitelisted, or false if not and can therefore be transmitted.
100
     * @since 1.0.21
101
     */
102
    public function isExceptionWhitelisted($exception)
103
    {
104
        if (!is_object($exception)) {
105
            return false;
106
        }
107
108
        if (ObjectHelper::isInstanceOf($exception, WhitelistedException::class, false)) {
109
            return true;
110
        }
111
112
        return ObjectHelper::isInstanceOf($exception, $this->whitelist, false);
113
    }
114
    
115
    /**
116
     * Send the array data to the api server.
117
     *
118
     * @param array $data The array to be sent to the server.
119
     * @return boolean|null true/false if data has been sent to the api successfull or not, null if the transfer is disabled.
120
     */
121
    private function apiServerSendData(array $data)
122
    {
123
        $curl = new Curl();
124
        $curl->setOpt(CURLOPT_CONNECTTIMEOUT, 2);
125
        $curl->setOpt(CURLOPT_TIMEOUT, 2);
126
        $curl->post(Url::ensureHttp(rtrim($this->api, '/')).'/create', [
127
            'error_json' => Json::encode($data),
128
        ]);
129
        $this->lastTransferCall = $curl;
130
        
131
        return $curl->isSuccess();
132
    }
133
    
134
    /**
135
     * @inheritdoc
136
     */
137
    public function renderException($exception)
138
    {
139
        if ($this->transferException && !$this->isExceptionWhitelisted($exception)) {
140
            $this->apiServerSendData($this->getExceptionArray($exception));
141
        }
142
        
143
        return parent::renderException($exception);
144
    }
145
146
    /**
147
     * Get an readable array to transfer from an exception
148
     *
149
     * @param mixed $exception Exception object
150
     * @return array An array with transformed exception data
151
     */
152
    public function getExceptionArray($exception)
153
    {
154
        $_message = 'Uknonwn exception object, not instance of \Exception.';
155
        $_file = 'unknown';
156
        $_line = 0;
157
        $_trace = [];
158
        $_previousException = [];
159
        $_exceptionClassName = 'Unknown';
160
        
161
        if (is_object($exception)) {
162
            $_exceptionClassName = get_class($exception);
163
            $prev = $exception->getPrevious();
164
            
165
            if (!empty($prev)) {
166
                $_previousException = [
167
                    'message' => $prev->getMessage(),
168
                    'file' => $prev->getFile(),
169
                    'line' => $prev->getLine(),
170
                    'trace' => $this->buildTrace($prev),
171
                ];
172
            }
173
            
174
            $_trace = $this->buildTrace($exception);
175
            $_message = $exception->getMessage();
176
            $_file = $exception->getFile();
177
            $_line = $exception->getLine();
178
        } elseif (is_string($exception)) {
179
            $_message = 'exception string: ' . $exception;
180
        } elseif (is_array($exception)) {
181
            $_message = isset($exception['message']) ? $exception['message'] : 'exception array dump: ' . print_r($exception, true);
182
            $_file = isset($exception['file']) ? $exception['file'] : __FILE__;
183
            $_line = isset($exception['line']) ? $exception['line'] : __LINE__;
184
        }
185
186
        $exceptionName = 'Exception';
187
188
        if ($exception instanceof Exception) {
189
            $exceptionName = $exception->getName();
190
        } elseif ($exception instanceof ErrorException) {
191
            $exceptionName = $exception->getName();
192
        }
193
194
        return [
195
            'message' => $_message,
196
            'file' => $_file,
197
            'line' => $_line,
198
            'requestUri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null,
199
            'serverName' => isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null,
200
            'date' => date('d.m.Y H:i'),
201
            'trace' => $_trace,
202
            'previousException' => $_previousException,
203
            'ip' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null,
204
            'get' => isset($_GET) ? ArrayHelper::coverSensitiveValues($_GET, $this->sensitiveKeys) : [],
205
            'post' => isset($_POST) ? ArrayHelper::coverSensitiveValues($_POST, $this->sensitiveKeys) : [],
206
            'bodyParams' => Yii::$app instanceof Application ? ArrayHelper::coverSensitiveValues(Yii::$app->request->bodyParams) : [],
207
            'session' => isset($_SESSION) ? ArrayHelper::coverSensitiveValues($_SESSION, $this->sensitiveKeys) : [],
208
            'server' => isset($_SERVER) ? ArrayHelper::coverSensitiveValues($_SERVER, $this->sensitiveKeys) : [],
209
            // since 1.0.20
210
            'yii_debug' => YII_DEBUG,
211
            'yii_env' => YII_ENV,
212
            'status_code' => $exception instanceof HttpException ? $exception->statusCode : 500,
213
            'exception_name' => $exceptionName,
214
            'exception_class_name' => $_exceptionClassName,
215
            'php_version' => phpversion(),
216
            'luya_version' => Boot::VERSION,
217
        ];
218
    }
219
    
220
    /**
221
     * Build trace array from exception.
222
     *
223
     * @param object $exception
224
     * @return array
225
     */
226
    private function buildTrace($exception)
227
    {
228
        $_trace = [];
229
        foreach ($exception->getTrace() as $key => $item) {
230
            $_trace[$key] = $this->buildTraceItem($item);
231
        }
232
        
233
        return $_trace;
234
    }
235
236
    /**
237
     * Build the array trace item with file context.
238
     *
239
     * @param array $item
240
     * @return array
241
     */
242
    private function buildTraceItem(array $item)
243
    {
244
        $file = isset($item['file']) ? $item['file'] : null;
245
        $line = isset($item['line']) ? $item['line'] : null;
246
        $contextLine = null;
247
        $preContext = [];
248
        $postContext = [];
249
250
        if (!empty($file)) {
251
            try {
252
                $lineInArray = $line -1;
253
                // load file from path
254
                $fileInfo = file($file, FILE_IGNORE_NEW_LINES);
255
                // load file if false from real path
256
                if (!$fileInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
257
                    $fileInfo = file(realpath($file), FILE_IGNORE_NEW_LINES);
258
                }
259
                if ($fileInfo && isset($fileInfo[$lineInArray])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
260
                    $contextLine = $fileInfo[$lineInArray];
261
                }
262
                if ($contextLine) {
263
                    for ($i = 1; $i <= 6; $i++) {
264
                        // pre context
265
                        if (isset($fileInfo[$lineInArray - $i])) {
266
                            $preContext[] = $fileInfo[$lineInArray - $i];
267
                        }
268
                        // post context
269
                        if (isset($fileInfo[$i + $lineInArray])) {
270
                            $postContext[] = $fileInfo[$i + $lineInArray];
271
                        }
272
                    }
273
                    $preContext = array_reverse($preContext);
274
                }
275
                unset($fileInfo);
276
            } catch (\Exception $e) {
277
                // catch if any file load error appears
278
            }
279
        }
280
281
        return array_filter([
282
            'file' => $file,
283
            'abs_path' => realpath($file),
284
            'line' => $line,
285
            'context_line' => $contextLine,
286
            'pre_context' => $preContext,
287
            'post_context' => $postContext,
288
            'function' => isset($item['function']) ? $item['function'] : null,
289
            'class' => isset($item['class']) ? $item['class'] : null,
290
            // currently arguments wont be transmited due to large amount of informations based on base object
291
            //'args' => isset($item['args']) ? ArrayHelper::coverSensitiveValues($item['args'], $this->sensitiveKeys) : [],
292
        ], function($value) {
293
            return !empty($value);
294
        });
295
    }
296
}
297