Passed
Push — master ( 3ebf33...973b18 )
by Marwan
09:59
created

App::shutdown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 5
ccs 0
cts 0
cp 0
crap 2
rs 10
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\Backend\Session;
19
use MAKS\Velox\Backend\Database;
20
use MAKS\Velox\Backend\Auth;
21
use MAKS\Velox\Frontend\Data;
22
use MAKS\Velox\Frontend\View;
23
use MAKS\Velox\Frontend\HTML;
24
use MAKS\Velox\Frontend\Path;
25
use MAKS\Velox\Helper\Dumper;
26
use MAKS\Velox\Helper\Misc;
27
28
/**
29
 * A class that serves as a basic service-container for VELOX.
30
 * This class has most VELOX classes as public properties:
31
 * - `$auth`: Instance of the `Auth` class.
32
 * - `$event`: Instance of the `Event` class.
33
 * - `$config`: Instance of the `Config` class.
34
 * - `$router`: Instance of the `Router` class.
35
 * - `$globals`: Instance of the `Globals` class.
36
 * - `$session`: Instance of the `Session` class.
37
 * - `$database`: Instance of the `Database` class.
38
 * - `$data`: Instance of the `Data` class.
39
 * - `$view`: Instance of the `View` class.
40
 * - `$html`: Instance of the `HTML` class.
41
 * - `$path`: Instance of the `Path` class.
42
 * - `$dumper`: Instance of the `Dumper` class.
43
 * - `$misc`: Instance of the `Misc` class.
44
 *
45
 * Example:
46
 * ```
47
 * // create an instance
48
 * $app = new App();
49
 * // get an instance of the `Config` class via public property access notation
50
 * $app->config;
51
 * // or via calling a method with the same name
52
 * $app->config()->get('global');
53
 * ```
54
 *
55
 * @package Velox
56
 * @since 1.0.0
57
 * @api
58
 *
59
 * @method static void handleException(\Throwable $expression) This method is available only at shutdown.
60
 * @method static void handleError(int $code, string $message, string $file, int $line) This method is available only at shutdown.
61
 */
