Completed
Pull Request — master (#22)
by Todd
02:46
created

Ebi::writeCompileException()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 43
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 8.439
c 0
b 0
f 0
ccs 0
cts 32
cp 0
cc 6
eloc 28
nc 6
nop 1
crap 42
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 60
    public function __construct(TemplateLoaderInterface $templateLoader, $cachePath, CompilerInterface $compiler = null) {
45 60
        $this->templateLoader = $templateLoader;
46 60
        $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 60
        $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 60
        $this->defineFunction('abs');
50 60
        $this->defineFunction('arrayColumn', 'array_column');
51 60
        $this->defineFunction('arrayKeyExists', 'array_key_exists');
52 60
        $this->defineFunction('arrayKeys', 'array_keys');
53 60
        $this->defineFunction('arrayMerge', 'array_merge');
54 60
        $this->defineFunction('arrayMergeRecursive', 'array_merge_recursive');
55 60
        $this->defineFunction('arrayReplace', 'array_replace');
56 60
        $this->defineFunction('arrayReplaceRecursive', 'array_replace_recursive');
57 60
        $this->defineFunction('arrayReverse', 'array_reverse');
58 60
        $this->defineFunction('arrayValues', 'array_values');
59 60
        $this->defineFunction('base64Encode', 'base64_encode');
60 60
        $this->defineFunction('ceil');
61 60
        $this->defineFunction('componentExists', [$this, 'componentExists']);
62 60
        $this->defineFunction('count');
63 60
        $this->defineFunction('empty');
64 60
        $this->defineFunction('floor');
65 60
        $this->defineFunction('formatDate', [$this, 'formatDate']);
66 60
        $this->defineFunction('formatNumber', 'number_format');
67 60
        $this->defineFunction('htmlEncode', 'htmlspecialchars');
68 60
        $this->defineFunction('join');
69 60
        $this->defineFunction('lcase', $this->mb('strtolower'));
70 60
        $this->defineFunction('lcfirst');
71 60
        $this->defineFunction('ltrim');
72 60
        $this->defineFunction('max');
73 60
        $this->defineFunction('min');
74 60
        $this->defineFunction('queryEncode', 'http_build_query');
75 60
        $this->defineFunction('round');
76 60
        $this->defineFunction('rtrim');
77 60
        $this->defineFunction('sprintf');
78 60
        $this->defineFunction('strlen', $this->mb('strlen'));
79 60
        $this->defineFunction('substr', $this->mb('substr'));
80 60
        $this->defineFunction('trim');
81 60
        $this->defineFunction('ucase', $this->mb('strtoupper'));
82 60
        $this->defineFunction('ucfirst');
83 60
        $this->defineFunction('ucwords');
84 60
        $this->defineFunction('urlencode', 'rawurlencode');
85
86 60
        $this->defineFunction('@class', [$this, 'attributeClass']);
87
88
        // Define a simple component not found component to help troubleshoot.
89
        $this->defineComponent('@component-not-found', function ($props) {
90 1
            echo '<!-- Ebi component "'.htmlspecialchars($props['component']).'" not found. -->';
91 60
        });
92
93
        // Define a simple component exception.
94
        $this->defineComponent('@exception', function ($props) {
95 1
            echo "\n<!--\nEbi exception in component \"".htmlspecialchars($props['component'])."\".\n".
96 1
                htmlspecialchars($props['message'])."\n-->\n";
97
98 60
        });
99
100 60
        $this->defineComponent('@compile-exception', [$this, 'writeCompileException']);
101 60
    }
102
103
    /**
104
     * Register a runtime function.
105
     *
106
     * @param string $name The name of the function.
107
     * @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...
108
     */
109 60
    public function defineFunction($name, $function = null) {
110 60
        if ($function === null) {
111 60
            $function = $name;
112 60
        }
113
114 60
        $this->functions[strtolower($name)] = $function;
115 60
        $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...
116 60
    }
117
118 60
    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...
119 60
        return function_exists("mb_$func") ? "mb_$func" : $func;
120
    }
