Completed
Pull Request — master (#35)
by Todd
03:39
created

Ebi::tagName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2017 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Ebi;
9
10
11
class Ebi {
12
    /**
13
     * @var string
14
     */
15
    protected $cachePath;
16
    /**
17
     * @var callable[]
18
     */
19
    protected $functions;
20
    /**
21
     * @var TemplateLoaderInterface
22
     */
23
    private $templateLoader;
24
    /**
25
     * @var CompilerInterface
26
     */
27
    private $compiler;
28
    /**
29
     * @var callable[]
30
     */
31
    private $components = [];
32
    /**
33
     * @var array
34
     */
35
    private $meta;
36
37
    /**
38
     * Ebi constructor.
39
     *
40
     * @param TemplateLoaderInterface $templateLoader Used to load template sources from component names.
41
     * @param string $cachePath The path to cache compiled templates.
42
     * @param CompilerInterface $compiler The compiler used to compile templates.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $compiler not be null|CompilerInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
43
     */
44 78
    public function __construct(TemplateLoaderInterface $templateLoader, $cachePath, CompilerInterface $compiler = null) {
45 78
        $this->templateLoader = $templateLoader;
46 78
        $this->cachePath = $cachePath;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
47 78
        $this->compiler = $compiler ?: new Compiler();
0 ignored issues
show
Documentation Bug introduced by
It seems like $compiler ?: new \Ebi\Compiler() can also be of type object<Ebi\Compiler>. However, the property $compiler is declared as type object<Ebi\CompilerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
48
49 78
        $this->defineFunction('abs');
50 78
        $this->defineFunction('arrayColumn', 'array_column');
51 78
        $this->defineFunction('arrayKeyExists', 'array_key_exists');
52 78
        $this->defineFunction('arrayKeys', 'array_keys');
53 78
        $this->defineFunction('arrayMerge', 'array_merge');
54 78
        $this->defineFunction('arrayMergeRecursive', 'array_merge_recursive');
55 78
        $this->defineFunction('arrayReplace', 'array_replace');
56 78
        $this->defineFunction('arrayReplaceRecursive', 'array_replace_recursive');
57 78
        $this->defineFunction('arrayReverse', 'array_reverse');
58 78
        $this->defineFunction('arrayValues', 'array_values');
59 78
        $this->defineFunction('base64Encode', 'base64_encode');
60 78
        $this->defineFunction('ceil');
61 78
        $this->defineFunction('componentExists', [$this, 'componentExists']);
62 78
        $this->defineFunction('count');
63 78
        $this->defineFunction('empty');
64 78
        $this->defineFunction('floor');
65 78
        $this->defineFunction('formatDate', [$this, 'formatDate']);
66 78
        $this->defineFunction('formatNumber', 'number_format');
67 78
        $this->defineFunction('htmlEncode', 'htmlspecialchars');
68 78
        $this->defineFunction('isArray', 'is_array');
69 78
        $this->defineFunction('isBool', 'is_bool');
70 78
        $this->defineFunction('isInt', 'is_int');
71 78
        $this->defineFunction('isScalar', 'is_scalar');
72 78
        $this->defineFunction('isString', 'is_string');
73 78
        $this->defineFunction('join');
74 78
        $this->defineFunction('lcase', $this->mb('strtolower'));
75 78
        $this->defineFunction('lcfirst');
76 78
        $this->defineFunction('ltrim');
77 78
        $this->defineFunction('max');
78 78
        $this->defineFunction('min');
79 78
        $this->defineFunction('queryEncode', 'http_build_query');
80 78
        $this->defineFunction('round');
81 78
        $this->defineFunction('rtrim');
82 78
        $this->defineFunction('sprintf');
83 78
        $this->defineFunction('strlen', $this->mb('strlen'));
84 78
        $this->defineFunction('substr', $this->mb('substr'));
85 78
        $this->defineFunction('trim');
86 78
        $this->defineFunction('ucase', $this->mb('strtoupper'));
87 78
        $this->defineFunction('ucfirst');
88 78
        $this->defineFunction('ucwords');
89 78
        $this->defineFunction('urlencode', 'rawurlencode');
90
91 78
        $this->defineFunction('@class', [$this, 'attributeClass']);
92
93
        // Define a simple component not found component to help troubleshoot.
94 78
        $this->defineComponent('@component-not-found', function ($props) {
95 1
            echo '<!-- Ebi component "'.htmlspecialchars($props['component']).'" not found. -->';
96 78
        });
97
98
        // Define a simple component exception.
99 78
        $this->defineComponent('@exception', function ($props) {
100 1
            echo "\n<!--\nEbi exception in component \"".htmlspecialchars($props['component'])."\".\n".
101 1
                htmlspecialchars($props['message'])."\n-->\n";
102
103 78
        });
104
105 78
        $this->defineComponent('@compile-exception', [$this, 'writeCompileException']);
106 78
    }
107
108
    /**
109
     * Register a runtime function.
110
     *
111
     * @param string $name The name of the function.
112
     * @param callable $function The function callback.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $function not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
113
     */
114 78
    public function defineFunction($name, $function = null) {
115 78
        if ($function === null) {
116 78
            $function = $name;
117
        }
118
119 78
        $this->functions[strtolower($name)] = $function;
120 78
        $this->compiler->defineFunction($name, $function);
0 ignored issues
show
Bug introduced by
The method defineFunction() does not seem to exist on object<Ebi\CompilerInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
121 78
    }
122
123 78
    private function mb($func) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
124 78
        return function_exists("mb_$func") ? "mb_$func" : $func;
125
    }
126
127
    /**
128
     * Write a component to the output buffer.
129
     *
130
     * @param string $component The name of the component.
131
     * @param array ...$args
132
     */
133 22
    public function write($component, ...$args) {
134 22
        $component = strtolower($component);
135
136
        try {
137 22
            $callback = $this->lookup($component);
138
139 22
            if (is_callable($callback)) {
140 22
                call_user_func($callback, ...$args);
141
            } else {
142 22
                $this->write('@component-not-found', ['component' => $component]);
143
            }
144 1
        } catch (\Throwable $ex) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
145 1
            $this->write('@exception', ['message' => $ex->getMessage(), 'code', $ex->getCode(), 'component' => $component]);
146 1
            return;
147
        } catch (\Exception $ex) {
148
            $this->write('@exception', ['message' => $ex->getMessage(), 'code', $ex->getCode(), 'component' => $component]);
149
            return;
150
        }
151 22
    }
