xref: /webtrees/app/View.php (revision d11be7027e34e3121be11cc025421873364403f9)
1f3281ad6SGreg Roach<?php
23976b470SGreg Roach
3f3281ad6SGreg Roach/**
4f3281ad6SGreg Roach * webtrees: online genealogy
5*d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6f3281ad6SGreg Roach * This program is free software: you can redistribute it and/or modify
7f3281ad6SGreg Roach * it under the terms of the GNU General Public License as published by
8f3281ad6SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9f3281ad6SGreg Roach * (at your option) any later version.
10f3281ad6SGreg Roach * This program is distributed in the hope that it will be useful,
11f3281ad6SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12f3281ad6SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13f3281ad6SGreg Roach * GNU General Public License for more details.
14f3281ad6SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16f3281ad6SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
20f3281ad6SGreg Roachnamespace Fisharebest\Webtrees;
21f3281ad6SGreg Roach
2250c68a25SGreg Roachuse Exception;
233e4bf26fSGreg Roachuse InvalidArgumentException;
24e50f5fc6SGreg Roachuse LogicException;
253e4bf26fSGreg Roachuse RuntimeException;
2670f31542SGreg Roachuse Throwable;
273976b470SGreg Roach
283e4bf26fSGreg Roachuse function array_key_exists;
293e4bf26fSGreg Roachuse function explode;
30d7952a34SGreg Roachuse function extract;
31d7952a34SGreg Roachuse function implode;
32d7952a34SGreg Roachuse function is_file;
33d7952a34SGreg Roachuse function ob_end_clean;
34d3da353bSGreg Roachuse function ob_get_level;
35d7952a34SGreg Roachuse function ob_start;
36d7952a34SGreg Roachuse function sha1;
37dec352c1SGreg Roachuse function str_contains;
38dec352c1SGreg Roachuse function str_ends_with;
39721424feSGreg Roachuse function strlen;
40721424feSGreg Roachuse function strncmp;
4150c68a25SGreg Roach
42ef5d23f1SGreg Roachuse const EXTR_OVERWRITE;
43ef5d23f1SGreg Roach
44f3281ad6SGreg Roach/**
45f3281ad6SGreg Roach * Simple view/template class.
46f3281ad6SGreg Roach */
47c1010edaSGreg Roachclass View
48c1010edaSGreg Roach{
493e4bf26fSGreg Roach    public const NAMESPACE_SEPARATOR = '::';
50dd6b2bfcSGreg Roach
5116d6367aSGreg Roach    private const TEMPLATE_EXTENSION = '.phtml';
52dd6b2bfcSGreg Roach
5343f2f523SGreg Roach    private string $name;
54f3281ad6SGreg Roach
5543f2f523SGreg Roach    /** @var array<mixed> */
5643f2f523SGreg Roach    private array $data;
57f3281ad6SGreg Roach
58f3281ad6SGreg Roach    /**
5909482a55SGreg Roach     * @var array<string> Where do the templates live, for each namespace.
603e4bf26fSGreg Roach     */
615f2657cbSGreg Roach    private static array $namespaces = [
62f397d0fdSGreg Roach        '' => Webtrees::ROOT_DIR . 'resources/views/',
633e4bf26fSGreg Roach    ];
643e4bf26fSGreg Roach
653e4bf26fSGreg Roach    /**
6609482a55SGreg Roach     * @var array<string> Modules can replace core views with their own.
673e4bf26fSGreg Roach     */
685f2657cbSGreg Roach    private static array $replacements = [];
693e4bf26fSGreg Roach
703e4bf26fSGreg Roach    /**
71ecf66805SGreg Roach     * @var string Implementation of Blade "stacks".
72ecf66805SGreg Roach     */
7333c746f1SGreg Roach    private static string $stack;
74ecf66805SGreg Roach
75ecf66805SGreg Roach    /**
7633c746f1SGreg Roach     * @var array<array<string>> Implementation of Blade "stacks".
77ecf66805SGreg Roach     */
7833c746f1SGreg Roach    private static array $stacks = [];
79ecf66805SGreg Roach
80ecf66805SGreg Roach    /**
81f3281ad6SGreg Roach     * Create a view from a template name and optional data.
82f3281ad6SGreg Roach     *
83fa4036e8SGreg Roach     * @param string       $name
845d4b7ec2SGreg Roach     * @param array<mixed> $data
85f3281ad6SGreg Roach     */
8673d58381SGreg Roach    public function __construct(string $name, array $data = [])
87c1010edaSGreg Roach    {
88f3281ad6SGreg Roach        $this->name = $name;
89f3281ad6SGreg Roach        $this->data = $data;
90f3281ad6SGreg Roach    }
91f3281ad6SGreg Roach
92f3281ad6SGreg Roach    /**
93ecf66805SGreg Roach     * Implementation of Blade "stacks".
94ecf66805SGreg Roach     *
95ecf66805SGreg Roach     * @see https://laravel.com/docs/5.5/blade#stacks
96962e29c9SGreg Roach     *
97962e29c9SGreg Roach     * @param string $stack
98fa4036e8SGreg Roach     *
99fa4036e8SGreg Roach     * @return void
100ecf66805SGreg Roach     */
101e364afe4SGreg Roach    public static function push(string $stack): void
102c1010edaSGreg Roach    {
103ecf66805SGreg Roach        self::$stack = $stack;
104d7952a34SGreg Roach
105ecf66805SGreg Roach        ob_start();
106ecf66805SGreg Roach    }
107ecf66805SGreg Roach
108ecf66805SGreg Roach    /**
109ecf66805SGreg Roach     * Implementation of Blade "stacks".
110fa4036e8SGreg Roach     *
111fa4036e8SGreg Roach     * @return void
112ecf66805SGreg Roach     */
113e364afe4SGreg Roach    public static function endpush(): void
114c1010edaSGreg Roach    {
11588de55fdSRico Sonntag        $content = ob_get_clean();
11688de55fdSRico Sonntag
117e50f5fc6SGreg Roach        if ($content === false) {
118e50f5fc6SGreg Roach            throw new LogicException('found endpush(), but did not find push()');
119e50f5fc6SGreg Roach        }
120e50f5fc6SGreg Roach
121d7952a34SGreg Roach        self::$stacks[self::$stack][] = $content;
122d7952a34SGreg Roach    }
123d7952a34SGreg Roach
124d7952a34SGreg Roach    /**
125d7952a34SGreg Roach     * Variant of push that will only add one copy of each item.
126d7952a34SGreg Roach     *
127d7952a34SGreg Roach     * @param string $stack
128d7952a34SGreg Roach     *
129d7952a34SGreg Roach     * @return void
130d7952a34SGreg Roach     */
131e364afe4SGreg Roach    public static function pushunique(string $stack): void
132d7952a34SGreg Roach    {
133d7952a34SGreg Roach        self::$stack = $stack;
134d7952a34SGreg Roach
135d7952a34SGreg Roach        ob_start();
136d7952a34SGreg Roach    }
137d7952a34SGreg Roach
138d7952a34SGreg Roach    /**
139d7952a34SGreg Roach     * Variant of push that will only add one copy of each item.
140d7952a34SGreg Roach     *
141d7952a34SGreg Roach     * @return void
142d7952a34SGreg Roach     */
143e364afe4SGreg Roach    public static function endpushunique(): void
144d7952a34SGreg Roach    {
145d7952a34SGreg Roach        $content = ob_get_clean();
146d7952a34SGreg Roach
147e50f5fc6SGreg Roach        if ($content === false) {
148e50f5fc6SGreg Roach            throw new LogicException('found endpushunique(), but did not find pushunique()');
149e50f5fc6SGreg Roach        }
150e50f5fc6SGreg Roach
151d7952a34SGreg Roach        self::$stacks[self::$stack][sha1($content)] = $content;
152ecf66805SGreg Roach    }
153ecf66805SGreg Roach
154ecf66805SGreg Roach    /**
155ecf66805SGreg Roach     * Implementation of Blade "stacks".
156ecf66805SGreg Roach     *
157962e29c9SGreg Roach     * @param string $stack
158962e29c9SGreg Roach     *
159ecf66805SGreg Roach     * @return string
160ecf66805SGreg Roach     */
161c1010edaSGreg Roach    public static function stack(string $stack): string
162c1010edaSGreg Roach    {
163ecf66805SGreg Roach        $content = implode('', self::$stacks[$stack] ?? []);
164ecf66805SGreg Roach
165ecf66805SGreg Roach        self::$stacks[$stack] = [];
166ecf66805SGreg Roach
167ecf66805SGreg Roach        return $content;
168ecf66805SGreg Roach    }
169ecf66805SGreg Roach
170ecf66805SGreg Roach    /**
171f3281ad6SGreg Roach     * Render a view.
172f3281ad6SGreg Roach     *
173f3281ad6SGreg Roach     * @return string
17470f31542SGreg Roach     * @throws Throwable
175f3281ad6SGreg Roach     */
1768f53f488SRico Sonntag    public function render(): string
177c1010edaSGreg Roach    {
178ef5d23f1SGreg Roach        extract($this->data, EXTR_OVERWRITE);
179f3281ad6SGreg Roach
18070f31542SGreg Roach        try {
181f3281ad6SGreg Roach            ob_start();
182bc241c54SGreg Roach            // Do not use require, so we can catch errors for missing files
183bc241c54SGreg Roach            include $this->getFilenameForView($this->name);
18475d70144SGreg Roach
185ac701fbdSGreg Roach            return (string) ob_get_clean();
18670f31542SGreg Roach        } catch (Throwable $ex) {
187d3da353bSGreg Roach            while (ob_get_level() > 0) {
18870f31542SGreg Roach                ob_end_clean();
189d3da353bSGreg Roach            }
19070f31542SGreg Roach            throw $ex;
19170f31542SGreg Roach        }
192f3281ad6SGreg Roach    }
193f3281ad6SGreg Roach
194f3281ad6SGreg Roach    /**
1953e4bf26fSGreg Roach     * @param string $namespace
1963e4bf26fSGreg Roach     * @param string $path
1973e4bf26fSGreg Roach     *
1983e4bf26fSGreg Roach     * @throws InvalidArgumentException
1993e4bf26fSGreg Roach     */
2003e4bf26fSGreg Roach    public static function registerNamespace(string $namespace, string $path): void
2013e4bf26fSGreg Roach    {
2023e4bf26fSGreg Roach        if ($namespace === '') {
2033e4bf26fSGreg Roach            throw new InvalidArgumentException('Cannot register the default namespace');
2043e4bf26fSGreg Roach        }
2053e4bf26fSGreg Roach
206dec352c1SGreg Roach        if (!str_ends_with($path, '/')) {
2073e4bf26fSGreg Roach            throw new InvalidArgumentException('Paths must end with a directory separator');
2083e4bf26fSGreg Roach        }
2093e4bf26fSGreg Roach
2103e4bf26fSGreg Roach        self::$namespaces[$namespace] = $path;
2113e4bf26fSGreg Roach    }
2123e4bf26fSGreg Roach
2133e4bf26fSGreg Roach    /**
2143e4bf26fSGreg Roach     * @param string $old
2153e4bf26fSGreg Roach     * @param string $new
2163e4bf26fSGreg Roach     *
2173e4bf26fSGreg Roach     * @throws InvalidArgumentException
2183e4bf26fSGreg Roach     */
2193e4bf26fSGreg Roach    public static function registerCustomView(string $old, string $new): void
2203e4bf26fSGreg Roach    {
221dec352c1SGreg Roach        if (str_contains($old, self::NAMESPACE_SEPARATOR) && str_contains($new, self::NAMESPACE_SEPARATOR)) {
2223e4bf26fSGreg Roach            self::$replacements[$old] = $new;
2233e4bf26fSGreg Roach        } else {
2243e4bf26fSGreg Roach            throw new InvalidArgumentException();
2253e4bf26fSGreg Roach        }
2263e4bf26fSGreg Roach    }
2273e4bf26fSGreg Roach
2283e4bf26fSGreg Roach    /**
2293e4bf26fSGreg Roach     * Find the file for a view.
230f3281ad6SGreg Roach     *
231f3281ad6SGreg Roach     * @param string $view_name
232f3281ad6SGreg Roach     *
23375d70144SGreg Roach     * @return string
23450c68a25SGreg Roach     * @throws Exception
235f3281ad6SGreg Roach     */
2363e4bf26fSGreg Roach    public function getFilenameForView(string $view_name): string
237c1010edaSGreg Roach    {
238dec352c1SGreg Roach        // If we request "::view", then use it explicitly.  Don't allow replacements.
239721424feSGreg Roach        // NOTE: cannot use str_starts_with() as it wasn't available in 2.0.6, and is called by the upgrade wizard.
240721424feSGreg Roach        $explicit = strncmp($view_name, self::NAMESPACE_SEPARATOR, strlen(self::NAMESPACE_SEPARATOR)) === 0;
241fdbcd0efSGreg Roach
242dec352c1SGreg Roach        if (!str_contains($view_name, self::NAMESPACE_SEPARATOR)) {
2433e4bf26fSGreg Roach            $view_name = self::NAMESPACE_SEPARATOR . $view_name;
2443e4bf26fSGreg Roach        }
24575d70144SGreg Roach
246dec352c1SGreg Roach        // Apply replacements / customizations
247fdbcd0efSGreg Roach        while (!$explicit && array_key_exists($view_name, self::$replacements)) {
2483e4bf26fSGreg Roach            $view_name = self::$replacements[$view_name];
2493e4bf26fSGreg Roach        }
2503e4bf26fSGreg Roach
2513e4bf26fSGreg Roach        [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2);
2523e4bf26fSGreg Roach
2533e4bf26fSGreg Roach        if ((self::$namespaces[$namespace] ?? null) === null) {
2543e4bf26fSGreg Roach            throw new RuntimeException('Namespace "' . e($namespace) . '" not found.');
2553e4bf26fSGreg Roach        }
2563e4bf26fSGreg Roach
2573e4bf26fSGreg Roach        $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION;
2583e4bf26fSGreg Roach
2593e4bf26fSGreg Roach        if (!is_file($view_file)) {
2603e4bf26fSGreg Roach            throw new RuntimeException('View file not found: ' . e($view_file));
2613e4bf26fSGreg Roach        }
2623e4bf26fSGreg Roach
26350c68a25SGreg Roach        return $view_file;
264f21917b2SGreg Roach    }
26550c68a25SGreg Roach
266f3281ad6SGreg Roach    /**
267219fc02dSGreg Roach     * Create and render a view in a single operation.
268f3281ad6SGreg Roach     *
269f3281ad6SGreg Roach     * @param string       $name
27009482a55SGreg Roach     * @param array<mixed> $data
271f3281ad6SGreg Roach     *
272f3281ad6SGreg Roach     * @return string
273f3281ad6SGreg Roach     */
27424f2a3afSGreg Roach    public static function make(string $name, array $data = []): string
275c1010edaSGreg Roach    {
2761e752ebbSGreg Roach        $view = new self($name, $data);
277f3281ad6SGreg Roach
278f3281ad6SGreg Roach        return $view->render();
279f3281ad6SGreg Roach    }
280f3281ad6SGreg Roach}
281