Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Methods
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
4 / 4
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isReserved
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Boiler;
6
7use Celema\Boiler\Exception\UnexpectedValueException;
8use ReflectionClass;
9use ReflectionMethod;
10
11/** @internal */
12final class Methods
13{
14    /** @var array<non-empty-string, Method> */
15    private array $methods = [];
16
17    /**
18     * A template's registry layers over its engine's: methods added here stay
19     * on the template, while methods added to the engine, even later, remain
20     * visible unless the template overrides them.
21     */
22    public function __construct(
23        private readonly ?self $parent = null,
24    ) {}
25
26    /** @param non-empty-string $name */
27    public function add(string $name, callable $callable, bool $safe = false): void
28    {
29        if (self::isReserved($name)) {
30            throw new UnexpectedValueException("Method name `{$name}` is reserved by a template helper");
31        }
32
33        $this->methods[$name] = new Method($callable, $safe);
34    }
35
36    public function get(string $name): Method
37    {
38        if (array_key_exists($name, $this->methods)) {
39            return $this->methods[$name];
40        }
41
42        if ($this->parent !== null) {
43            return $this->parent->get($name);
44        }
45
46        throw new UnexpectedValueException("Method '{$name}' does not exist");
47    }
48
49    /**
50     * Templates reach registered methods through Context::__call(), which PHP
51     * only calls for names the context does not define itself, ignoring case.
52     * Templates run in the scope of the context, so its non-public methods
53     * count too.
54     */
55    private static function isReserved(string $name): bool
56    {
57        return in_array(
58            strtolower($name),
59            array_map(
60                static fn(ReflectionMethod $method): string => strtolower($method->name),
61                new ReflectionClass(Context::class)->getMethods(),
62            ),
63            true,
64        );
65    }
66}