Issues (2564)

app/View.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Exception;
23
use InvalidArgumentException;
24
use LogicException;
25
use RuntimeException;
26
use Throwable;
27
28
use function array_key_exists;
29
use function explode;
30
use function extract;
31
use function implode;
32
use function is_file;
33
use function ob_end_clean;
34
use function ob_get_level;
35
use function ob_start;
36
use function sha1;
37
use function str_contains;
38
use function str_ends_with;
39
use function strlen;
40
use function strncmp;
41
42
use const EXTR_OVERWRITE;
43
44
/**
45
 * Simple view/template class.
46
 */
47
class View
48
{
49
    public const string NAMESPACE_SEPARATOR = '::';
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 49 at column 24
Loading history...
50
51
    private const string TEMPLATE_EXTENSION = '.phtml';
52
53
    private string $name;
54
55
    /** @var array<mixed> */
56
    private array $data;
57
58
    /**
59
     * @var array<string> Where do the templates live, for each namespace.
60
     */
61
    private static array $namespaces = [
62
        '' => Webtrees::ROOT_DIR . 'resources/views/',
63
    ];
64
65
    /**
66
     * @var array<string> Modules can replace core views with their own.
67
     */
68
    private static array $replacements = [];
69
70
    /**
71
     * @var string Implementation of Blade "stacks".
72
     */
73
    private static string $stack;
74
75
    /**
76
     * @var array<array<string>> Implementation of Blade "stacks".
77
     */
78
    private static array $stacks = [];
79
80
    /**
81
     * Create a view from a template name and optional data.
82
     *
83
     * @param string       $name
84
     * @param array<mixed> $data
85
     */
86
    public function __construct(string $name, array $data = [])
87
    {
88
        $this->name = $name;
89
        $this->data = $data;
90
    }
91
92
    /**
93
     * Implementation of Blade "stacks".
94
     *
95
     * @see https://laravel.com/docs/5.5/blade#stacks
96
     *
97
     * @param string $stack
98
     *
99
     * @return void
100
     */
101
    public static function push(string $stack): void
102
    {
103
        self::$stack = $stack;
104
105
        ob_start();
106
    }
107
108
    /**
109
     * Implementation of Blade "stacks".
110
     *
111
     * @return void
112
     */
113
    public static function endpush(): void
114
    {
115
        $content = ob_get_clean();
116
117
        if ($content === false) {
118
            throw new LogicException('found endpush(), but did not find push()');
119
        }
120
121
        self::$stacks[self::$stack][] = $content;
122
    }
123
124
    /**
125
     * Variant of push that will only add one copy of each item.
126
     *
127
     * @param string $stack
128
     *
129
     * @return void
130
     */
131
    public static function pushunique(string $stack): void
132
    {
133
        self::$stack = $stack;
134
135
        ob_start();
136
    }
137
138
    /**
139
     * Variant of push that will only add one copy of each item.
140
     *
141
     * @return void
142
     */
143
    public static function endpushunique(): void
144
    {
145
        $content = ob_get_clean();
146
147
        if ($content === false) {
148
            throw new LogicException('found endpushunique(), but did not find pushunique()');
149
        }
150
151
        self::$stacks[self::$stack][sha1($content)] = $content;
152
    }
153
154
    /**
155
     * Implementation of Blade "stacks".
156
     *
157
     * @param string $stack
158
     *
159
     * @return string
160
     */
161
    public static function stack(string $stack): string
162
    {
163
        $content = implode('', self::$stacks[$stack] ?? []);
164
165
        self::$stacks[$stack] = [];
166
167
        return $content;
168
    }
169
170
    /**
171
     * Render a view.
172
     *
173
     * @return string
174
     * @throws Throwable
175
     */
176
    public function render(): string
177
    {
178
        extract($this->data, EXTR_OVERWRITE);
179
180
        try {
181
            ob_start();
182
            // Do not use require, so we can catch errors for missing files
183
            include $this->getFilenameForView($this->name);
184
185
            return (string) ob_get_clean();
186
        } catch (Throwable $ex) {
187
            while (ob_get_level() > 0) {
188
                ob_end_clean();
189
            }
190
            throw $ex;
191
        }
192
    }
193
194
    /**
195
     * @param string $namespace
196
     * @param string $path
197
     *
198
     * @throws InvalidArgumentException
199
     */
200
    public static function registerNamespace(string $namespace, string $path): void
201
    {
202
        if ($namespace === '') {
203
            throw new InvalidArgumentException('Cannot register the default namespace');
204
        }
205
206
        if (!str_ends_with($path, '/')) {
207
            throw new InvalidArgumentException('Paths must end with a directory separator');
208
        }
209
210
        self::$namespaces[$namespace] = $path;
211
    }
212
213
    /**
214
     * @param string $old
215
     * @param string $new
216
     *
217
     * @throws InvalidArgumentException
218
     */
219
    public static function registerCustomView(string $old, string $new): void
220
    {
221
        if (str_contains($old, self::NAMESPACE_SEPARATOR) && str_contains($new, self::NAMESPACE_SEPARATOR)) {
222
            self::$replacements[$old] = $new;
223
        } else {
224
            throw new InvalidArgumentException();
225
        }
226
    }
227
228
    /**
229
     * Find the file for a view.
230
     *
231
     * @param string $view_name
232
     *
233
     * @return string
234
     * @throws Exception
235
     */
236
    public function getFilenameForView(string $view_name): string
237
    {
238
        // If we request "::view", then use it explicitly.  Don't allow replacements.
239
        // NOTE: cannot use str_starts_with() as it wasn't available in 2.0.6, and is called by the upgrade wizard.
240
        $explicit = strncmp($view_name, self::NAMESPACE_SEPARATOR, strlen(self::NAMESPACE_SEPARATOR)) === 0;
241
242
        if (!str_contains($view_name, self::NAMESPACE_SEPARATOR)) {
243
            $view_name = self::NAMESPACE_SEPARATOR . $view_name;
244
        }
245
246
        // Apply replacements / customizations
247
        while (!$explicit && array_key_exists($view_name, self::$replacements)) {
248
            $view_name = self::$replacements[$view_name];
249
        }
250
251
        [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2);
252
253
        if ((self::$namespaces[$namespace] ?? null) === null) {
254
            throw new RuntimeException('Namespace "' . e($namespace) . '" not found.');
255
        }
256
257
        $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION;
258
259
        if (!is_file($view_file)) {
260
            throw new RuntimeException('View file not found: ' . e($view_file));
261
        }
262
263
        return $view_file;
264
    }
265
266
    /**
267
     * Create and render a view in a single operation.
268
     *
269
     * @param string       $name
270
     * @param array<mixed> $data
271
     *
272
     * @return string
273
     */
274
    public static function make(string $name, array $data = []): string
275
    {
276
        $view = new self($name, $data);
277
278
        return $view->render();
279
    }
280
}
281