121
122
    /**
123
     * Write a component to the output buffer.
124
     *
125
     * @param string $component The name of the component.
126
     * @param array ...$args
127
     */
128 13
    public function write($component, ...$args) {
129 13
        $component = strtolower($component);
130
131
        try {
132 13
            $callback = $this->lookup($component);
133
134 13
            if (is_callable($callback)) {
135 13
                call_user_func($callback, ...$args);
136 13
            } else {
137 1
                $this->write('@component-not-found', ['component' => $component]);
138
            }
139 13
        } 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...
140
            $this->write('@exception', ['message' => $ex->getMessage(), 'code', $ex->getCode(), 'component' => $component]);
141
            return;
142 1
        } catch (\Exception $ex) {
143 1
            $this->write('@exception', ['message' => $ex->getMessage(), 'code', $ex->getCode(), 'component' => $component]);
144 1
            return;
145
        }
146 13
    }
147
148
    /**
149
     * Lookup a component with a given name.
150
     *
151
     * @param string $component The component to lookup.
152
     * @return callable|null Returns the component function or **null** if the component is not found.
153
     */
154 55
    public function lookup($component) {
155 55
        $component = strtolower($component);
156 55
        $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...
157
158 55
        if (!array_key_exists($key, $this->components)) {
159 50
            $this->loadComponent($component);
160 50
        }
161
162 55
        if (isset($this->components[$key])) {
163 53
            return $this->components[$key];
164
        } else {
165
            // Mark a tombstone to the component array so it doesn't keep getting loaded.
166 3
            $this->components[$key] = null;
167 3
            return null;
168
        }
169
    }
170
171
    /**
172
     * Check to see if a component exists.
173
     *
174
     * @param string $component The name of the component.
175
     * @param bool $loader Whether or not to use the component loader or just look in the component cache.
176
     * @return bool Returns **true** if the component exists or **false** otherwise.
177
     */
178 2
    public function componentExists($component, $loader = true) {
179 2
        $componentKey = $this->componentKey($component);
180 2
        if (array_key_exists($componentKey, $this->components)) {
181 1
            return $this->components[$componentKey] !== null;
182 2
        } elseif ($loader) {
183 2
            return !empty($this->templateLoader->cacheKey($component));
184
        }
185 1
        return false;
186
    }
187
188
    /**
189
     * Strip the namespace off a component name to get the component key.
190
     *
191
     * @param string $component The full name of the component with a possible namespace.
192
     * @return string Returns the component key.
193
     */
194 57
    protected function componentKey($component) {
195 57
        if (false !== $pos = strpos($component, ':')) {
196 1
            $component = substr($component, $pos + 1);
197 1
        }
198 57
        return strtolower($component);
199
    }
200
201
    /**
202
     * Load a component.
203
     *
204
     * @param string $component The name of the component to load.
205
     * @return callable|null Returns the component or **null** if the component isn't found.
206
     */
207 50
    protected function loadComponent($component) {
208 50
        $cacheKey = $this->templateLoader->cacheKey($component);
209
        // The template loader can tell us a template doesn't exist when giving the cache key.
210 50
        if (empty($cacheKey)) {
211 3
            return null;
212
        }
213
214 47
        $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...
215 47
        $componentKey = $this->componentKey($component);
216
217 47
        if (!file_exists($cachePath)) {
218 47
            $src = $this->templateLoader->load($component);
219
            try {
220 47
                return $this->compile($componentKey, $src, $cacheKey);
221
            } catch (CompileException $ex) {
222
                $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...
223
                return $this->components[$componentKey] = function() use ($props) {
224
                    $this->write('@compile-exception', $props);
225
                };
226
            }
227
        } else {
228
            return $this->includeComponent($componentKey, $cachePath);
229
        }
230
    }
