Passed
Push — master ( 57cfb9...a2aa4e )
by Marwan
10:09
created

App::instance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

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