xref: /webtrees/app/View.php (revision d3da353b24988503c1bc73f29b8ef3ab86d7b4da)
1f3281ad6SGreg Roach<?php
2f3281ad6SGreg Roach/**
3f3281ad6SGreg Roach * webtrees: online genealogy
48fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team
5f3281ad6SGreg Roach * This program is free software: you can redistribute it and/or modify
6f3281ad6SGreg Roach * it under the terms of the GNU General Public License as published by
7f3281ad6SGreg Roach * the Free Software Foundation, either version 3 of the License, or
8f3281ad6SGreg Roach * (at your option) any later version.
9f3281ad6SGreg Roach * This program is distributed in the hope that it will be useful,
10f3281ad6SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
11f3281ad6SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12f3281ad6SGreg Roach * GNU General Public License for more details.
13f3281ad6SGreg Roach * You should have received a copy of the GNU General Public License
14f3281ad6SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
15f3281ad6SGreg Roach */
16e7f56f2aSGreg Roachdeclare(strict_types=1);
17e7f56f2aSGreg Roach
18f3281ad6SGreg Roachnamespace Fisharebest\Webtrees;
19f3281ad6SGreg Roach
2050c68a25SGreg Roachuse Exception;
213e4bf26fSGreg Roachuse Illuminate\Support\Str;
223e4bf26fSGreg Roachuse InvalidArgumentException;
233e4bf26fSGreg Roachuse RuntimeException;
2470f31542SGreg Roachuse Throwable;
253e4bf26fSGreg Roachuse function array_key_exists;
263e4bf26fSGreg Roachuse function explode;
27d7952a34SGreg Roachuse function extract;
28d7952a34SGreg Roachuse function implode;
29d7952a34SGreg Roachuse function is_file;
30d7952a34SGreg Roachuse function ob_end_clean;
31*d3da353bSGreg Roachuse function ob_get_level;
32d7952a34SGreg Roachuse function ob_start;
33d7952a34SGreg Roachuse function sha1;
3450c68a25SGreg Roach
35f3281ad6SGreg Roach/**
36f3281ad6SGreg Roach * Simple view/template class.
37f3281ad6SGreg Roach */
38c1010edaSGreg Roachclass View
39c1010edaSGreg Roach{
403e4bf26fSGreg Roach    public const NAMESPACE_SEPARATOR = '::';
41dd6b2bfcSGreg Roach
42dd6b2bfcSGreg Roach    // File extension for our template files.
4316d6367aSGreg Roach    private const TEMPLATE_EXTENSION = '.phtml';
44dd6b2bfcSGreg Roach
45f3281ad6SGreg Roach    /**
46f3281ad6SGreg Roach     * @var string The (file) name of the view.
47f3281ad6SGreg Roach     */
48f3281ad6SGreg Roach    private $name;
49f3281ad6SGreg Roach
50f3281ad6SGreg Roach    /**
51f3281ad6SGreg Roach     * @var mixed[] Data to be inserted into the view.
52f3281ad6SGreg Roach     */
53f3281ad6SGreg Roach    private $data;
54f3281ad6SGreg Roach
55f3281ad6SGreg Roach    /**
563e4bf26fSGreg Roach     * @var string[] Where do the templates live, for each namespace.
573e4bf26fSGreg Roach     */
583e4bf26fSGreg Roach    private static $namespaces = [
59f397d0fdSGreg Roach        '' => Webtrees::ROOT_DIR . 'resources/views/',
603e4bf26fSGreg Roach    ];
613e4bf26fSGreg Roach
623e4bf26fSGreg Roach    /**
633e4bf26fSGreg Roach     * @var string[] Modules can replace core views with their own.
643e4bf26fSGreg Roach     */
653e4bf26fSGreg Roach    private static $replacements = [];
663e4bf26fSGreg Roach
673e4bf26fSGreg Roach    /**
6808d24c7aSGreg Roach     * @var mixed[] Data to be inserted into all views.
6908d24c7aSGreg Roach     */
7008d24c7aSGreg Roach    private static $shared_data = [];
7108d24c7aSGreg Roach
7208d24c7aSGreg Roach    /**
73ecf66805SGreg Roach     * @var string Implementation of Blade "stacks".
74ecf66805SGreg Roach     */
75ecf66805SGreg Roach    private static $stack;
76ecf66805SGreg Roach
77ecf66805SGreg Roach    /**
78ecf66805SGreg Roach     * @var array[] Implementation of Blade "stacks".
79ecf66805SGreg Roach     */
80ecf66805SGreg Roach    private static $stacks = [];
81ecf66805SGreg Roach
82ecf66805SGreg Roach    /**
83f3281ad6SGreg Roach     * Createa view from a template name and optional data.
84f3281ad6SGreg Roach     *
85fa4036e8SGreg Roach     * @param string $name
86f3281ad6SGreg Roach     * @param array  $data
87f3281ad6SGreg Roach     */
88fa4036e8SGreg Roach    public function __construct(string $name, $data = [])
89c1010edaSGreg Roach    {
90f3281ad6SGreg Roach        $this->name = $name;
91f3281ad6SGreg Roach        $this->data = $data;
92f3281ad6SGreg Roach    }
93f3281ad6SGreg Roach
94f3281ad6SGreg Roach    /**
9508d24c7aSGreg Roach     * Shared data that is available to all views.
96962e29c9SGreg Roach     *
97962e29c9SGreg Roach     * @param string $key
98962e29c9SGreg Roach     * @param mixed  $value
99fa4036e8SGreg Roach     *
100fa4036e8SGreg Roach     * @return void
10108d24c7aSGreg Roach     */
102e364afe4SGreg Roach    public static function share(string $key, $value): void
103c1010edaSGreg Roach    {
10408d24c7aSGreg Roach        self::$shared_data[$key] = $value;
10508d24c7aSGreg Roach    }
10608d24c7aSGreg Roach
10708d24c7aSGreg Roach    /**
108ecf66805SGreg Roach     * Implementation of Blade "stacks".
109ecf66805SGreg Roach     *
110ecf66805SGreg Roach     * @see https://laravel.com/docs/5.5/blade#stacks
111962e29c9SGreg Roach     *
112962e29c9SGreg Roach     * @param string $stack
113fa4036e8SGreg Roach     *
114fa4036e8SGreg Roach     * @return void
115ecf66805SGreg Roach     */
116e364afe4SGreg Roach    public static function push(string $stack): void
117c1010edaSGreg Roach    {
118ecf66805SGreg Roach        self::$stack = $stack;
119d7952a34SGreg Roach
120ecf66805SGreg Roach        ob_start();
121ecf66805SGreg Roach    }
122ecf66805SGreg Roach
123ecf66805SGreg Roach    /**
124ecf66805SGreg Roach     * Implementation of Blade "stacks".
125fa4036e8SGreg Roach     *
126fa4036e8SGreg Roach     * @return void
127ecf66805SGreg Roach     */
128e364afe4SGreg Roach    public static function endpush(): void
129c1010edaSGreg Roach    {
13088de55fdSRico Sonntag        $content = ob_get_clean();
13188de55fdSRico Sonntag
132d7952a34SGreg Roach        self::$stacks[self::$stack][] = $content;
133d7952a34SGreg Roach    }
134d7952a34SGreg Roach
135d7952a34SGreg Roach    /**
136d7952a34SGreg Roach     * Variant of push that will only add one copy of each item.
137d7952a34SGreg Roach     *
138d7952a34SGreg Roach     * @param string $stack
139d7952a34SGreg Roach     *
140d7952a34SGreg Roach     * @return void
141d7952a34SGreg Roach     */
142e364afe4SGreg Roach    public static function pushunique(string $stack): void
143d7952a34SGreg Roach    {
144d7952a34SGreg Roach        self::$stack = $stack;
145d7952a34SGreg Roach
146d7952a34SGreg Roach        ob_start();
147d7952a34SGreg Roach    }
148d7952a34SGreg Roach
149d7952a34SGreg Roach    /**
150d7952a34SGreg Roach     * Variant of push that will only add one copy of each item.
151d7952a34SGreg Roach     *
152d7952a34SGreg Roach     * @return void
153d7952a34SGreg Roach     */
154e364afe4SGreg Roach    public static function endpushunique(): void
155d7952a34SGreg Roach    {
156d7952a34SGreg Roach        $content = ob_get_clean();
157d7952a34SGreg Roach
158d7952a34SGreg Roach        self::$stacks[self::$stack][sha1($content)] = $content;
159ecf66805SGreg Roach    }
160ecf66805SGreg Roach
161ecf66805SGreg Roach    /**
162ecf66805SGreg Roach     * Implementation of Blade "stacks".
163ecf66805SGreg Roach     *
164962e29c9SGreg Roach     * @param string $stack
165962e29c9SGreg Roach     *
166ecf66805SGreg Roach     * @return string
167ecf66805SGreg Roach     */
168c1010edaSGreg Roach    public static function stack(string $stack): string
169c1010edaSGreg Roach    {
170ecf66805SGreg Roach        $content = implode('', self::$stacks[$stack] ?? []);
171ecf66805SGreg Roach
172ecf66805SGreg Roach        self::$stacks[$stack] = [];
173ecf66805SGreg Roach
174ecf66805SGreg Roach        return $content;
175ecf66805SGreg Roach    }
176ecf66805SGreg Roach
177ecf66805SGreg Roach    /**
178f3281ad6SGreg Roach     * Render a view.
179f3281ad6SGreg Roach     *
180f3281ad6SGreg Roach     * @return string
18170f31542SGreg Roach     * @throws Throwable
182f3281ad6SGreg Roach     */
1838f53f488SRico Sonntag    public function render(): string
184c1010edaSGreg Roach    {
1854a86d714SGreg Roach        $variables_for_view = $this->data + self::$shared_data;
186e364afe4SGreg Roach        extract($variables_for_view, EXTR_SKIP);
187f3281ad6SGreg Roach
18870f31542SGreg Roach        try {
189f3281ad6SGreg Roach            ob_start();
190bc241c54SGreg Roach            // Do not use require, so we can catch errors for missing files
191bc241c54SGreg Roach            include $this->getFilenameForView($this->name);
19275d70144SGreg Roach
193f3281ad6SGreg Roach            return ob_get_clean();
19470f31542SGreg Roach        } catch (Throwable $ex) {
195*d3da353bSGreg Roach            while (ob_get_level() > 0) {
19670f31542SGreg Roach                ob_end_clean();
197*d3da353bSGreg Roach            }
19870f31542SGreg Roach            throw $ex;
19970f31542SGreg Roach        }
200f3281ad6SGreg Roach    }
201f3281ad6SGreg Roach
202f3281ad6SGreg Roach    /**
2033e4bf26fSGreg Roach     * @param string $namespace
2043e4bf26fSGreg Roach     * @param string $path
2053e4bf26fSGreg Roach     *
2063e4bf26fSGreg Roach     * @throws InvalidArgumentException
2073e4bf26fSGreg Roach     */
2083e4bf26fSGreg Roach    public static function registerNamespace(string $namespace, string $path): void
2093e4bf26fSGreg Roach    {
2103e4bf26fSGreg Roach        if ($namespace === '') {
2113e4bf26fSGreg Roach            throw new InvalidArgumentException('Cannot register the default namespace');
2123e4bf26fSGreg Roach        }
2133e4bf26fSGreg Roach
2143e4bf26fSGreg Roach        if (!Str::endsWith($path, '/')) {
2153e4bf26fSGreg Roach            throw new InvalidArgumentException('Paths must end with a directory separator');
2163e4bf26fSGreg Roach        }
2173e4bf26fSGreg Roach
2183e4bf26fSGreg Roach        self::$namespaces[$namespace] = $path;
2193e4bf26fSGreg Roach    }
2203e4bf26fSGreg Roach
2213e4bf26fSGreg Roach    /**
2223e4bf26fSGreg Roach     * @param string $old
2233e4bf26fSGreg Roach     * @param string $new
2243e4bf26fSGreg Roach     *
2253e4bf26fSGreg Roach     * @throws InvalidArgumentException
2263e4bf26fSGreg Roach     */
2273e4bf26fSGreg Roach    public static function registerCustomView(string $old, string $new): void
2283e4bf26fSGreg Roach    {
2293e4bf26fSGreg Roach        if (Str::contains($old, self::NAMESPACE_SEPARATOR) && Str::contains($new, self::NAMESPACE_SEPARATOR)) {
2303e4bf26fSGreg Roach            self::$replacements[$old] = $new;
2313e4bf26fSGreg Roach        } else {
2323e4bf26fSGreg Roach            throw new InvalidArgumentException();
2333e4bf26fSGreg Roach        }
2343e4bf26fSGreg Roach    }
2353e4bf26fSGreg Roach
2363e4bf26fSGreg Roach    /**
2373e4bf26fSGreg Roach     * Find the file for a view.
238f3281ad6SGreg Roach     *
239f3281ad6SGreg Roach     * @param string $view_name
240f3281ad6SGreg Roach     *
24175d70144SGreg Roach     * @return string
24250c68a25SGreg Roach     * @throws Exception
243f3281ad6SGreg Roach     */
2443e4bf26fSGreg Roach    public function getFilenameForView(string $view_name): string
245c1010edaSGreg Roach    {
246fdbcd0efSGreg Roach        // If we request "::view", then use it explicityly.  Don't allow replacements.
247fdbcd0efSGreg Roach        $explicit = Str::startsWith($view_name, self::NAMESPACE_SEPARATOR);
248fdbcd0efSGreg Roach
2493e4bf26fSGreg Roach        if (!Str::contains($view_name, self::NAMESPACE_SEPARATOR)) {
2503e4bf26fSGreg Roach            $view_name = self::NAMESPACE_SEPARATOR . $view_name;
2513e4bf26fSGreg Roach        }
25275d70144SGreg Roach
2533e4bf26fSGreg Roach        // Apply replacements / customisations
254fdbcd0efSGreg Roach        while (!$explicit && array_key_exists($view_name, self::$replacements)) {
2553e4bf26fSGreg Roach            $view_name = self::$replacements[$view_name];
2563e4bf26fSGreg Roach        }
2573e4bf26fSGreg Roach
2583e4bf26fSGreg Roach        [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2);
2593e4bf26fSGreg Roach
2603e4bf26fSGreg Roach        if ((self::$namespaces[$namespace] ?? null) === null) {
2613e4bf26fSGreg Roach            throw new RuntimeException('Namespace "' . e($namespace) .  '" not found.');
2623e4bf26fSGreg Roach        }
2633e4bf26fSGreg Roach
2643e4bf26fSGreg Roach        $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION;
2653e4bf26fSGreg Roach
2663e4bf26fSGreg Roach        if (!is_file($view_file)) {
2673e4bf26fSGreg Roach            throw new RuntimeException('View file not found: ' . e($view_file));
2683e4bf26fSGreg Roach        }
2693e4bf26fSGreg Roach
27050c68a25SGreg Roach        return $view_file;
271f21917b2SGreg Roach    }
27250c68a25SGreg Roach
273f3281ad6SGreg Roach    /**
274f3281ad6SGreg Roach     * Cerate and render a view in a single operation.
275f3281ad6SGreg Roach     *
276f3281ad6SGreg Roach     * @param string  $name
277f3281ad6SGreg Roach     * @param mixed[] $data
278f3281ad6SGreg Roach     *
279f3281ad6SGreg Roach     * @return string
280f3281ad6SGreg Roach     */
2818f53f488SRico Sonntag    public static function make($name, $data = []): string
282c1010edaSGreg Roach    {
283f3281ad6SGreg Roach        $view = new static($name, $data);
284f3281ad6SGreg Roach
28544116e73SGreg Roach        DebugBar::addView($name, $data);
28644116e73SGreg Roach
287f3281ad6SGreg Roach        return $view->render();
288f3281ad6SGreg Roach    }
289f3281ad6SGreg Roach}
290