Passed
Push — master ( 2a966a...079cce )
by y
01:17
created

Site::_onException()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 6
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Helix;
4
5
use ErrorException;
6
use Helix\Site\Auth;
7
use Helix\Site\Error;
8
use Helix\Site\Request;
9
use Helix\Site\Response;
10
use Throwable;
11
12
/**
13
 * Routing and error handling.
14
 */
15
class Site {
16
17
    /**
18
     * @var Auth
19
     */
20
    protected $auth;
21
22
    /**
23
     * @var bool
24
     */
25
    protected $dev = false;
26
27
    /**
28
     * @var Request
29
     */
30
    protected $request;
31
32
    /**
33
     * @var Response
34
     */
35
    protected $response;
36
37
    /**
38
     * Initializes the system for routing, error handling, and output.
39
     */
40
    public function __construct () {
41
        error_reporting(E_ALL);
42
        set_error_handler([$this, '_onRaise']);
43
        $this->request = new Request();
44
        $this->response = new Response($this);
45
        $this->setDev(php_sapi_name() === 'cli-server');
46
        set_exception_handler([$this, '_onException']);
47
        error_reporting(E_RECOVERABLE_ERROR | E_WARNING | E_USER_ERROR | E_USER_WARNING);
48
    }
49
50
    /**
51
     * Handles uncaught exceptions and exits.
52
     *
53
     * @param Throwable $error
54
     * @return void
55
     */
56
    public function _onException (Throwable $error): void {
57
        if (!$error instanceof Error) {
58
            $this->log(500, "[{$error->getCode()}] {$error}");
59
        }
60
        elseif ($error->getCode() >= 500) {
61
            $this->log($error->getCode(), $error);
62
        }
63
        $this->response->error($error) and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
64
    }
65
66
    /**
67
     * Handles raised PHP errors by throwing or logging them,
68
     * depending on whether they're in `error_reporting()`
69
     *
70
     * @param int $code
71
     * @param string $message
72
     * @param string $file
73
     * @param int $line
74
     * @throws ErrorException
75
     */
76
    public function _onRaise (int $code, string $message, string $file, int $line) {
77
        $type = [
78
            E_DEPRECATED => 'E_DEPRECATED',
79
            E_NOTICE => 'E_NOTICE',
80
            E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
81
            E_WARNING => 'E_WARNING',
82
            E_USER_ERROR => 'E_USER_ERROR',
83
            E_USER_DEPRECATED => 'E_USER_DEPRECATED',
84
            E_USER_NOTICE => 'E_USER_NOTICE',
85
            E_USER_WARNING => 'E_USER_WARNING',
86
        ][$code];
87
        if (error_reporting() & $code) {
88
            throw new ErrorException("{$type}: {$message}", $code, 1, $file, $line);
89
        }
90
        $this->log($type, "{$message} in {$file}:{$line}");
91
    }
92
93
    /**
94
     * Routes `DELETE`
95
     *
96
     * @param string $path
97
     * @param callable $controller
98
     * @return void
99
     */
100
    public function delete (string $path, callable $controller): void {
101
        $this->route(['DELETE'], $path, $controller);
102
    }
103
104
    /**
105
     * Routes `GET` and `HEAD`
106
     *
107
     * @param string $path
108
     * @param callable $controller
109
     * @return void
110
     */
111
    public function get (string $path, callable $controller): void {
112
        $this->route(['GET', 'HEAD'], $path, $controller);
113
    }
114
115
    /**
116
     * @return Auth
117
     */
118
    final public function getAuth () {
119
        return $this->auth ?? $this->auth = new Auth($this);
120
    }
121
122
    /**
123
     * @return Request
124
     */
125
    final public function getRequest () {
126
        return $this->request;
127
    }
128
129
    /**
130
     * @return Response
131
     */
132
    final public function getResponse () {
133
        return $this->response;
134
    }
135
136
    /**
137
     * @return bool
138
     */
139
    final public function isDev (): bool {
140
        return $this->dev;
141
    }
142
143
    /**
144
     * @param mixed $code
145
     * @param string $message
146
     * @return $this
147
     */
148
    public function log ($code, string $message) {
149
        $now = date('Y-m-d H:i:s');
150
        $id = $this->response->getId();
151
        $ip = $this->request->getClient();
152
        $method = $this->request->getMethod();
153
        $path = $this->request->getPath();
154
        $line = "{$now} {$code} {$id} {$ip} {$method} {$path} - {$message}\n\n";
155
        error_log($line, 3, 'error.log');
156
        return $this;
157
    }
158
159
    /**
160
     * Routes `POST`
161
     *
162
     * @param string $path
163
     * @param callable $controller
164
     */
165
    public function post (string $path, callable $controller): void {
166
        $this->route(['POST'], $path, $controller);
167
    }
168
169
    /**
170
     * Routes `PUT`
171
     *
172
     * @param string $path
173
     * @param callable $controller
174
     * @return void
175
     */
176
    public function put (string $path, callable $controller): void {
177
        $this->route(['PUT'], $path, $controller);
178
    }
179
180
    /**
181
     * Invokes a controller if the HTTP method and path match, and exits.
182
     * Absolute paths must start with `/`, all other paths are treated as regular expressions.
183
     *
184
     * @param string[] $methods
185
     * @param string $path
186
     * @param callable $controller `(string[] $match, Site $site):mixed`
187
     * @return void
188
     */
189
    protected function route (array $methods, string $path, callable $controller): void {
190
        $match = [];
191
        if ($path[0] !== '/') {
192
            preg_match($path, $this->request->getPath(), $match);
193
        }
194
        elseif ($path === $this->request->getPath()) {
195
            $match = [$path];
196
        }
197
        if ($match) {
198
            if (in_array($this->request->getMethod(), $methods)) {
199
                $this->response->setCode(200);
200
                $content = call_user_func($controller, $match, $this);
201
                $this->response->mixed($content) and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
202
            }
203
            $this->response->setCode(405);
204
        }
205
    }
206
207
    /**
208
     * @param bool $dev
209
     * @return $this
210
     */
211
    public function setDev (bool $dev) {
212
        $this->dev = $dev;
213
        return $this;
214
    }
215
}