Completed
Branch 6.0 (d30585)
by yun
04:17
created

Handle   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 308
rs 8.8798
c 0
b 0
f 0
ccs 0
cts 122
cp 0
wmc 44
lcom 1
cbo 11

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A report() 0 29 5
A isIgnoreReport() 0 10 3
A render() 0 11 3
A renderForConsole() 0 8 2
A renderHttpException() 0 11 3
B convertExceptionToArray() 0 48 5
A convertExceptionToResponse() 0 15 3
A renderExceptionContent() 0 9 2
A getCode() 0 10 3
B getMessage() 0 22 7
A getSourceCode() 0 18 4
A getExtendData() 0 10 2
A getConst() 0 6 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
// +----------------------------------------------------------------------
3
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
6
// +----------------------------------------------------------------------
7
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
8
// +----------------------------------------------------------------------
9
// | Author: yunwuxin <[email protected]>
10
// +----------------------------------------------------------------------
11
declare (strict_types = 1);
12
13
namespace think\exception;
14
15
use Exception;
16
use think\App;
17
use think\console\Output;
18
use think\db\exception\DataNotFoundException;
19
use think\db\exception\ModelNotFoundException;
20
use think\Request;
21
use think\Response;
22
use Throwable;
23
24
/**
25
 * 系统异常处理类
26
 */
27
class Handle
28
{
29
    /** @var App */
30
    protected $app;
31
32
    protected $ignoreReport = [
33
        HttpException::class,
34
        HttpResponseException::class,
35
        ModelNotFoundException::class,
36
        DataNotFoundException::class,
37
        ValidateException::class,
38
    ];
39
40
    protected $isJson = false;
41
42
    public function __construct(App $app)
43
    {
44
        $this->app = $app;
45
    }
46
47
    /**
48
     * Report or log an exception.
49
     *
50
     * @access public
51
     * @param Throwable $exception
52
     * @return void
53
     */
54
    public function report(Throwable $exception): void
55
    {
56
        if (!$this->isIgnoreReport($exception)) {
57
            // 收集异常数据
58
            if ($this->app->isDebug()) {
59
                $data = [
60
                    'file'    => $exception->getFile(),
61
                    'line'    => $exception->getLine(),
62
                    'message' => $this->getMessage($exception),
63
                    'code'    => $this->getCode($exception),
64
                ];
65
                $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]";
66
            } else {
67
                $data = [
68
                    'code'    => $this->getCode($exception),
69
                    'message' => $this->getMessage($exception),
70
                ];
71
                $log = "[{$data['code']}]{$data['message']}";
72
            }
73
74
            if ($this->app->config->get('log.record_trace')) {
75
                $log .= PHP_EOL . $exception->getTraceAsString();
76
            }
77
78
            try {
79
                $this->app->log->record($log, 'error');
80
            } catch (Exception $e){}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
81
        }
82
    }
83
84
    protected function isIgnoreReport(Throwable $exception): bool
85
    {
86
        foreach ($this->ignoreReport as $class) {
87
            if ($exception instanceof $class) {
88
                return true;
89
            }
90
        }
91
92
        return false;
93
    }
94
95
    /**
96
     * Render an exception into an HTTP response.
97
     *
98
     * @access public
99
     * @param Request   $request
100
     * @param Throwable $e
101
     * @return Response
102
     */
103
    public function render($request, Throwable $e): Response
104
    {
105
        $this->isJson = $request->isJson();
106
        if ($e instanceof HttpResponseException) {
107
            return $e->getResponse();
108
        } elseif ($e instanceof HttpException) {
109
            return $this->renderHttpException($e);
110
        } else {
111
            return $this->convertExceptionToResponse($e);
112
        }
113
    }
114
115
    /**
116
     * @access public
117
     * @param Output    $output
118
     * @param Throwable $e
119
     */
120
    public function renderForConsole(Output $output, Throwable $e): void
121
    {
122
        if ($this->app->isDebug()) {
123
            $output->setVerbosity(Output::VERBOSITY_DEBUG);
124
        }
125
126
        $output->renderException($e);
127
    }
128
129
    /**
130
     * @access protected
131
     * @param HttpException $e
132
     * @return Response
133
     */
134
    protected function renderHttpException(HttpException $e): Response
135
    {
136
        $status   = $e->getStatusCode();
137
        $template = $this->app->config->get('app.http_exception_template');
138
139
        if (!$this->app->isDebug() && !empty($template[$status])) {
140
            return Response::create($template[$status], 'view', $status)->assign(['e' => $e]);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class think\Response as the method assign() does only exist in the following sub-classes of think\Response: think\response\View. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
141
        } else {
142
            return $this->convertExceptionToResponse($e);
143
        }
144
    }
145
146
    /**
147
     * 收集异常数据
148
     * @param Throwable $exception
149
     * @return array
150
     */
151
    protected function convertExceptionToArray(Throwable $exception): array
