Passed
Push — master ( 48e2e4...58f05f )
by Marwan
08:52
created

View   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 410
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 13
Bugs 0 Features 0
Metric Value
c 13
b 0
f 0
dl 0
loc 410
ccs 128
cts 128
cp 1
rs 9.36
eloc 134
wmc 38

14 Methods

Rating   Name   Duplication   Size   Complexity  
A sectionEnd() 0 17 5
A yield() 0 14 4
A section() 0 7 2
A sectionReset() 0 3 1
A sectionStart() 0 9 2
A layout() 0 9 1
A partial() 0 9 1
A resolveCachePath() 0 15 2
A render() 0 19 3
A clearCache() 0 24 6
B cache() 0 59 8
A include() 0 7 1
A page() 0 9 1
A resolvePath() 0 8 1
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\Frontend;
13
14
use MAKS\Velox\App;
15
use MAKS\Velox\Backend\Event;
16
use MAKS\Velox\Backend\Config;
17
use MAKS\Velox\Frontend\HTML;
18
use MAKS\Velox\Frontend\Path;
19
use MAKS\Velox\Frontend\View\Compiler;
20
use MAKS\Velox\Helper\Misc;
21
22
/**
23
 * A class that renders view files (Layouts, Pages, and Partials) with the ability to include additional files and divide page content into sections and cache rendered views.
24
 *
25
 * Example:
26
 * ```
27
 * // render a view
28
 * $html = View::render('somePage', ['someVar' => 'someValue'], 'someLayout');
29
 *
30
 * // render a view, cache it and get it from cache on subsequent calls
31
 * $html = View::cache('somePage', ['someVar' => 'someValue'], 'someLayout');
32
 *
33
 * // delete cached views
34
 * View::clearCache();
35
 *
36
 * // set section value
37
 * View::section('name', $content);
38
 *
39
 * // start capturing section content
40
 * View::sectionStart('name');
41
 *
42
 * // end capturing section content
43
 * View::sectionEnd();
44
 *
45
 * // reset (empty) section content
46
 * View::sectionReset('name');
47
 *
48
 * // get section content
49
 * View::yield('name', 'fallback');
50
 *
51
 * // include a file
52
 * View::include('path/to/a/file');
53
 *
54
 * // render a layout from theme layouts
55
 * $html = View::layout('layoutName', $vars);
56
 *
57
 * // render a page from theme pages
58
 * $html = View::page('pageName', $vars);
59
 *
60
 * // render a partial from theme partials
61
 * $html = View::partial('partialName', $vars);
62
 * ```
63
 *
64
 * @package Velox\Frontend
65
 * @since 1.0.0
66
 * @api
67
 */