152
153
    /**
154
     * Lookup a component with a given name.
155
     *
156
     * @param string $component The component to lookup.
157
     * @return callable|null Returns the component function or **null** if the component is not found.
158
     */
159 73
    public function lookup($component) {
160 73
        $component = strtolower($component);
161 73
        $key = $this->componentKey($component);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
162
163 73
        if (!array_key_exists($key, $this->components)) {
164 68
            $this->loadComponent($component);
165
        }
166
167 73
        if (isset($this->components[$key])) {
168 72
            return $this->components[$key];
169
        } else {
170
            // Mark a tombstone to the component array so it doesn't keep getting loaded.
171 2
            $this->components[$key] = null;
172 2
            return null;
173
        }
174
    }
175
176
    /**
177
     * Check to see if a component exists.
178
     *
179
     * @param string $component The name of the component.
180
     * @param bool $loader Whether or not to use the component loader or just look in the component cache.
181
     * @return bool Returns **true** if the component exists or **false** otherwise.
182
     */
183 2
    public function componentExists($component, $loader = true) {
184 2
        $componentKey = $this->componentKey($component);
185 2
        if (array_key_exists($componentKey, $this->components)) {
186 1
            return $this->components[$componentKey] !== null;
187 2
        } elseif ($loader) {
188 2
            return !empty($this->templateLoader->cacheKey($component));
189
        }
190 1
        return false;
191
    }