62
class App
63
{
64
    /**
65
     * This event will be dispatched on app termination. Note that this event can be dispatched multiple times in app life-cycle.
66
     * This event will not be passed any arguments.
67
     *
68
     * @var string
69
     */
70
    public const ON_TERMINATE = 'app.on.terminate';
71
72
    /**
73
     * This event will be dispatched on app shutdown. Note that this event is dispatched only once in app life-cycle.
74
     * This event will not be passed any arguments.
75
     *
76
     * @var string
77
     */
78
    public const ON_SHUTDOWN = 'app.on.shutdown';
79
80
81
    public Event $event;
82
83
    public Config $config;
84
85
    public Router $router;
86
87
    public Globals $globals;
88
89
    public Session $session;
90
91
    public Database $database;
92
93
    public Auth $auth;
94
95
    public Data $data;
96
97
    public View $view;
98
99
    public HTML $html;
100
101
    public Path $path;
102
103
    public Dumper $dumper;
104
105
    public Misc $misc;
106
107
    protected array $methods;
108
109
    protected static array $staticMethods;
110
111
112
    /**
113
     * Class constructor.
114
     */
115 11
    public function __construct()
116
    {
117 11
        $this->event    = new Event();
118 11
        $this->config   = new Config();
119 11
        $this->router   = new Router();
120 11
        $this->globals  = new Globals();
121 11
        $this->session  = new Session();
122 11
        $this->database = Database::instance();
123 11
        $this->auth     = Auth::instance();
124 11
        $this->data     = new Data();
125 11
        $this->view     = new View();
126 11
        $this->html     = new HTML();
127 11
        $this->path     = new Path();
128 11
        $this->dumper   = new Dumper();
129 11
        $this->misc     = new Misc();
130 11
        $this->methods  = [];
131 11
    }
132
133 2
    public function __get(string $property)
134
    {
135 2
        $class = static::class;
136
137 2
        throw new \Exception("Call to undefined property {$class}::${$property}");
138
    }
139
140 3
    public function __call(string $method, array $arguments)
141
    {
142 3
        $class = static::class;
143
144
        try {
145 3
            return isset($this->methods[$method]) ? $this->methods[$method](...$arguments) : $this->{$method};
146 1
        } catch (\Exception $error) {
147 1
            throw new \Exception(
148 1
                "Call to undefined method {$class}::{$method}()",
149 1
                (int)$error->getCode(),
150
                $error
151
            );
152
        }
153
    }
154
155 2
    public static function __callStatic(string $method, array $arguments)
156
    {
157 2
        $class = static::class;
158
159 2
        if (!isset(static::$staticMethods[$method])) {
160 1
            throw new \Exception("Call to undefined static method {$class}::{$method}()");
161
        }
162
163 1
        return static::$staticMethods[$method](...$arguments);
164
    }
165
166
167
    /**
168
     * Returns the singleton instance of the `App` class.
169
     *
170
     * @return static
171
     *
172
     * @since 1.4.0
173
     */
174 3
    final public static function instance(): self
175
    {
176 3
        static $instance = null;
177
178 3
        if ($instance === null) {
179
            $instance = new static();
180
        }
181
182 3
        return $instance;
183
    }
184
185
    /**
186
     * Extends the class using the passed callback.
187
     *
188
     * @param string $name Method name.
189
     * @param callable $callback The callback to use as method body.
190
     *
191
     * @return callable The created bound closure.
192
     */
193 1
    public function extend(string $name, callable $callback): callable
194
    {
195 1
        $method = \Closure::fromCallable($callback);
196 1
        $method = \Closure::bind($method, $this, $this);
197
198 1
        return $this->methods[$name] = $method;
199
    }
200
201
    /**
202
     * Extends the class using the passed callback.
203
     *
204
     * @param string $name Method name.
205
     * @param callable $callback The callback to use as method body.
206
     *
207
     * @return callable The created closure.
208
     */
209 1
    public static function extendStatic(string $name, callable $callback): callable
210
    {
211 1
        $method = \Closure::fromCallable($callback);
212 1
        $method = \Closure::bind($method, null, static::class);
213
214 1
        return static::$staticMethods[$name] = $method;
215
    }
216
217
    /**
218
     * Logs a message to a file and generates it if it does not exist.
219
     *
220
     * @param string $message The message wished to be logged.
221
     * @param array|null $context An associative array of values where array key = {key} in the message (context).
222
     * @param string|null $filename [optional] The name wished to be given to the file. If not provided `{global.logging.defaultFilename}` will be used instead.
223
     * @param string|null $directory [optional] The directory where the log file should be written. If not provided `{global.logging.defaultDirectory}` will be used instead.
224
     *
225
     * @return bool True on success (if the message was written).
226
     */
227 28
    public static function log(string $message, ?array $context = [], ?string $filename = null, ?string $directory = null): bool
228
    {
229 28
        if (!Config::get('global.logging.enabled', true)) {
230 1
            return true;
231
        }
232
233 28
        $hasPassed = false;
234
235 28
        if (!$filename) {
236 1
            $filename = Config::get('global.logging.defaultFilename', sprintf('autogenerated-%s', date('Ymd')));
237
        }
238
239 28
        if (!$directory) {
240 28
            $directory = Config::get('global.logging.defaultDirectory', BASE_PATH);
241
        }
242
243 28
        $file = Path::normalize($directory, $filename, '.log');
244
245 28
        if (!file_exists($directory)) {
246 1
            mkdir($directory, 0744, true);
247
        }
248
249
        // create log file if it does not exist
250 28
        if (!is_file($file) && is_writable($directory)) {
251 2
            $signature = 'Created by ' . __METHOD__ . date('() \o\\n l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL . PHP_EOL;
252 2
            file_put_contents($file, $signature, 0);
253 2
            chmod($file, 0775);
254
        }
255
256
        // write in the log file
257 28
        if (is_writable($file)) {
258 28
            clearstatcache(true, $file);
259
            // empty the file if it exceeds the configured file size
260 28
            $maxFileSize = Config::get('global.logging.maxFileSize', 6.4e+7);
261 28
            if (filesize($file) > $maxFileSize) {
262 1
                $stream = fopen($file, 'r');
263 1
                if (is_resource($stream)) {
264 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;
265 1
                    fclose($stream);
266 1
                    file_put_contents($file, $signature, 0);
267 1
                    chmod($file, 0775);
268
                }
269
            }
270
271 28
            $timestamp = (new \DateTime())->format(DATE_ISO8601);
272 28
            $message   = Misc::interpolate($message, $context ?? []);
273
274 28
            $log = "$timestamp\t$message\n";
275
276 28
            $stream = fopen($file, 'a+');
277 28
            if (is_resource($stream)) {
278 28
                fwrite($stream, $log);
279 28
                fclose($stream);
280 28
                $hasPassed = true;
281
            }
282
        }
283
284 28
        return $hasPassed;
285
    }
286
287
    /**
288
     * Aborts the current request and sends a response with the specified HTTP status code, title, and message.
289
     * An HTML page will be rendered with the specified title and message.
290
     * If a view file for the error page is set using `{global.errorPages.CODE}`,
291
     * it will be rendered instead of the normal page and passed the `$code`, `$title`, and `$message` variables.
292
     * The title for the most common HTTP status codes (`200`, `401`, `403`, `404`, `405`, `500`, `503`) is already configured.
293
     *
294
     * @param int $code The HTTP status code.
295
     * @param string|null $title [optional] The title of the HTML page.
296
     * @param string|null $message [optional] The message of the HTML page.
297
     *
298
     * @return void
299
     *
300
     * @since 1.2.5
301
     */
302 4
    public static function abort(int $code, ?string $title = null, ?string $message = null): void
303
    {
304
        $http = [
305 4
            200 => 'OK',
306
            401 => 'Unauthorized',
307
            403 => 'Forbidden',
308
            404 => 'Not Found',
309
            405 => 'Not Allowed',
310
            500 => 'Internal Server Error',
311
            503 => 'Service Unavailable',
312
        ];
313
314 4
        $title    = htmlspecialchars($title ?? $code . ' ' . $http[$code] ?? '', ENT_QUOTES, 'UTF-8');
315 4
        $message  = htmlspecialchars($message ?? '', ENT_QUOTES, 'UTF-8');
316
317
        try {
318 4
            $html = View::render(Config::get("global.errorPages.{$code}"), compact('code', 'title', 'message'));
0 ignored issues
show
Bug introduced by
It seems like MAKS\Velox\Backend\Confi...bal.errorPages.'.$code) can also be of type null; however, parameter $page of MAKS\Velox\Frontend\View::render() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

318
            $html = View::render(/** @scrutinizer ignore-type */ Config::get("global.errorPages.{$code}"), compact('code', 'title', 'message'));
Loading history...
319 1
        } catch (\Throwable $e) {
320 1
            $html = (new HTML(false))
321 1
                ->node('<!DOCTYPE html>')
322 1
                ->open('html', ['lang' => 'en'])
323 1
                    ->open('head')
324 1
                        ->title((string)$code)
325 1
                        ->link(null, [
326 1
                            'href' => 'https://cdn.jsdelivr.net/npm/bulma@latest/css/bulma.min.css',
327
                            'rel' => 'stylesheet'
328
                        ])
329 1
                    ->close()
330 1
                    ->open('body')
331 1
                        ->open('section', ['class' => 'section is-large has-text-centered'])
332 1
                            ->hr(null)
333 1
                            ->h1($title, ['class' => 'title is-1 is-spaced has-text-danger'])
334 1
                            ->condition(strlen($message))
335 1
                            ->h4($message, ['class' => 'subtitle'])
336 1
                            ->hr(null)
337 1
                            ->a('Reload', ['class' => 'button is-warning is-light', 'href' => 'javascript:location.reload();'])
338 1
                            ->entity('nbsp')
339 1
                            ->entity('nbsp')
340 1
                            ->a('Home', ['class' => 'button is-success is-light', 'href' => '/'])
341 1
                            ->hr(null)
342 1
                        ->close()
343 1
                    ->close()
344 1
                ->close()
345 1
            ->return();
346
        } finally {
347 4
            http_response_code($code);
348 4
            echo $html;
349
350 4
            static::terminate();
351
        }
352
    }
353
354
355
    /**
356
     * Terminates (exits) the PHP script.
357
     * This function is used instead of PHP `exit` to allow for testing `exit` without breaking the unit tests.
358
     *
359
     * @param int|string|null $status The exit status code/message.
360
     * @param bool $noShutdown Whether to not execute the shutdown function or not.
361
     *
362
     * @return void This function never returns. It will terminate the script.
363
     * @throws \Exception If `EXIT_EXCEPTION` is defined and truthy.
364
     *
365
     * @since 1.2.5
366
     */
367 5
    public static function terminate($status = null, bool $noShutdown = true): void
368
    {
369 5
        Event::dispatch(self::ON_TERMINATE);
370
371 5
        if (defined('EXIT_EXCEPTION') && EXIT_EXCEPTION) {
372 5
            throw new \Exception(empty($status) ? 'Exit' : 'Exit: ' . $status);
373
        }
374
375
        // @codeCoverageIgnoreStart
376
        if ($noShutdown) {
377
            // app shutdown function checks for this variable
378
            // to determine if it should exit, see bootstrap/loader.php
379
            Misc::setArrayValueByKey($GLOBALS, '_VELOX.TERMINATE', true);
380
        }
381
382
        exit($status);
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...
383
        // @codeCoverageIgnoreEnd
384
    }
385
386
    /**
387
     * Shuts the app down by terminating it and executing shutdown function(s).
388
     * The triggered shutdown functions can be normal shutdown functions registered,
389
     * using `register_shutdown_function()` or the `self::ON_SHUTDOWN` event.
390
     *
391
     * @return void
392
     *
393
     * @since 1.4.2
394
     *
395
     * @codeCoverageIgnore
396
     */
397
    public static function shutdown(): void
398
    {
399
        Event::dispatch(self::ON_SHUTDOWN);
400
401
        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...
402
    }
403
}
404