App::log()   B
last analyzed

Complexity

Conditions 11
Paths 113

Size

Total Lines 58
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 11

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 11
eloc 33
c 3
b 0
f 0
nc 113
nop 4
dl 0
loc 58
ccs 33
cts 33
cp 1
crap 11
rs 7.2083

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Exception;
15
use MAKS\Velox\Backend\Event;
16
use MAKS\Velox\Backend\Config;
17
use MAKS\Velox\Backend\Router;
18
use MAKS\Velox\Backend\Globals;
19
use MAKS\Velox\Backend\Session;
20
use MAKS\Velox\Backend\Database;
21
use MAKS\Velox\Backend\Auth;
22
use MAKS\Velox\Frontend\Data;
23
use MAKS\Velox\Frontend\View;
24
use MAKS\Velox\Frontend\HTML;
25
use MAKS\Velox\Frontend\Path;
26
use MAKS\Velox\Helper\Dumper;
27
use MAKS\Velox\Helper\Misc;
28
29
/**
30
 * A class that serves as a basic service-container for VELOX.
31
 * This class has most VELOX classes as public properties:
32
 * - `$auth`: Instance of the `Auth` class.
33
 * - `$event`: Instance of the `Event` class.
34
 * - `$config`: Instance of the `Config` class.
35
 * - `$router`: Instance of the `Router` class.
36
 * - `$globals`: Instance of the `Globals` class.
37
 * - `$session`: Instance of the `Session` class.
38
 * - `$database`: Instance of the `Database` class.
39
 * - `$data`: Instance of the `Data` class.
40
 * - `$view`: Instance of the `View` class.
41
 * - `$html`: Instance of the `HTML` class.
42
 * - `$path`: Instance of the `Path` class.
43
 * - `$dumper`: Instance of the `Dumper` class.
44
 * - `$misc`: Instance of the `Misc` class.
45
 *
46
 * Example:
47
 * ```
48
 * // create an instance
49
 * $app = new App();
50
 * // get an instance of the `Config` class via public property access notation
51
 * $app->config;
52
 * // or via calling a method with the same name
53
 * $app->config()->get('global');
54
 * ```
55
 *
56
 * @package Velox
57
 * @since 1.0.0
58
 * @api
59
 *
60
 * @method static void handleException(\Throwable $expression) This method is available only at shutdown.
61
 * @method static void handleError(int $code, string $message, string $file, int $line) This method is available only at shutdown.
62
 */