192
193
    /**
194
     * Strip the namespace off a component name to get the component key.
195
     *
196
     * @param string $component The full name of the component with a possible namespace.
197
     * @return string Returns the component key.
198
     */
199 75
    protected function componentKey($component) {
200 75
        if (false !== $pos = strpos($component, ':')) {
201 1
            $component = substr($component, $pos + 1);
202
        }
203 75
        return strtolower($component);
204
    }
205
206
    /**
207
     * Load a component.
208
     *
209
     * @param string $component The name of the component to load.
210
     * @return callable|null Returns the component or **null** if the component isn't found.
211
     */
212 68
    protected function loadComponent($component) {
213 68
        $cacheKey = $this->templateLoader->cacheKey($component);
214
        // The template loader can tell us a template doesn't exist when giving the cache key.
215 68
        if (empty($cacheKey)) {
216 2
            return null;
217
        }
218
219 66
        $cachePath = "{$this->cachePath}/$cacheKey.php";
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
220 66
        $componentKey = $this->componentKey($component);
221
222 66
        if (!file_exists($cachePath)) {
223 66
            $src = $this->templateLoader->load($component);
224
            try {
225 66
                return $this->compile($componentKey, $src, $cacheKey);
226 8
            } catch (CompileException $ex) {
227 8
                $props = ['message' => $ex->getMessage()] + $ex->getContext();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 34 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
228 8
                return $this->components[$componentKey] = function() use ($props) {
229 8
                    $this->write('@compile-exception', $props);
230 8
                };
231
            }
232
        } else {
233
            return $this->includeComponent($componentKey, $cachePath);
234
        }
235
    }
236
237 8
    protected function writeCompileException($props) {
238 8
        echo "\n<section class=\"ebi-ex\">\n",
239 8
            '<h2>Error compiling '.htmlspecialchars($props['path'])." near line {$props['line']}.</h2>\n";
240
241 8
        echo '<p class="ebi-ex-message">'.htmlspecialchars($props['message'])."</p>\n";
242
243 8
        if (!empty($props['source'])) {
244 6
            $source = $props['source'];
245 6
            if (isset($props['sourcePosition'])) {
246 3
                $pos = $props['sourcePosition'];
247 3
                $len = isset($props['sourceLength']) ? $props['sourceLength'] : 1;
248
249 3
                if ($len === 1) {
250
                    // Small kludge to select a viewable character.
251 3
                    for (; $pos >= 0 && isset($source[$pos]) && in_array($source[$pos], [' ', "\n"], true); $pos--, $len++) {
0 ignored issues
show
Unused Code introduced by
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
252
                        // It's all in the loop.
253
                    }
254
                }
255
256 3
                $source = htmlspecialchars(substr($source, 0, $pos)).
257 3
                    '<mark class="ebi-ex-highlight">'.htmlspecialchars(substr($source, $pos, $len)).'</mark>'.
258 3
                    htmlspecialchars(substr($source, $pos + $len));
259
            } else {
260 3
                $source = htmlspecialchars($source);
261
            }
262
263 6
            echo '<pre class="ebi-ex-source ebi-ex-context"><code>',
264 6
                $source,
265 6
                "</code></pre>\n";
266
        }
267
268 8
        if (!empty($props['lines'])) {
269 8
            echo '<pre class="ebi-ex-source ebi-ex-lines">';
270
271 8
            foreach ($props['lines'] as $i => $line) {
272 8
                echo '<code class="ebi-ex-line">';
273
274 8
                $str = sprintf("%3d. %s", $i, htmlspecialchars($line));
275 8
                if ($i === $props['line']) {
276 8
                    echo "<mark class=\"ebi-ex-highlight\">$str</mark>";
277
                } else {
278 6
                    echo $str;
279
                }
280
281 8
                echo "</code>\n";
282
            }
283
284 8
            echo "</pre>\n";
285
        }
286
287 8
        echo "</section>\n";
288 8
    }