231
232
    protected function writeCompileException($props) {
233
        echo "\n<section class=\"ebi-ex\">\n",
234
            '<h2>Error compiling '.htmlspecialchars($props['path'])." near line {$props['line']}.</h2>\n";
235
236
        echo '<p class="ebi-ex-message">'.htmlspecialchars($props['message'])."</p>\n";
237
238
        if (!empty($props['context'])) {
239
            $source = $props['context']['source'];
240
            if (isset($props['context']['position'])) {
241
                $pos = $props['context']['position'];
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...
242
                $source = htmlspecialchars(substr($source, 0, $pos)).
243
                    '<mark class="ebi-ex-highlight">'.htmlspecialchars(substr($source, $pos, 1)).'</mark>'.
244
                    htmlspecialchars(substr($source, $pos + 1));
245
            } else {
246
                $source = htmlspecialchars($source);
247
            }
248
249
            echo '<pre class="ebi-ex-source ebi-ex-context"><code>',
250
                $source,
251
                "</code></pre>\n";
252
        }
253
254
        if (!empty($props['lines'])) {
255
            echo '<pre class="ebi-ex-source ebi-ex-lines">';
256
257
            foreach ($props['lines'] as $i => $line) {
258
                echo '<code class="ebi-ex-line">';
259
260
                $str = sprintf("%3d. %s", $i, htmlspecialchars($line));
261
                if ($i === $props['line']) {
262
                    echo "<mark class=\"ebi-ex-highlight\">$str</mark>";
263
                } else {
264
                    echo $str;
265
                }
266
267
                echo "</code>\n";
268
            }
269
270
            echo "</pre>\n";
271
        }
272
273
        echo "</section>\n";
274
    }
275
276
    /**
277
     * Check to see if a specific cache key exists in the cache.
278
     *
279
     * @param string $cacheKey The cache key to check.
280
     * @return bool Returns **true** if there is a cache key at the file or **false** otherwise.
281
     */
282 1
    public function cacheKeyExists($cacheKey) {
283 1
        $cachePath = "{$this->cachePath}/$cacheKey.php";
284 1
        return file_exists($cachePath);
285
    }
286
287
    /**
288
     * Compile a component from source, cache it and include it.
289
     *
290
     * @param string $component The name of the component.
291
     * @param string $src The component source.
292
     * @param string $cacheKey The cache key of the component.
293
     * @return callable|null Returns the compiled component closure.
294
     */
295 51
    public function compile($component, $src, $cacheKey) {
296 51
        $cachePath = "{$this->cachePath}/$cacheKey.php";
297 51
        $component = strtolower($component);
298
299 51
        $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...
300 51
        $comment = "/*\n".str_replace('*/', '❄/', trim($src))."\n*/";
301
302 51
        $this->filePutContents($cachePath, "<?php\n$comment\n$php");
303
304 51
        return $this->includeComponent($component, $cachePath);
305
    }
306
307
    /**
308
     * Include a cached component.
309
     *
310
     * @param string $component The component key.
311
     * @param string $cachePath The path to the component.
312
     * @return callable|null Returns the component function or **null** if the component wasn't properly defined.
313
     */
314 51
    private function includeComponent($component, $cachePath) {
315 51
        unset($this->components[$component]);
316 51
        $fn = $this->requireFile($cachePath);
317
318 51
        if (isset($this->components[$component])) {
319 51
            return $this->components[$component];
320
        } elseif (is_callable($fn)) {
321
            $this->defineComponent($component, $fn);
322
            return $fn;
323
        } else {
324
            $this->components[$component] = null;
325
            return null;
326
        }
327
    }
328
329
    /**
330
     * A safe version of {@link file_put_contents()} that also clears op caches.
331
     *
332
     * @param string $path The path to save to.
333
     * @param string $contents The contents of the file.
334
     * @return bool Returns **true** on success or **false** on failure.
335
     */
