Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
Resolver
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
5 / 5
19
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
 resolve
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 segments
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 path
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 prepareDirs
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Boiler;
6
7use Celemas\Boiler\Exception\LookupException;
8use Celemas\Boiler\Exception\UnexpectedValueException;
9use Override;
10
11/**
12 * @psalm-type DirsInput = non-empty-string|list<non-empty-string>|array<non-empty-string, non-empty-string>
13 * @psalm-type Dirs = non-empty-list<non-empty-string>|non-empty-array<non-empty-string, non-empty-string>
14 */
15final class Resolver implements Contract\Resolver
16{
17    /** @psalm-var Dirs */
18    private readonly array $dirs;
19    /** @var array<string, non-empty-string> */
20    private array $pathCache = [];
21
22    /** @psalm-param DirsInput $dirs */
23    public function __construct(
24        array|string $dirs,
25    ) {
26        $this->dirs = $this->prepareDirs($dirs);
27    }
28
29    /** @return non-empty-string */
30    #[Override]
31    public function resolve(string $path): string
32    {
33        if (isset($this->pathCache[$path])) {
34            return $this->pathCache[$path];
35        }
36
37        if (!preg_match('/^[\w\.\/:_-]+$/u', $path)) {
38            throw new UnexpectedValueException('The template path is invalid or empty');
39        }
40
41        [$namespace, $file] = $this->segments($path);
42        $candidate = $this->path($namespace, $file);
43
44        if (!$candidate->isValid()) {
45            throw new LookupException($candidate->error());
46        }
47
48        return $this->pathCache[$path] = $candidate->path();
49    }
50
51    /** @return list{null|non-empty-string, non-empty-string} */
52    private function segments(string $path): array
53    {
54        if (!str_contains($path, ':')) {
55            $path = trim($path);
56            assert($path !== '', 'Template path must not be empty after trimming');
57
58            return [null, $path];
59        }
60
61        $segments = array_map(static fn($seg) => trim($seg), explode(':', $path));
62
63        if (count($segments) === 2) {
64            [$namespace, $file] = $segments;
65
66            if ($namespace !== '' && $file !== '') {
67                return [$namespace, $file];
68            }
69
70            throw new LookupException(
71                "Invalid template format: '{$path}'. " . "Use 'namespace:template/path or template/path'.",
72            );
73        }
74
75        throw new LookupException(
76            "Invalid template format: '{$path}'. " . "Use 'namespace:template/path or template/path'.",
77        );
78    }
79
80    /** @param non-empty-string $file */
81    private function path(?string $namespace, string $file): Path
82    {
83        if ($namespace !== null) {
84            if (array_key_exists($namespace, $this->dirs)) {
85                return new Path($this->dirs[$namespace], $file);
86            }
87
88            throw new LookupException("Template namespace `{$namespace}` does not exist");
89        }
90
91        foreach ($this->dirs as $dir) {
92            $candidate = new Path($dir, $file);
93
94            if ($candidate->isValid()) {
95                return $candidate;
96            }
97        }
98
99        return $candidate;
100    }
101
102    /**
103     * @psalm-param DirsInput $dirs
104     *
105     * @psalm-return Dirs
106     */
107    private function prepareDirs(array|string $dirs): array
108    {
109        $preparePath = static function (string $dir): string {
110            $realpath = realpath($dir);
111
112            if ($realpath === false) {
113                throw new LookupException(
114                    'Template directory does not exist ' . $dir,
115                );
116            }
117
118            assert($realpath !== '', 'Resolved template directory path must not be empty');
119
120            return $realpath;
121        };
122
123        if (is_string($dirs)) {
124            return [$preparePath($dirs)];
125        }
126
127        if ($dirs === []) {
128            throw new LookupException('At least one template directory must be configured');
129        }
130
131        return array_map(
132            static function ($dir) use ($preparePath) {
133                return $preparePath($dir);
134            },
135            $dirs,
136        );
137    }
138}