152
    {
153
        if ($this->app->isDebug()) {
154
            // 调试模式,获取详细的错误信息
155
            $traces = [];
156
            $nextException = $exception;
157
            do {
158
                $traces[] = [
159
                    'name'    => get_class($nextException),
160
                    'file'    => $nextException->getFile(),
161
                    'line'    => $nextException->getLine(),
162
                    'code'    => $this->getCode($nextException),
163
                    'message' => $this->getMessage($nextException),
164
                    'trace'   => $nextException->getTrace(),
165
                    'source'  => $this->getSourceCode($nextException),
166
                ];
167
            } while ($nextException = $nextException->getPrevious());
168
            $data = [
169
                'code'    => $this->getCode($exception),
170
                'message' => $this->getMessage($exception),
171
                'traces'  => $traces,
172
                'datas'   => $this->getExtendData($exception),
173
                'tables'  => [
174
                    'GET Data'              => $this->app->request->get(),
175
                    'POST Data'             => $this->app->request->post(),
176
                    'Files'                 => $this->app->request->file(),
177
                    'Cookies'               => $this->app->request->cookie(),
178
                    'Session'               => $this->app->exists('session') ? $this->app->session->all() : [],
0 ignored issues
show
Documentation Bug introduced by
The method all does not exist on object<think\Session>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
179
                    'Server/Request Data'   => $this->app->request->server(),
180
                    'Environment Variables' => $this->app->request->env(),
181
                    'ThinkPHP Constants'    => $this->getConst(),
182
                ],
183
            ];
184
        } else {
185
            // 部署模式仅显示 Code 和 Message
186
            $data = [
187
                'code'    => $this->getCode($exception),
188
                'message' => $this->getMessage($exception),
189
            ];
190
191
            if (!$this->app->config->get('app.show_error_msg')) {
192
                // 不显示详细错误信息
193
                $data['message'] = $this->app->config->get('app.error_message');
194
            }
195
        }
196
197
        return $data;
198
    }
199
200
    /**
201
     * @access protected
202
     * @param Throwable $exception
203
     * @return Response
204
     */
205
    protected function convertExceptionToResponse(Throwable $exception): Response
206
    {
207
        if (!$this->isJson) {
208
            $response = Response::create($this->renderExceptionContent($exception));
209
        } else {
210
            $response = Response::create($this->convertExceptionToArray($exception), 'json');
211
        }
212
213
        if ($exception instanceof HttpException) {
214
            $statusCode = $exception->getStatusCode();
215
            $response->header($exception->getHeaders());
216
        }
217
218
        return $response->code($statusCode ?? 500);
219
    }
220
221
    protected function renderExceptionContent(Throwable $exception): string
222
    {
223
        ob_start();
224
        $data = $this->convertExceptionToArray($exception);
225
        extract($data);
226
        include $this->app->config->get('app.exception_tmpl') ?: __DIR__ . '/../../tpl/think_exception.tpl';
227
228
        return ob_get_clean();
229
    }
230
231
    /**
232
     * 获取错误编码
233
     * ErrorException则使用错误级别作为错误编码
234
     * @access protected
235
     * @param Throwable $exception
236
     * @return integer                错误编码
237
     */
238
    protected function getCode(Throwable $exception)
239
    {
240
        $code = $exception->getCode();
241
242
        if (!$code && $exception instanceof ErrorException) {
243
            $code = $exception->getSeverity();
244
        }
245
246
        return $code;
247
    }
248
249
    /**
250
     * 获取错误信息
251
     * ErrorException则使用错误级别作为错误编码
252
     * @access protected
253
     * @param Throwable $exception
254
     * @return string                错误信息
255
     */
256
    protected function getMessage(Throwable $exception): string
257
    {
258
        $message = $exception->getMessage();
259
260
        if ($this->app->runningInConsole()) {
261
            return $message;
262
        }
263
264
        $lang = $this->app->lang;
265
266
        if (strpos($message, ':')) {
267
            $name    = strstr($message, ':', true);
268
            $message = $lang->has($name) ? $lang->get($name) . strstr($message, ':') : $message;
269
        } elseif (strpos($message, ',')) {
270
            $name    = strstr($message, ',', true);
271
            $message = $lang->has($name) ? $lang->get($name) . ':' . substr(strstr($message, ','), 1) : $message;
272
        } elseif ($lang->has($message)) {
273
            $message = $lang->get($message);
274
        }
275
276
        return $message;
277
    }
278
279
    /**
280
     * 获取出错文件内容
281
     * 获取错误的前9行和后9行
282
     * @access protected
283
     * @param Throwable $exception
284
     * @return array                 错误文件内容
285
     */
286
    protected function getSourceCode(Throwable $exception): array
287
    {
288
        // 读取前9行和后9行
289
        $line  = $exception->getLine();
290
        $first = ($line - 9 > 0) ? $line - 9 : 1;
291
292
        try {
293
            $contents = file($exception->getFile()) ?: [];
294
            $source   = [
295
                'first'  => $first,
296
                'source' => array_slice($contents, $first - 1, 19),
297
            ];
298
        } catch (Exception $e) {
299
            $source = [];
300
        }
301
302
        return $source;
303
    }
304
305
    /**
306
     * 获取异常扩展信息
307
     * 用于非调试模式html返回类型显示
308
     * @access protected
309
     * @param Throwable $exception
310
     * @return array                 异常类定义的扩展数据
311
     */
312
    protected function getExtendData(Throwable $exception): array
313
    {
314
        $data = [];
315
316
        if ($exception instanceof \think\Exception) {
317
            $data = $exception->getData();
318
        }
319
320
        return $data;
321
    }
322
323
    /**
324
     * 获取常量列表
325
     * @access protected
326
     * @return array 常量列表
327
     */
328
    protected function getConst(): array
329
    {
330
        $const = get_defined_constants(true);
331
332
        return $const['user'] ?? [];
333
    }
334
}
335