336 51
    private function filePutContents($path, $contents) {
337 51
        if (!file_exists(dirname($path))) {
338 4
            mkdir(dirname($path), 0777, true);
339 4
        }
340 51
        $tmpPath = tempnam(dirname($path), 'ebi-');
341 51
        $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...
342 51
        if (file_put_contents($tmpPath, $contents) !== false) {
343 51
            chmod($tmpPath, 0664);
344 51
            $r = rename($tmpPath, $path);
345 51
        }
346
347 51
        if (function_exists('apc_delete_file')) {
348
            // This fixes a bug with some configurations of apc.
349
            @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...
350 51
        } elseif (function_exists('opcache_invalidate')) {
351 51
            @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...
352 51
        }
353
354 51
        return $r;
355
    }
356
357
    /**
358
     * Include a file.
359
     *
360
     * This is method is useful for including a file bound to this object instance.
361
     *
362
     * @param string $path The path to the file to include.
363
     * @return mixed Returns the result of the include.
364
     */
365 51
    public function requireFile($path) {
366 51
        return require $path;
367
    }
368
369
    /**
370
     * Register a component.
371
     *
372
     * @param string $name The name of the component to register.
373
     * @param callable $component The component function.
374
     */
375 60
    public function defineComponent($name, callable $component) {
376 60
        $this->components[$name] = $component;
377 60
    }
378
379
    /**
380
     * Render a component to a string.
381
     *
382
     * @param string $component The name of the component to render.
383
     * @param array ...$args Arguments to pass to the component.
384
     * @return string|null Returns the rendered component or **null** if the component was not found.
385
     */
386 51
    public function render($component, ...$args) {
387 51
        if ($callback = $this->lookup($component)) {
388 50
            ob_start();
389 50
            $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
390 50
            call_user_func($callback, ...$args);
391 50
            error_reporting($errs);
392 50
            $str = ob_get_clean();
393 50
            return $str;
394
        } else {
395 1
            trigger_error("Could not find component $component.", E_USER_NOTICE);
396
            return null;
397
        }
398
    }
399
400
    /**
401
     * Set the error reporting appropriate for template rendering.
402
     *
403
     * @return int Returns the previous error level.
404
     */
405
    public function setErrorReporting() {
406
        $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
407
        return $errs;
408
    }
409
410
    /**
411
     * Call a function registered with **defineFunction()**.
412
     *
413
     * If a static or global function is registered then it's simply rendered in the compiled template.
414
     * This method is for closures or callbacks.
415
     *
416
     * @param string $name The name of the registered function.
417
     * @param array ...$args The function's argument.
418
     * @return mixed Returns the result of the function
419
     * @throws RuntimeException Throws an exception when the function isn't found.
420
     */
421 3
    public function call($name, ...$args) {
422 3
        if (!isset($this->functions[$name])) {
423 1
            throw new RuntimeException("Call to undefined function $name.", 500);
424
        } else {
425 2
            return $this->functions[$name](...$args);
426
        }
427
    }
428
429
    /**
430
     * Render a variable appropriately for CSS.
431
     *
432
     * This is a convenience runtime function.
433
     *
434
     * @param string|array $expr A CSS class, an array of CSS classes, or an associative array where the keys are class
435
     * names and the values are truthy conditions to include the class (or not).
436
     * @return string Returns a space-delimited CSS class string.
437
     */
438 6
    public function attributeClass($expr) {
439 6
        if (is_array($expr)) {
440 3
            $classes = [];
441 3
            foreach ($expr as $i => $val) {
442 3
                if (is_array($val)) {
443 1
                    $classes[] = $this->attributeClass($val);
444 3
                } elseif (is_int($i)) {
445 1
                    $classes[] = $val;
446 3
                } elseif (!empty($val)) {
447 2
                    $classes[] = $i;
448 2
                }
449 3
            }
450 3
            return implode(' ', $classes);
451
        } else {
452 3
            return (string)$expr;
453
        }
454
    }
