Passed
Push — master ( 28c60c...6c6d7e )
by Marwan
10:15
created

App::abort()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 49
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 38
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 49
ccs 0
cts 32
cp 0
crap 6
rs 9.312
1
<?php
2
3
/**
4
 * @author Marwan Al-Soltany <[email protected]>
5
 * @copyright Marwan Al-Soltany 2021
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace MAKS\Velox;
13
14
use MAKS\Velox\Backend\Event;
15
use MAKS\Velox\Backend\Config;
16
use MAKS\Velox\Backend\Router;
17
use MAKS\Velox\Backend\Globals;
18
use MAKS\Velox\Frontend\Data;
19
use MAKS\Velox\Frontend\View;
20
use MAKS\Velox\Frontend\HTML;
21
use MAKS\Velox\Frontend\Path;
22
use MAKS\Velox\Helper\Dumper;
23
use MAKS\Velox\Helper\Misc;
24
25
/**
26
 * A class that serves as a basic service-container for VELOX.
27
 * This class has all VELOX classes as public properties:
28
 *      - `$event`   = `Event::class`
29
 *      - `$config`  = `Config::class`
30
 *      - `$router`  = `Router::class`
31
 *      - `$globals` = `Globals::class`
32
 *      - `$data`    = `Data::class`
33
 *      - `$view`    = `View::class`
34
 *      - `$html`    = `HTML::class`
35
 *      - `$path`    = `Path::class`
36
 *      - `$dumper`  = `Dumper::class`
37
 *      - `$misc`    = `Misc::class`
38
 *
39
 * Example:
40
 * ```
41
 * // create an instance
42
 * $app = new App();
43
 * // get an instance of the `Router` class via public property access notation
44
 * $app->router->handle('/dump', 'dd');
45
 * // or via calling a method with the same name
46
 * $app->router()->handle('/dump', 'dd');
47
 * ```
48
 *
49
 * @method static void handleException(\Throwable $expression) This function is available only at shutdown.
50
 * @method static void handleError(int $code, string $message, string $file, int $line) This function is available only at shutdown.
51
 *
52
 * @since 1.0.0
53
 */