289
290
    /**
291
     * Check to see if a specific cache key exists in the cache.
292
     *
293
     * @param string $cacheKey The cache key to check.
294
     * @return bool Returns **true** if there is a cache key at the file or **false** otherwise.
295
     */
296 1
    public function cacheKeyExists($cacheKey) {
297 1
        $cachePath = "{$this->cachePath}/$cacheKey.php";
298 1
        return file_exists($cachePath);
299
    }
300
301
    /**
302
     * Compile a component from source, cache it and include it.
303
     *
304
     * @param string $component The name of the component.
305
     * @param string $src The component source.
306
     * @param string $cacheKey The cache key of the component.
307
     * @return callable|null Returns the compiled component closure.
308
     */
309 70
    public function compile($component, $src, $cacheKey) {
310 70
        $cachePath = "{$this->cachePath}/$cacheKey.php";
311 70
        $component = strtolower($component);
312
313 70
        $php = $this->compiler->compile($src, ['basename' => $component, 'path' => $cacheKey]);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
314 62
        $comment = "/*\n".str_replace('*/', '❄/', trim($src))."\n*/";
315
316 62
        $this->filePutContents($cachePath, "<?php\n$comment\n$php");
317
318 62
        return $this->includeComponent($component, $cachePath);
319
    }
320
321
    /**
322
     * Include a cached component.
323
     *
324
     * @param string $component The component key.
325
     * @param string $cachePath The path to the component.
326
     * @return callable|null Returns the component function or **null** if the component wasn't properly defined.
327
     */
328 62
    private function includeComponent($component, $cachePath) {
329 62
        unset($this->components[$component]);
330 62
        $fn = $this->requireFile($cachePath);
331
332 62
        if (isset($this->components[$component])) {
333 62
            return $this->components[$component];
334
        } elseif (is_callable($fn)) {
335
            $this->defineComponent($component, $fn);
336
            return $fn;
337
        } else {
338
            $this->components[$component] = null;
339
            return null;
340
        }
341
    }
342
343
    /**
344
     * A safe version of {@link file_put_contents()} that also clears op caches.
345
     *
346
     * @param string $path The path to save to.
347
     * @param string $contents The contents of the file.
348
     * @return bool Returns **true** on success or **false** on failure.
349
     */