63
class App
64
{
65
    /**
66
     * This event will be dispatched on app termination. Note that this event can be dispatched multiple times in app life-cycle.
67
     * This event will not be passed any arguments.
68
     *
69
     * @var string
70
     */
71
    public const ON_TERMINATE = 'app.on.terminate';
72
73
    /**
74
     * This event will be dispatched on app shutdown. Note that this event is dispatched only once in app life-cycle.
75
     * This event will not be passed any arguments.
76
     *
77
     * @var string
78
     */
79
    public const ON_SHUTDOWN = 'app.on.shutdown';
80
81
82
    /**
83
     * The class singleton instance.
84
     */
85
    protected static self $instance;
86
87
88
    public Event $event;
89
90
    public Config $config;
91
92
    public Router $router;
93
94
    public Globals $globals;
95
96
    public Session $session;
97
98
    public Database $database;
99
100
    public Auth $auth;
101
102
    public Data $data;
103
104
    public View $view;
105
106
    public HTML $html;
107
108
    public Path $path;
109
110
    public Dumper $dumper;
111
112
    public Misc $misc;
113
114
    protected array $methods;
115
116
    protected static array $staticMethods;
117
118
119
    /**
120
     * Class constructor.
121
     */
122 10
    final public function __construct()
123
    {
124 10
        empty(static::$instance) && static::$instance = $this;
125
126
        $this->event    = new Event();
127
        $this->config   = new Config();
128 10
        $this->router   = new Router();
129 10
        $this->globals  = new Globals();
130 10
        $this->session  = new Session();
131 10
        $this->database = Database::instance();
132 10
        $this->auth     = Auth::instance();
133 10
        $this->data     = new Data();
134 10
        $this->view     = new View();
135 10
        $this->html     = new HTML();
136 10
        $this->path     = new Path();
137 10
        $this->dumper   = new Dumper();
138 10
        $this->misc     = new Misc();
139 10
140 10
        $this->methods  = [];
141
        static::$staticMethods = [];
142 10
    }
143 10
144
    public function __call(string $method, array $arguments)
145
    {
146 3
        $class = static::class;
147
148 3
        try {
149
            return isset($this->methods[$method]) ? $this->methods[$method](...$arguments) : $this->{$method};
150
        } catch (\Exception $error) {
151 3
            Exception::throw(
152 1
                'UndefinedMethodException:BadMethodCallException',
153 1
                "Call to undefined method {$class}::{$method}()",
154
                $error->getCode(),
155 1
                $error,
156 1
            );
157
        }
158
    }
159
160
    public static function __callStatic(string $method, array $arguments)
161
    {
162 2
        $class = static::class;
163
164 2
        if (!isset(static::$staticMethods[$method])) {
165
            Exception::throw(
166 2
                'UndefinedStaticMethodException:BadMethodCallException',
167 1
                "Call to undefined static method {$class}::{$method}()"
168
            );
169 1
        }
170
171
        return static::$staticMethods[$method](...$arguments);
172
    }
173 1
174
175
    /**
176
     * Returns the singleton instance of the `App` class.
177
     *
178
     * NOTE: This method returns only the first instance of the class
179
     * which is normally the one that was created during application bootstrap.
180
     *
181
     * @return static
182
     *
183
     * @since 1.4.0
184
     */
185
    final public static function instance(): self
186
    {
187 3
        empty(static::$instance) && static::$instance = new static();
188
189 3
        return static::$instance;
190
    }
191
192
    /**
193 3
     * Extends the class using the passed callback.
194
     *
195
     * @param string $name Method name.
196
     * @param callable $callback The callback to use as method body.
197
     *
198
     * @return callable The created bound closure.
199
     */
200
    public function extend(string $name, callable $callback): callable
201
    {
202
        $method = \Closure::fromCallable($callback);
203
        $method = \Closure::bind($method, $this, $this);
204 1
205
        return $this->methods[$name] = $method;
206 1
    }
207 1
208
    /**
209 1
     * Extends the class using the passed callback.
210
     *
211
     * @param string $name Method name.
212
     * @param callable $callback The callback to use as method body.
213
     *
214
     * @return callable The created closure.
215
     */
216
    public static function extendStatic(string $name, callable $callback): callable
217
    {
218
        $method = \Closure::fromCallable($callback);
219
        $method = \Closure::bind($method, null, static::class);
220 1
221
        return static::$staticMethods[$name] = $method;
222 1
    }
223 1
224
    /**
225 1
     * Logs a message to a file and generates it if it does not exist.
226
     *
227
     * @param string $message The message wished to be logged.
228
     * @param array|null $context An associative array of values where array key = {key} in the message (context).
229
     * @param string|null $filename [optional] The name wished to be given to the file. If not provided `{global.logging.defaultFilename}` will be used instead.
230
     * @param string|null $directory [optional] The directory where the log file should be written. If not provided `{global.logging.defaultDirectory}` will be used instead.
231
     *
232
     * @return bool True on success (if the message was written).
233
     */
234
    public static function log(string $message, ?array $context = [], ?string $filename = null, ?string $directory = null): bool
235
    {
236
        if (!Config::get('global.logging.enabled', true)) {
237
            return true;
238 27
        }
239
240 27
        $hasPassed = false;
241 1
242
        if (!$filename) {
243
            $filename = Config::get('global.logging.defaultFilename', sprintf('autogenerated-%s', date('Ymd')));
244 27
        }
245
246 27
        if (!$directory) {
247 1
            $directory = Config::get('global.logging.defaultDirectory', BASE_PATH);
248
        }
249
250 27
        $file = Path::normalize($directory, $filename, '.log');
251 27
252
        if (!file_exists($directory)) {
253
            mkdir($directory, 0744, true);
254 27
        }
255
256 27
        // create log file if it does not exist
257 1
        if (!is_file($file) && is_writable($directory)) {
258
            $signature = 'Created by ' . __METHOD__ . date('() \o\\n l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL . PHP_EOL;
259
            file_put_contents($file, $signature, 0);
260
            chmod($file, 0775);
261 27
        }
262 2
263 2
        // write in the log file
264 2
        if (is_writable($file)) {
265
            clearstatcache(true, $file);
266
            // empty the file if it exceeds the configured file size
267
            $maxFileSize = Config::get('global.logging.maxFileSize', 6.4e+7);
268 27
            if (filesize($file) > $maxFileSize) {
269 27
                $stream = fopen($file, 'r');
270
                if (is_resource($stream)) {
271 27
                    $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;
272 27
                    fclose($stream);
273 1
                    file_put_contents($file, $signature, 0);
274 1
                    chmod($file, 0775);
275 1
                }
276 1
            }
277 1
278 1
            $timestamp = (new \DateTime())->format(DATE_ISO8601);
279
            $message   = Misc::interpolate($message, $context ?? []);
280
281
            $log = "$timestamp\t$message\n";
282 27
283 27
            $stream = fopen($file, 'a+');
284
            if (is_resource($stream)) {
285 27
                fwrite($stream, $log);
286
                fclose($stream);
287 27
                $hasPassed = true;
288 27
            }
289 27
        }
290 27
291 27
        return $hasPassed;
292
    }
293
294
    /**
295 27
     * Aborts the current request and sends a response with the specified HTTP status code, title, and message.
296
     * An HTML page will be rendered with the specified title and message.
297
     * If a view file for the error page is set using `{global.errorPages.CODE}`,
298
     * it will be rendered instead of the normal page and passed the `$code`, `$title`, and `$message` variables.
299
     * The title for the most common HTTP status codes (`200`, `401`, `403`, `404`, `405`, `500`, `503`) is already configured.
300
     *
301
     * @param int $code The HTTP status code.
302
     * @param string|null $title [optional] The title of the HTML page.
303
     * @param string|null $message [optional] The message of the HTML page.
304
     *
305
     * @return void
306
     *
307
     * @since 1.2.5
308
     */
309
    public static function abort(int $code, ?string $title = null, ?string $message = null): void
310
    {
311
        $http = [
312
            200 => 'OK',
313 4
            401 => 'Unauthorized',
314
            403 => 'Forbidden',
315
            404 => 'Not Found',
316
            405 => 'Not Allowed',
317
            500 => 'Internal Server Error',
318
            503 => 'Service Unavailable',
319
        ];
320
321
        $title    = htmlspecialchars($title ?? $code . ' ' . $http[$code] ?? '', ENT_QUOTES, 'UTF-8');
322
        $message  = htmlspecialchars($message ?? '', ENT_QUOTES, 'UTF-8');
323
324
        try {
325 4
            $html = View::render((string)Config::get("global.errorPages.{$code}"), compact('code', 'title', 'message'));
326 4
        } catch (\Throwable $e) {
327
            $html = (new HTML(false))
328
                ->node('<!DOCTYPE html>')
329 4
                ->open('html', ['lang' => 'en'])
330 1
                    ->open('head')
331 1
                        ->title((string)$code)
332 1
                        ->link(null, [
333 1
                            'href' => 'https://cdn.jsdelivr.net/npm/bulma@latest/css/bulma.min.css',
334 1
                            'rel' => 'stylesheet'
335 1
                        ])
336 1
                    ->close()
337
                    ->open('body')
338
                        ->open('section', ['class' => 'section is-large has-text-centered'])
339
                            ->hr(null)
340 1
                            ->h1($title, ['class' => 'title is-1 is-spaced has-text-danger'])
341 1
                            ->condition(strlen($message))
342 1
                            ->h4($message, ['class' => 'subtitle'])
343 1
                            ->hr(null)
344 1
                            ->a('Reload', ['class' => 'button is-warning is-light', 'href' => 'javascript:location.reload();'])
345 1
                            ->entity('nbsp')
346 1
                            ->entity('nbsp')
347 1
                            ->a('Home', ['class' => 'button is-success is-light', 'href' => '/'])
348 1
                            ->hr(null)
349 1
                        ->close()
350 1
                    ->close()
351 1
                ->close()
352 1
            ->return();
353 1
        } finally {
354 1
            http_response_code($code);
355 1
            echo $html;
356 1
357
            static::terminate();
358 4
        }
359 4
    }
360
361 4
362
    /**
363
     * Terminates (exits) the PHP script.
364
     * This function is used instead of PHP `exit` to allow for testing `exit` without breaking the unit tests.
365
     *
366
     * @param int|string|null $status The exit status code/message.
367
     * @param bool $noShutdown Whether to not execute the shutdown function or not.
368
     *
369
     * @return void This function never returns. It will terminate the script.
370
     * @throws \Exception If `EXIT_EXCEPTION` is defined and truthy.
371
     *
372
     * @since 1.2.5
373
     */
374
    public static function terminate($status = null, bool $noShutdown = true): void
375
    {
376
        Event::dispatch(self::ON_TERMINATE);
377
378 5
        if (defined('EXIT_EXCEPTION') && EXIT_EXCEPTION) {
379
            throw new \Exception(empty($status) ? 'Exit' : 'Exit: ' . $status);
380 5
        }
381
382 5
        // @codeCoverageIgnoreStart
383 5
        if ($noShutdown) {
384
            // app shutdown function checks for this variable
385
            // to determine if it should exit, see bootstrap/loader.php
386
            Misc::setArrayValueByKey($GLOBALS['_VELOX'], 'TERMINATE', true);
387
        }
388
389
        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...
390
        // @codeCoverageIgnoreEnd
391
    }
392
393
    /**
394
     * Shuts the app down by terminating it and executing shutdown function(s).
395
     * The triggered shutdown functions can be normal shutdown functions registered,
396
     * using `register_shutdown_function()` or the `self::ON_SHUTDOWN` event.
397
     *
398
     * @return void
399
     *
400
     * @internal This method is to be used by the framework and not the user.
401
     * @since 1.4.2
402
     *
403
     * @codeCoverageIgnore
404
     */
405
    public static function shutdown(): void
406
    {
407
        Event::dispatch(self::ON_SHUTDOWN);
408
409
        Misc::setArrayValueByKey($GLOBALS['_VELOX'], 'SHUTDOWN', false);
410
411
        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...
412
    }
413
}
414