455
456
    /**
457
     * Format a data.
458
     *
459
     * @param mixed $date The date to format. This can be a string data, a timestamp or an instance of **DateTimeInterface**.
460
     * @param string $format The format of the date.
461
     * @return string Returns the formatted data.
462
     * @see date_format()
463
     */
464 1
    public function formatDate($date, $format = 'c') {
465 1
        if (is_string($date)) {
466
            try {
467 1
                $date = new \DateTimeImmutable($date);
468 1
            } catch (\Exception $ex) {
469
                return '#error#';
470
            }
471 1
        } elseif (empty($date)) {
472
            return '';
473
        } elseif (is_int($date)) {
474
            try {
475
                $date = new \DateTimeImmutable('@'.$date);
476
            } catch (\Exception $ex) {
477
                return '#error#';
478
            }
479
        } elseif (!$date instanceof \DateTimeInterface) {
480
            return '#error#';
481
        }
482
483 1
        return $date->format($format);
484
    }
485
486
    /**
487
     * Get a single item from the meta array.
488
     *
489
     * @param string $name The key to get from.
490
     * @param mixed $default The default value if no item at the key exists.
491
     * @return mixed Returns the meta value.
492
     */
493
    public function getMeta($name, $default = null) {
494
        return isset($this->meta[$name]) ? $this->meta[$name] : $default;
495
    }
496
497
    /**
498
     * Set a single item to the meta array.
499
     *
500
     * @param string $name The key to set.
501
     * @param mixed $value The new value.
502
     * @return $this
503
     */
504 1
    public function setMeta($name, $value) {
505 1
        $this->meta[$name] = $value;
506 1
        return $this;
507
    }
508
509
    /**
510
     * Get the template loader.
511
     *
512
     * The template loader translates component names into template contents.
513
     *
514
     * @return TemplateLoaderInterface Returns the template loader.
515
     */
516 1
    public function getTemplateLoader() {
517 1
        return $this->templateLoader;
518
    }
519
520
    /**
521
     * Set the template loader.
522
     *
523
     * The template loader translates component names into template contents.
524
     *
525
     * @param TemplateLoaderInterface $templateLoader The new template loader.
526
     * @return $this
527
     */
528
    public function setTemplateLoader($templateLoader) {
529
        $this->templateLoader = $templateLoader;
530
        return $this;
531
    }
532
533
    /**
534
     * Get the entire meta array.
535
     *
536
     * @return array Returns the meta.
537
     */
538
    public function getMetaArray() {
539
        return $this->meta;
540
    }
541
542
    /**
543
     * Set the entire meta array.
544
     *
545
     * @param array $meta The new meta array.
546
     * @return $this
547
     */
548
    public function setMetaArray(array $meta) {
549
        $this->meta = $meta;
550
        return $this;
551
    }
552
553
    /**
554
     * Return a dynamic attribute.
555
     *
556
     * The attribute renders differently depending on the value.
557
     *
558
     * - If the value is **true** then it will render as an HTML5 boolean attribute.
559
     * - If the value is **false** or **null** then the attribute will not render.
560
     * - Other values render as attribute values.
561
     * - Attributes that start with **aria-** render **true** and **false** as values.
562
     *
563
     * @param string $name The name of the attribute.
564
     * @param mixed $value The value of the attribute.
565
     * @return string Returns the attribute definition or an empty string.
566
     */
567 15
    protected function attribute($name, $value) {
568 15
        if (substr($name, 0, 5) === 'aria-' && is_bool($value)) {
569 2
            $value = $value ? 'true' : 'false';
570 2
        }
571
572 15
        if ($value === true) {
573 1
            return ' '.$name;
574 14
        } elseif (!in_array($value, [null, false], true)) {
575 11
            return " $name=\"".htmlspecialchars($value).'"';
576
        }
577 4
        return '';
578
    }
579
}
580