54
class App
55
{
56
    public Event $event;
57
58
    public Config $config;
59
60
    public Router $router;
61
62
    public Globals $globals;
63
64
    public Data $data;
65
66
    public View $view;
67
68
    public HTML $html;
69
70
    public Path $path;
71
72
    public Dumper $dumper;
73
74
    public Misc $misc;
75
76
    protected array $methods;
77
78
    protected static array $staticMethods;
79
80
81
    /**
82
     * Class constructor.
83
     */
84 7
    public function __construct()
85
    {
86 7
        $this->event   = new Event();
87 7
        $this->config  = new Config();
88 7
        $this->router  = new Router();
89 7
        $this->globals = new Globals();
90 7
        $this->data    = new Data();
91 7
        $this->view    = new View();
92 7
        $this->html    = new HTML();
93 7
        $this->path    = new Path();
94 7
        $this->dumper  = new Dumper();
95 7
        $this->misc    = new Misc();
96 7
        $this->methods = [];
97 7
    }
98
99 2
    public function __get(string $property)
100
    {
101 2
        $class = static::class;
102
103 2
        throw new \Exception("Call to undefined property {$class}::${$property}");
104
    }
105
106 3
    public function __call(string $method, array $arguments)
107
    {
108 3
        $class = static::class;
109
110
        try {
111 3
            return isset($this->methods[$method]) ? $this->methods[$method](...$arguments) : $this->{$method};
112 1
        } catch (\Exception $error) {
113 1
            throw new \Exception(
114 1
                "Call to undefined method {$class}::{$method}()",
115 1
                (int)$error->getCode(),
116
                $error
117
            );
118
        }
119
    }
120
121 2
    public static function __callStatic(string $method, array $arguments)
122
    {
123 2
        $class = static::class;
124
125 2
        if (!isset(static::$staticMethods[$method])) {
126 1
            throw new \Exception("Call to undefined static method {$class}::{$method}()");
127
        }
128
129 1
        return static::$staticMethods[$method](...$arguments);
130
    }
131
132
133
    /**
134
     * Extends the class using the passed callback.
135
     *
136
     * @param string $name Method name.
137
     * @param callable $callback The callback to use as method body.
138
     *
139
     * @return callable The created bound closure.
140
     */
141 1
    public function extend(string $name, callable $callback): callable
142
    {
143 1
        $method = \Closure::fromCallable($callback);
144 1
        $method = \Closure::bind($method, $this, $this);
145
146 1
        return $this->methods[$name] = $method;
147
    }
148
149
    /**
150
     * Extends the class using the passed callback.
151
     *
152
     * @param string $name Method name.
153
     * @param callable $callback The callback to use as method body.
154
     *
155
     * @return callable The created closure.
156
     */
157 1
    public static function extendStatic(string $name, callable $callback): callable
158
    {
159 1
        $method = \Closure::fromCallable($callback);
160 1
        $method = \Closure::bind($method, null, static::class);
161
162 1
        return static::$staticMethods[$name] = $method;
163
    }
164
165
    /**
166
     * Logs a message to a file and generates it if it does not exist.
167
     *
168
     * @param string $message The message wished to be logged.
169
     * @param array|null $context An associative array of values where array key = {key} in the message (context).
170
     * @param string|null $filename [optional] The name wished to be given to the file. If not provided `{global.logging.defaultFilename}` will be used instead.
171
     * @param string|null $directory [optional] The directory where the log file should be written. If not provided `{global.logging.defaultDirectory}` will be used instead.
172
     *
173
     * @return bool True on success (if the message was written).
174
     */
175 17
    public static function log(string $message, ?array $context = [], ?string $filename = null, ?string $directory = null): bool
176
    {
177 17
        if (!Config::get('global.logging.enabled', true)) {
178 1
            return true;
179
        }
180
181 17
        $hasPassed = false;
182
183 17
        if (!$filename) {
184 1
            $filename = Config::get('global.logging.defaultFilename', sprintf('autogenerated-%s', date('Ymd')));
185
        }
186
187 17
        if (!$directory) {
188 17
            $directory = Config::get('global.logging.defaultDirectory', BASE_PATH);
189
        }
190
191 17
        $file = Path::normalize($directory, $filename, '.log');
192
193 17
        if (!file_exists($directory)) {
194 1
            mkdir($directory, 0744, true);
195
        }
196
197
        // create log file if it does not exist
198 17
        if (!is_file($file) && is_writable($directory)) {
199 2
            $signature = 'Created by ' . __METHOD__ . date('() \o\\n l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL . PHP_EOL;
200 2
            file_put_contents($file, $signature, 0);
201 2
            chmod($file, 0775);
202
        }
203
204
        // write in the log file
205 17
        if (is_writable($file)) {
206 17
            clearstatcache(true, $file);
207
            // empty the file if it exceeds the configured file size
208 17
            $maxFileSize = Config::get('global.logging.maxFileSize', 6.4e+7);
209 17
            if (filesize($file) > $maxFileSize) {
210 1
                $stream = fopen($file, 'r');
211 1
                if (is_resource($stream)) {
212 1
                    $signature = fgets($stream) . 'For exceeding the configured {global.logging.maxFileSize}, it was overwritten on ' . date('l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL . PHP_EOL;
213 1
                    fclose($stream);
214 1
                    file_put_contents($file, $signature, 0);
215 1
                    chmod($file, 0775);
216
                }
217
            }
218
219 17
            $timestamp = (new \DateTime())->format(DATE_ISO8601);
220 17
            $message   = Misc::interpolate($message, $context ?? []);
221
222 17
            $log = "$timestamp\t$message\n";
223
224 17
            $stream = fopen($file, 'a+');
225 17
            if (is_resource($stream)) {
226 17
                fwrite($stream, $log);
227 17
                fclose($stream);
228 17
                $hasPassed = true;
229
            }
230
        }
231
232 17
        return $hasPassed;
233
    }
234
235
    /**
236
     * Aborts the current request and sends a response with the specified HTTP status code, title, and message.
237
     * An HTML page will be rendered with the specified title and message.
238
     * The title for the most comment HTTP status codes (`200`, `403`, `404`, `405`, `500`, `503`) is already configured.
239
     *
240
     * @param int $code The HTTP status code.
241
     * @param string|null $title [optional] The title of the HTML page.
242
     * @param string|null $message [optional] The message of the HTML page.
243
     *
244
     * @return void
245
     *
246
     * @since 1.2.5
247
     */
248
    public static function abort(int $code, ?string $title = null, ?string $message = null): void
249
    {
250
        $http = [
251
            200 => 'OK',
252
            403 => 'Forbidden',
253
            404 => 'Not Found',
254
            405 => 'Not Allowed',
255
            500 => 'Internal Server Error',
256
            503 => 'Service Unavailable',
257
        ];
258
259
        // only keep the last buffer if nested
260
        while (ob_get_level() > 0) {
261
            ob_end_clean();
262
        }
263
264
        http_response_code($code);
265
266
        $title    = htmlspecialchars($title ?? $code . ' ' . $http[$code] ?? '', ENT_QUOTES, 'UTF-8');
267
        $message  = htmlspecialchars($message ?? '', ENT_QUOTES, 'UTF-8');
268
269
        (new HTML(false))
270
            ->node('<!DOCTYPE html>')
271
            ->open('html', ['lang' => 'en'])
272
                ->open('head')
273
                    ->title((string)$code)
274
                    ->link(null, [
275
                        'href' => 'https://cdn.jsdelivr.net/npm/bulma@latest/css/bulma.min.css',
276
                        'rel' => 'stylesheet'
277
                    ])
278
                ->close()
279
                ->open('body')
280
                    ->open('section', ['class' => 'section is-large has-text-centered'])
281
                        ->hr(null)
282
                        ->h1($title, ['class' => 'title is-1 is-spaced has-text-danger'])
283
                        ->condition(strlen($message))
284
                        ->h4($message, ['class' => 'subtitle'])
285
                        ->hr(null)
286
                        ->a('Reload', ['class' => 'button is-warning is-light', 'href' => 'javascript:location.reload();'])
287
                        ->entity('nbsp')
288
                        ->entity('nbsp')
289
                        ->a('Home', ['class' => 'button is-success is-light', 'href' => '/'])
290
                        ->hr(null)
291
                    ->close()
292
                ->close()
293
            ->close()
294
        ->echo();
295
296
        static::terminate();
297
    }
298
299
300
    /**
301
     * Terminates (exits) the PHP script.
302
     * This function is used instead of PHP `exit` to allow for testing `exit` without breaking the unit tests.
303
     *
304
     * @param int|string|null $status The exit status code/message.
305
     *
306
     * @return void This function never returns. It will terminate the script.
307
     * @throws \Exception If `UNIT_TESTING` is defined and truthy.
308
     *
309
     * @since 1.2.5
310
     */
311
    public static function terminate($status = null): void
312
    {
313
        if (defined('UNIT_TESTING') && UNIT_TESTING) {
314
            throw new \Exception(empty($status) ? 'Exit' : 'Exit: ' . $status);
315
        }
316
317
        exit($status); // @codeCoverageIgnore
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...
318
    }
319
}
320