350 62
    private function filePutContents($path, $contents) {
351 62
        if (!file_exists(dirname($path))) {
352 4
            mkdir(dirname($path), 0777, true);
353
        }
354 62
        $tmpPath = tempnam(dirname($path), 'ebi-');
355 62
        $r = false;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
356 62
        if (file_put_contents($tmpPath, $contents) !== false) {
357 62
            chmod($tmpPath, 0664);
358 62
            $r = rename($tmpPath, $path);
359
        }
360
361 62
        if (function_exists('apc_delete_file')) {
362
            // This fixes a bug with some configurations of apc.
363
            @apc_delete_file($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
364 62
        } elseif (function_exists('opcache_invalidate')) {
365 62
            @opcache_invalidate($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
366
        }
367
368 62
        return $r;
369
    }
370
371
    /**
372
     * Include a file.
373
     *
374
     * This is method is useful for including a file bound to this object instance.
375
     *
376
     * @param string $path The path to the file to include.
377
     * @return mixed Returns the result of the include.
378
     */
379 62
    public function requireFile($path) {
380 62
        return require $path;
381
    }
382
383
    /**
384
     * Register a component.
385
     *
386
     * @param string $name The name of the component to register.
387
     * @param callable $component The component function.
388
     */
389 78
    public function defineComponent($name, callable $component) {
390 78
        $this->components[$name] = $component;
391 78
    }
392
393
    /**
394
     * Render a component to a string.
395
     *
396
     * @param string $component The name of the component to render.
397
     * @param array ...$args Arguments to pass to the component.
398
     * @return string|null Returns the rendered component or **null** if the component was not found.
399
     */
400 69
    public function render($component, ...$args) {
401 69
        if ($callback = $this->lookup($component)) {
402 69
            ob_start();
403 69
            $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
404 69
            call_user_func($callback, ...$args);
405 69
            error_reporting($errs);
406 69
            $str = ob_get_clean();
407 69
            return $str;
408
        } else {
409
            trigger_error("Could not find component $component.", E_USER_NOTICE);
410
            return null;
411
        }
412
    }
413
414
    /**
415
     * Set the error reporting appropriate for template rendering.
416
     *
417
     * @return int Returns the previous error level.
418
     */
419
    public function setErrorReporting() {
420
        $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
421
        return $errs;
422
    }
423
424
    /**
425
     * Call a function registered with **defineFunction()**.
426
     *
427
     * If a static or global function is registered then it's simply rendered in the compiled template.
428
     * This method is for closures or callbacks.
429
     *
430
     * @param string $name The name of the registered function.
431
     * @param array ...$args The function's argument.
432
     * @return mixed Returns the result of the function
433
     * @throws RuntimeException Throws an exception when the function isn't found.
434
     */
435 4
    public function call($name, ...$args) {
436 4
        if (!isset($this->functions[$name])) {
437 1
            throw new RuntimeException("Call to undefined function $name.", 500);
438
        } else {
439 3
            return $this->functions[$name](...$args);
440
        }
441
    }
442
443
    /**
444
     * Render a variable appropriately for CSS.
445
     *
446
     * This is a convenience runtime function.
447
     *
448
     * @param string|array $expr A CSS class, an array of CSS classes, or an associative array where the keys are class
449
     * names and the values are truthy conditions to include the class (or not).
450
     * @return string Returns a space-delimited CSS class string.
451
     */
452 11
    public function attributeClass($expr) {
453 11
        if (is_array($expr)) {
454 4
            $classes = [];
455 4
            foreach ($expr as $i => $val) {
456 4
                if (is_array($val)) {
457 1
                    $classes[] = $this->attributeClass($val);
458 4
                } elseif (is_int($i)) {
459 1
                    $classes[] = $val;
460 3
                } elseif (!empty($val)) {
461 4
                    $classes[] = $i;
462
                }
463
            }
464 4
            return implode(' ', $classes);
465
        } else {
466 7
            return (string)$expr;
467
        }
468
    }
469
470
    /**
471
     * Format a data.
472
     *
473
     * @param mixed $date The date to format. This can be a string data, a timestamp or an instance of **DateTimeInterface**.
474
     * @param string $format The format of the date.
475
     * @return string Returns the formatted data.
476
     * @see date_format()
477
     */
478 1
    public function formatDate($date, $format = 'c') {
479 1
        if (is_string($date)) {
480
            try {
481 1
                $date = new \DateTimeImmutable($date);
482
            } catch (\Exception $ex) {
483 1
                return '#error#';
484
            }
485
        } elseif (empty($date)) {
486
            return '';
487
        } elseif (is_int($date)) {
488
            try {
489
                $date = new \DateTimeImmutable('@'.$date);
490
            } catch (\Exception $ex) {
491
                return '#error#';
492
            }
493
        } elseif (!$date instanceof \DateTimeInterface) {
494
            return '#error#';
495
        }
496
497 1
        return $date->format($format);
498
    }
499
500
    /**
501
     * Get a single item from the meta array.
502
     *
503
     * @param string $name The key to get from.
504
     * @param mixed $default The default value if no item at the key exists.
505
     * @return mixed Returns the meta value.
506
     */
507
    public function getMeta($name, $default = null) {
508
        return isset($this->meta[$name]) ? $this->meta[$name] : $default;
509
    }
510
511
    /**
512
     * Set a single item to the meta array.
513
     *
514
     * @param string $name The key to set.
515
     * @param mixed $value The new value.
516
     * @return $this
517
     */
518 1
    public function setMeta($name, $value) {
519 1
        $this->meta[$name] = $value;
520 1
        return $this;
521
    }
522
523
    /**
524
     * Get the template loader.
525
     *
526
     * The template loader translates component names into template contents.
527
     *
528
     * @return TemplateLoaderInterface Returns the template loader.
529
     */
530 1
    public function getTemplateLoader() {
531 1
        return $this->templateLoader;
532
    }
533
534
    /**
535
     * Set the template loader.
536
     *
537
     * The template loader translates component names into template contents.
538
     *
539
     * @param TemplateLoaderInterface $templateLoader The new template loader.
540
     * @return $this
541
     */
542
    public function setTemplateLoader($templateLoader) {
543
        $this->templateLoader = $templateLoader;
544
        return $this;
545
    }
546
547
    /**
548
     * Get the entire meta array.
549
     *
550
     * @return array Returns the meta.
551
     */
552
    public function getMetaArray() {
553
        return $this->meta;
554
    }
555
556
    /**
557
     * Set the entire meta array.
558
     *
559
     * @param array $meta The new meta array.
560
     * @return $this
561
     */
562
    public function setMetaArray(array $meta) {
563
        $this->meta = $meta;
564
        return $this;
565
    }
566
567
    /**
568
     * Return a dynamic attribute.
569
     *
570
     * The attribute renders differently depending on the value.
571
     *
572
     * - If the value is **true** then it will render as an HTML5 boolean attribute.
573
     * - If the value is **false** or **null** then the attribute will not render.
574
     * - Other values render as attribute values.
575
     * - Attributes that start with **aria-** render **true** and **false** as values.
576
     *
577
     * @param string $name The name of the attribute.
578
     * @param mixed $value The value of the attribute.
579
     * @return string Returns the attribute definition or an empty string.
580
     */
581 20
    public function attribute($name, $value) {
582 20
        if (substr($name, 0, 5) === 'aria-' && is_bool($value)) {
583 2
            $value = $value ? 'true' : 'false';
584
        }
585
586 20
        if ($value === true) {
587 1
            return ' '.$name;
588 19
        } elseif (!in_array($value, [null, false], true)) {
589 16
            return " $name=\"".htmlspecialchars($value).'"';
590
        }
591 4
        return '';
592
    }
593
594
    /**
595
     * Escape a value for echoing to HTML with a bit of non-scalar checking.
596
     *
597
     * @param mixed $val The value to escape.
598
     * @return string The escaped value.
599
     */
600 27
    public function escape($val = null) {
601 27
        if (is_array($val)) {
602 1
            return '[array]';
603 27
        } elseif ($val instanceof \DateTimeInterface) {
604 1
            return htmlspecialchars($val->format(\DateTime::RFC3339));
605 27
        } elseif (is_object($val) && !method_exists($val, '__toString')) {
606 1
            return '{object}';
607
        } else {
608 27
            return htmlspecialchars($val);
609
        }
610
    }
611
612
    /**
613
     * Write children blocks.
614
     *
615
     * @param array|callable|null $children The children blocks to write.
616
     */
617 4
    public function writeChildren($children) {
618 4
        if (empty($children)) {
619
            return;
620 4
        } elseif (is_array($children)) {
621 1
            array_map([$this, 'writeChildren'], $children);
622
        } else {
623 4
            $children();
624
        }
625 4
    }
626
627
    /**
628
     * Massage a dynamic tag name.
629
     *
630
     * @param mixed $expr The result of the tag name expression.
631
     * @param string $tag The default tag name if **$expr** is true.
632
     * @return string Returns the expected tag name.
633
     */
634 5
    protected function tagName($expr, $tag) {
635 5
        if ($expr === true) {
636 3
            return $tag;
637
        }
638 4
        return $expr;
639
    }
640
}
641