68
class View
69
{
70
    /**
71
     * This event will be dispatched before rendering a view.
72
     * This event will be passed a reference to the array that will be passed to the view as variables.
73
     *
74
     * @var string
75
     */
76
    public const BEFORE_RENDER = 'view.before.render';
77
78
    /**
79
     * This event will be dispatched when a view is cached.
80
     * This event will not be passed any arguments.
81
     *
82
     * @var string
83
     */
84
    public const ON_CACHE = 'view.on.cache';
85
86
    /**
87
     * This event will be dispatched when views cache is cleared.
88
     * This event will not be passed any arguments.
89
     *
90
     * @var string
91
     */
92
    public const ON_CACHE_CLEAR = 'view.on.cacheClear';
93
94
95
    /**
96
     * The default values of class parameters.
97
     *
98
     * @var array
99
     */
100
    public const DEFAULTS = [
101
        'name' => '__default__',
102
        'variables' => [],
103
        'fileExtension' => '.phtml',
104
        'inherit' => true,
105
        'minify' => true,
106
        'cache' => false,
107
        'cacheExclude' => ['__default__'],
108
        'cacheAsIndex' => false,
109
        'cacheWithTimestamp' => true,
110
        'engine' => [
111
            'enabled' => true,
112
            'cache'   => true,
113
            'debug'   => false,
114
        ]
115
    ];
116
117
118
    /**
119
     * Sections buffer.
120
     */
121
    protected static array $sections = [];
122
123
    /**
124
     * Sections stack.
125
     */
126
    protected static array $stack = [];
127
128
129
    /**
130
     * Pushes content to the buffer of the section with the given name.
131
     * Note that a section will not be rendered unless it's yielded.
132
     *
133
     * @param string $name The name of the section.
134
     * @param string $content The content of the section.
135
     *
136
     * @return void
137
     */
138 7
    public static function section(string $name, string $content): void
139
    {
140 7
        if (!isset(static::$sections[$name])) {
141 5
            static::$sections[$name] = [];
142
        }
143
144 7
        static::$sections[$name][] = $content;
145 7
    }
146
147
    /**
148
     * Resets (empties) the buffer of the section with the given name.
149
     *
150
     * @param string $name The name of the section.
151
     *
152
     * @return void
153
     */
154 4
    public static function sectionReset(string $name): void
155
    {
156 4
        unset(static::$sections[$name]);
157 4
    }
158
159
    /**
160
     * Starts capturing buffer of the section with the given name. Works in conjunction with `self::sectionEnd()`.
161
     * Note that a section will not be rendered unless it's yielded.
162
     *
163
     * @param string $name The name of the section.
164
     *
165
     * @return void
166
     */
167 1
    public static function sectionStart(string $name): void
168
    {
169 1
        if (!isset(static::$sections[$name])) {
170 1
            static::$sections[$name] = [];
171
        }
172
173 1
        array_push(static::$stack, $name);
174
175 1
        ob_start();
176 1
    }
177
178
    /**
179
     * Ends capturing buffer of the section with the given name. Works in conjunction with `self::sectionStart()`.
180
     * Note that a section will not be rendered unless it's yielded.
181
     *
182
     * @return void
183
     *
184
     * @throws \Exception If no section has been started.
185
     */
186 1
    public static function sectionEnd(): void
187
    {
188 1
        if (!count(static::$stack) || !ob_get_level()) {
189 1
            $variables = ['class', 'function', 'file', 'line'];
190 1
            $backtrace = Misc::backtrace($variables, 1);
191 1
            $backtrace = is_array($backtrace) ? $backtrace : array_map('strtoupper', $variables);
192
193 1
            throw new \Exception(
194 1
                vsprintf('Not in a context to end a section! Call to %s::%s() in %s on line %s is superfluous', $backtrace)
195
            );
196
        }
197
198 1
        $buffer = ob_get_clean();
199
200 1
        $name = array_pop(static::$stack);
201
202 1
        static::$sections[$name][] = $buffer ?: '';
203 1
    }
204
205
    /**
206
     * Returns content of the section with the given name.
207
     *
208
     * @param string $name The name of the section.
209
     * @param string $default [optional] The default value to yield if the section has no content or is an empty string.
210
     *
211
     * @return string
212
     */
213 5
    public static function yield(string $name, string $default = ''): string
214
    {
215 5
        $section = '';
216
217 5
        if (isset(static::$sections[$name])) {
218 4
            foreach (static::$sections[$name] as $buffer) {
219
                // buffers are added in reverse order
220 4
                $section = $buffer . $section;
221
            }
222
223 4
            static::sectionReset($name);
224
        }
225
226 5
        return strlen(trim($section)) ? $section : $default;
227
    }
228
229
    /**
230
     * Includes a file from the active theme directory.
231
     * Can also be used as a mean of extending a layout if it was put at the end of it because the compilation is done
232
     * from top to bottom and from the deepest nested element to the upper most (imperative approach, there is no preprocessing).
233
     *
234
     * @param string $file The path of the file starting from theme root.
235
     * @param array|null $variables [optional] An associative array of the variables to pass.
236
     *
237
     * @return void
238
     */
239 1
    public static function include(string $file, ?array $variables = null): void
240
    {
241 1
        $path = Config::get('theme.paths.root');
242
243 1
        $include = self::resolvePath($path, $file);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $directory of MAKS\Velox\Frontend\View::resolvePath() 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

243
        $include = self::resolvePath(/** @scrutinizer ignore-type */ $path, $file);
Loading history...
244
245 1
        Compiler::require($include, $variables);
246 1
    }
247
248
    /**
249
     * Renders a theme layout with the passed variables.
250
     *
251
     * @param string $name The name of the layout.
252
     * @param array $variables [optional] An associative array of the variables to pass.
253
     *
254
     * @return string
255
     */
256 7
    public static function layout(string $name, array $variables = []): string
257
    {
258 7
        $path = Config::get('theme.paths.layouts');
259
260 7
        $variables['defaultLayoutVars'] = Config::get('view.defaultLayoutVars');
261
262 7
        $layout = self::resolvePath($path, $name);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $directory of MAKS\Velox\Frontend\View::resolvePath() 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

262
        $layout = self::resolvePath(/** @scrutinizer ignore-type */ $path, $name);
Loading history...
263
264 7
        return Compiler::compile($layout, __FUNCTION__, $variables);
265
    }
266
267
    /**
268
     * Renders a theme page with the passed variables.
269
     *
270
     * @param string $name The name of the page.
271
     * @param array $variables [optional] An associative array of the variables to pass.
272
     *
273
     * @return string
274
     */
275 8
    public static function page(string $name, array $variables = []): string
276
    {
277 8
        $path = Config::get('theme.paths.pages');
278
279 8
        $variables['defaultPageVars'] = Config::get('view.defaultPageVars');
280
281 8
        $page = self::resolvePath($path, $name);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $directory of MAKS\Velox\Frontend\View::resolvePath() 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

281
        $page = self::resolvePath(/** @scrutinizer ignore-type */ $path, $name);
Loading history...
282
283 8
        return Compiler::compile($page, __FUNCTION__, $variables);
284
    }
285
286
    /**
287
     * Renders a theme partial with the passed variables.
288
     *
289
     * @param string $name The name of the partial.
290
     * @param array $variables [optional] An associative array of the variables to pass.
291
     *
292
     * @return string
293
     */
294 5
    public static function partial(string $name, array $variables = []): string
295
    {
296 5
        $path = Config::get('theme.paths.partials');
297
298 5
        $variables['defaultPartialVars'] = Config::get('view.defaultPartialVars');
299
300 5
        $partial = self::resolvePath($path, $name);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $directory of MAKS\Velox\Frontend\View::resolvePath() 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

300
        $partial = self::resolvePath(/** @scrutinizer ignore-type */ $path, $name);
Loading history...
301
302 5
        return Compiler::compile($partial, __FUNCTION__, $variables);
303
    }
304
305
    /**
306
     * Renders a view (a Page wrapped in a Layout) with the passed variables, the Page content will be sent to `{view.defaultSectionName}` section.
307
     *
308
     * @param string $page The name of the page.
309
     * @param array $variables [optional] An associative array of the variables to pass.
310
     * @param string|null $layout [optional] The name of the Layout to use.
311
     *
312
     * @return string
313
     */
314 7
    public static function render(string $page, array $variables = [], ?string $layout = null): string
315
    {
316 7
        $viewConfig = Config::get('view');
317 7
        $layout     = $layout ?? $viewConfig['defaultLayoutName'];
318 7
        $section    = $viewConfig['defaultSectionName'];
319 7
        $minify     = $viewConfig['minify'];
320 7
        $cache      = $viewConfig['cache'];
321
322 7
        if ($cache) {
323 1
            return static::cache($page, $variables, $layout);
324
        }
325
326 7
        Event::dispatch(self::BEFORE_RENDER, [&$variables]);
327
328 7
        static::section($section, static::page($page, $variables));
329
330 6
        $view = static::layout($layout, $variables);
331
332 6
        return $minify ? HTML::minify($view) : $view;
333
    }
334
335
    /**
336
     * Renders a view with the passed variables and cache it as HTML, subsequent calls to this function will return the cached version.
337
     * This function is exactly like `self::render()` but with caching capabilities.
338
     *
339
     * @param string $page The name of the page.
340
     * @param array $variables [optional] An associative array of the variables to pass.
341
     * @param string|null $layout [optional] The name of the Layout to use.
342
     *
343
     * @return string
344
     */
345 1
    public static function cache(string $page, array $variables = [], ?string $layout = null)
346
    {
347 1
        $viewConfig         = Config::get('view');
348 1
        $cacheEnabled       = $viewConfig['cache'];
349 1
        $cacheExclude       = $viewConfig['cacheExclude'];
350 1
        $cacheAsIndex       = $viewConfig['cacheAsIndex'];
351 1
        $cacheWithTimestamp = $viewConfig['cacheWithTimestamp'];
352 1
        $cacheDir           = Config::get('global.paths.storage') . '/cache/views/';
353
354 1
        if (!file_exists($cacheDir)) {
355 1
            mkdir($cacheDir, 0744, true);
356
        }
357
358 1
        $cacheFile          = static::resolveCachePath($page);
359 1
        $cacheFileDirectory = dirname($cacheFile);
360 1
        $fileExists         = file_exists($cacheFile);
361
362 1
        $content = null;
363
364 1
        if (!$cacheEnabled || !$fileExists) {
365 1
            Config::set('view.cache', false);
366 1
            $view = static::render($page, $variables, $layout);
367 1
            Config::set('view.cache', true);
368
369 1
            if (in_array($page, (array)$cacheExclude)) {
370 1
                return $view;
371
            }
372
373 1
            if ($cacheAsIndex) {
374 1
                if (!file_exists($cacheFileDirectory)) {
375 1
                    mkdir($cacheFileDirectory, 0744, true);
376
                }
377
            } else {
378 1
                $cacheFile = preg_replace('/\/+|\\+/', '___', $page);
379 1
                $cacheFile = static::resolvePath($cacheDir, $cacheFile, '.html');
380
            }
381
382 1
            $comment = '';
383 1
            if ($cacheWithTimestamp) {
384 1
                $timestamp = date('l jS \of F Y h:i:s A (Ymdhis)');
385 1
                $comment   = sprintf('<!-- [CACHE] Generated on %s -->', $timestamp);
386
            }
387
388 1
            $content = preg_replace(
389 1
                '/<!DOCTYPE html>/i',
390 1
                '$0' . $comment,
391
                $view
392
            );
393
394 1
            file_put_contents($cacheFile, $content, LOCK_EX);
395
396 1
            Event::dispatch(self::ON_CACHE);
397
398 1
            App::log('Generated cache for the "{page}" page', ['page' => $page], 'system');
399
        }
400
401 1
        $content = $content ?? file_get_contents($cacheFile);
402
403 1
        return $content;
404
    }
405
406
    /**
407
     * Deletes all cached views generated by `self::cache()`.
408
     *
409
     * @return void
410
     */
411 1
    public static function clearCache(): void
412
    {
413 1
        $clear = static function ($path) use (&$clear) {
414 1
            static $base = null;
415 1
            if (!$base) {
416 1
                $base = $path;
417
            }
418
419 1
            $items = glob($path . '/*');
420 1
            foreach ($items as $item) {
421 1
                is_dir($item) ? $clear($item) : unlink($item);
422
            }
423
424 1
            if ($path !== $base) {
425 1
                file_exists($path) && rmdir($path);
426
            }
427 1
        };
428
429 1
        $clear(Config::get('global.paths.storage') . '/cache/views/');
430 1
        $clear(Config::get('global.paths.storage') . '/temp/views/');
431
432 1
        Event::dispatch(self::ON_CACHE_CLEAR);
433
434 1
        App::log('Cleared views cache', null, 'system');
435 1
    }
436
437
    /**
438
     * Returns a normalized path of a page from the cache directory.
439
     *
440
     * @param string $pageName
441
     *
442
     * @return string
443
     */
444 1
    private static function resolveCachePath(string $pageName): string
445
    {
446 1
        static $cacheDir = null;
447
448 1
        if ($cacheDir === null) {
449 1
            $cacheDir = Config::get('global.paths.storage') . '/cache/views/';
450
        }
451
452 1
        $cacheName = sprintf(
453 1
            '%s/%s',
454
            $pageName,
455 1
            'index'
456
        );
457
458 1
        return static::resolvePath($cacheDir, $cacheName, '.html');
459
    }
460
461
    /**
462
     * Returns a normalized path to a file based on OS.
463
     *
464
     * @param string $directory
465
     * @param string $filename
466
     * @param string|null $extension
467
     *
468
     * @return string
469
     */
470 11
    private static function resolvePath(string $directory, string $filename, ?string $extension = null): string
471
    {
472 11
        $extension = $extension ?? Config::get('view.fileExtension') ?? self::DEFAULTS['fileExtension'];
473
474 11
        return Path::normalize(
475 11
            $directory,
476
            $filename,
477
            $extension
478
        );
479
    }
480
}
481