xref: /webtrees/app/View.php (revision fdbcd0ef4c00a28c89d53366fe4d731528163bda)
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;
31d7952a34SGreg Roachuse function ob_start;
32d7952a34SGreg Roachuse function sha1;
3350c68a25SGreg Roach
34f3281ad6SGreg Roach/**
35f3281ad6SGreg Roach * Simple view/template class.
36f3281ad6SGreg Roach */
37c1010edaSGreg Roachclass View
38c1010edaSGreg Roach{
393e4bf26fSGreg Roach    public const NAMESPACE_SEPARATOR = '::';
40dd6b2bfcSGreg Roach
41dd6b2bfcSGreg Roach    // File extension for our template files.
4216d6367aSGreg Roach    private const TEMPLATE_EXTENSION = '.phtml';
43dd6b2bfcSGreg Roach
44f3281ad6SGreg Roach    /**
45f3281ad6SGreg Roach     * @var string The (file) name of the view.
46f3281ad6SGreg Roach     */
47f3281ad6SGreg Roach    private $name;
48f3281ad6SGreg Roach
49f3281ad6SGreg Roach    /**
50f3281ad6SGreg Roach     * @var mixed[] Data to be inserted into the view.
51f3281ad6SGreg Roach     */
52f3281ad6SGreg Roach    private $data;
53f3281ad6SGreg Roach
54f3281ad6SGreg Roach    /**
553e4bf26fSGreg Roach     * @var string[] Where do the templates live, for each namespace.
563e4bf26fSGreg Roach     */
573e4bf26fSGreg Roach    private static $namespaces = [
583e4bf26fSGreg Roach        '' => WT_ROOT . 'resources/views/',
593e4bf26fSGreg Roach    ];
603e4bf26fSGreg Roach
613e4bf26fSGreg Roach    /**
623e4bf26fSGreg Roach     * @var string[] Modules can replace core views with their own.
633e4bf26fSGreg Roach     */
643e4bf26fSGreg Roach    private static $replacements = [];
653e4bf26fSGreg Roach
663e4bf26fSGreg Roach    /**
6708d24c7aSGreg Roach     * @var mixed[] Data to be inserted into all views.
6808d24c7aSGreg Roach     */
6908d24c7aSGreg Roach    private static $shared_data = [];
7008d24c7aSGreg Roach
7108d24c7aSGreg Roach    /**
72ecf66805SGreg Roach     * @var string Implementation of Blade "stacks".
73ecf66805SGreg Roach     */
74ecf66805SGreg Roach    private static $stack;
75ecf66805SGreg Roach
76ecf66805SGreg Roach    /**
77ecf66805SGreg Roach     * @var array[] Implementation of Blade "stacks".
78ecf66805SGreg Roach     */
79ecf66805SGreg Roach    private static $stacks = [];
80ecf66805SGreg Roach
81ecf66805SGreg Roach    /**
82f3281ad6SGreg Roach     * Createa view from a template name and optional data.
83f3281ad6SGreg Roach     *
84fa4036e8SGreg Roach     * @param string $name
85f3281ad6SGreg Roach     * @param array  $data
86f3281ad6SGreg Roach     */
87fa4036e8SGreg Roach    public function __construct(string $name, $data = [])
88c1010edaSGreg Roach    {
89f3281ad6SGreg Roach        $this->name = $name;
90f3281ad6SGreg Roach        $this->data = $data;
91f3281ad6SGreg Roach    }
92f3281ad6SGreg Roach
93f3281ad6SGreg Roach    /**
9408d24c7aSGreg Roach     * Shared data that is available to all views.
95962e29c9SGreg Roach     *
96962e29c9SGreg Roach     * @param string $key
97962e29c9SGreg Roach     * @param mixed  $value
98fa4036e8SGreg Roach     *
99fa4036e8SGreg Roach     * @return void
10008d24c7aSGreg Roach     */
101e364afe4SGreg Roach    public static function share(string $key, $value): void
102c1010edaSGreg Roach    {
10308d24c7aSGreg Roach        self::$shared_data[$key] = $value;
10408d24c7aSGreg Roach    }
10508d24c7aSGreg Roach
10608d24c7aSGreg Roach    /**
107ecf66805SGreg Roach     * Implementation of Blade "stacks".
108ecf66805SGreg Roach     *
109ecf66805SGreg Roach     * @see https://laravel.com/docs/5.5/blade#stacks
110962e29c9SGreg Roach     *
111962e29c9SGreg Roach     * @param string $stack
112fa4036e8SGreg Roach     *
113fa4036e8SGreg Roach     * @return void
114ecf66805SGreg Roach     */
115e364afe4SGreg Roach    public static function push(string $stack): void
116c1010edaSGreg Roach    {
117ecf66805SGreg Roach        self::$stack = $stack;
118d7952a34SGreg Roach
119ecf66805SGreg Roach        ob_start();
120ecf66805SGreg Roach    }
121ecf66805SGreg Roach
122ecf66805SGreg Roach    /**
123ecf66805SGreg Roach     * Implementation of Blade "stacks".
124fa4036e8SGreg Roach     *
125fa4036e8SGreg Roach     * @return void
126ecf66805SGreg Roach     */
127e364afe4SGreg Roach    public static function endpush(): void
128c1010edaSGreg Roach    {
12988de55fdSRico Sonntag        $content = ob_get_clean();
13088de55fdSRico Sonntag
131d7952a34SGreg Roach        self::$stacks[self::$stack][] = $content;
132d7952a34SGreg Roach    }
133d7952a34SGreg Roach
134d7952a34SGreg Roach    /**
135d7952a34SGreg Roach     * Variant of push that will only add one copy of each item.
136d7952a34SGreg Roach     *
137d7952a34SGreg Roach     * @param string $stack
138d7952a34SGreg Roach     *
139d7952a34SGreg Roach     * @return void
140d7952a34SGreg Roach     */
141e364afe4SGreg Roach    public static function pushunique(string $stack): void
142d7952a34SGreg Roach    {
143d7952a34SGreg Roach        self::$stack = $stack;
144d7952a34SGreg Roach
145d7952a34SGreg Roach        ob_start();
146d7952a34SGreg Roach    }
147d7952a34SGreg Roach
148d7952a34SGreg Roach    /**
149d7952a34SGreg Roach     * Variant of push that will only add one copy of each item.
150d7952a34SGreg Roach     *
151d7952a34SGreg Roach     * @return void
152d7952a34SGreg Roach     */
153e364afe4SGreg Roach    public static function endpushunique(): void
154d7952a34SGreg Roach    {
155d7952a34SGreg Roach        $content = ob_get_clean();
156d7952a34SGreg Roach
157d7952a34SGreg Roach        self::$stacks[self::$stack][sha1($content)] = $content;
158ecf66805SGreg Roach    }
159ecf66805SGreg Roach
160ecf66805SGreg Roach    /**
161ecf66805SGreg Roach     * Implementation of Blade "stacks".
162ecf66805SGreg Roach     *
163962e29c9SGreg Roach     * @param string $stack
164962e29c9SGreg Roach     *
165ecf66805SGreg Roach     * @return string
166ecf66805SGreg Roach     */
167c1010edaSGreg Roach    public static function stack(string $stack): string
168c1010edaSGreg Roach    {
169ecf66805SGreg Roach        $content = implode('', self::$stacks[$stack] ?? []);
170ecf66805SGreg Roach
171ecf66805SGreg Roach        self::$stacks[$stack] = [];
172ecf66805SGreg Roach
173ecf66805SGreg Roach        return $content;
174ecf66805SGreg Roach    }
175ecf66805SGreg Roach
176ecf66805SGreg Roach    /**
177f3281ad6SGreg Roach     * Render a view.
178f3281ad6SGreg Roach     *
179f3281ad6SGreg Roach     * @return string
18070f31542SGreg Roach     * @throws Throwable
181f3281ad6SGreg Roach     */
1828f53f488SRico Sonntag    public function render(): string
183c1010edaSGreg Roach    {
1844a86d714SGreg Roach        $variables_for_view = $this->data + self::$shared_data;
185e364afe4SGreg Roach        extract($variables_for_view, EXTR_SKIP);
186f3281ad6SGreg Roach
18770f31542SGreg Roach        try {
188f3281ad6SGreg Roach            ob_start();
189bc241c54SGreg Roach            // Do not use require, so we can catch errors for missing files
190bc241c54SGreg Roach            include $this->getFilenameForView($this->name);
19175d70144SGreg Roach
192f3281ad6SGreg Roach            return ob_get_clean();
19370f31542SGreg Roach        } catch (Throwable $ex) {
19470f31542SGreg Roach            ob_end_clean();
19570f31542SGreg Roach            throw $ex;
19670f31542SGreg Roach        }
197f3281ad6SGreg Roach    }
198f3281ad6SGreg Roach
199f3281ad6SGreg Roach    /**
2003e4bf26fSGreg Roach     * @param string $namespace
2013e4bf26fSGreg Roach     * @param string $path
2023e4bf26fSGreg Roach     *
2033e4bf26fSGreg Roach     * @throws InvalidArgumentException
2043e4bf26fSGreg Roach     */
2053e4bf26fSGreg Roach    public static function registerNamespace(string $namespace, string $path): void
2063e4bf26fSGreg Roach    {
2073e4bf26fSGreg Roach        if ($namespace === '') {
2083e4bf26fSGreg Roach            throw new InvalidArgumentException('Cannot register the default namespace');
2093e4bf26fSGreg Roach        }
2103e4bf26fSGreg Roach
2113e4bf26fSGreg Roach        if (!Str::endsWith($path, '/')) {
2123e4bf26fSGreg Roach            throw new InvalidArgumentException('Paths must end with a directory separator');
2133e4bf26fSGreg Roach        }
2143e4bf26fSGreg Roach
2153e4bf26fSGreg Roach        self::$namespaces[$namespace] = $path;
2163e4bf26fSGreg Roach    }
2173e4bf26fSGreg Roach
2183e4bf26fSGreg Roach    /**
2193e4bf26fSGreg Roach     * @param string $old
2203e4bf26fSGreg Roach     * @param string $new
2213e4bf26fSGreg Roach     *
2223e4bf26fSGreg Roach     * @throws InvalidArgumentException
2233e4bf26fSGreg Roach     */
2243e4bf26fSGreg Roach    public static function registerCustomView(string $old, string $new): void
2253e4bf26fSGreg Roach    {
2263e4bf26fSGreg Roach        if (Str::contains($old, self::NAMESPACE_SEPARATOR) && Str::contains($new, self::NAMESPACE_SEPARATOR)) {
2273e4bf26fSGreg Roach            self::$replacements[$old] = $new;
2283e4bf26fSGreg Roach        } else {
2293e4bf26fSGreg Roach            throw new InvalidArgumentException();
2303e4bf26fSGreg Roach        }
2313e4bf26fSGreg Roach    }
2323e4bf26fSGreg Roach
2333e4bf26fSGreg Roach    /**
2343e4bf26fSGreg Roach     * Find the file for a view.
235f3281ad6SGreg Roach     *
236f3281ad6SGreg Roach     * @param string $view_name
237f3281ad6SGreg Roach     *
23875d70144SGreg Roach     * @return string
23950c68a25SGreg Roach     * @throws Exception
240f3281ad6SGreg Roach     */
2413e4bf26fSGreg Roach    public function getFilenameForView(string $view_name): string
242c1010edaSGreg Roach    {
243*fdbcd0efSGreg Roach        // If we request "::view", then use it explicityly.  Don't allow replacements.
244*fdbcd0efSGreg Roach        $explicit = Str::startsWith($view_name, self::NAMESPACE_SEPARATOR);
245*fdbcd0efSGreg Roach
2463e4bf26fSGreg Roach        if (!Str::contains($view_name, self::NAMESPACE_SEPARATOR)) {
2473e4bf26fSGreg Roach            $view_name = self::NAMESPACE_SEPARATOR . $view_name;
2483e4bf26fSGreg Roach        }
24975d70144SGreg Roach
2503e4bf26fSGreg Roach        // Apply replacements / customisations
251*fdbcd0efSGreg Roach        while (!$explicit && array_key_exists($view_name, self::$replacements)) {
2523e4bf26fSGreg Roach            $view_name = self::$replacements[$view_name];
2533e4bf26fSGreg Roach        }
2543e4bf26fSGreg Roach
2553e4bf26fSGreg Roach        [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2);
2563e4bf26fSGreg Roach
2573e4bf26fSGreg Roach        if ((self::$namespaces[$namespace] ?? null) === null) {
2583e4bf26fSGreg Roach            throw new RuntimeException('Namespace "' . e($namespace) .  '" not found.');
2593e4bf26fSGreg Roach        }
2603e4bf26fSGreg Roach
2613e4bf26fSGreg Roach        $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION;
2623e4bf26fSGreg Roach
2633e4bf26fSGreg Roach        if (!is_file($view_file)) {
2643e4bf26fSGreg Roach            throw new RuntimeException('View file not found: ' . e($view_file));
2653e4bf26fSGreg Roach        }
2663e4bf26fSGreg Roach
26750c68a25SGreg Roach        return $view_file;
268f21917b2SGreg Roach    }
26950c68a25SGreg Roach
270f3281ad6SGreg Roach    /**
271f3281ad6SGreg Roach     * Cerate and render a view in a single operation.
272f3281ad6SGreg Roach     *
273f3281ad6SGreg Roach     * @param string  $name
274f3281ad6SGreg Roach     * @param mixed[] $data
275f3281ad6SGreg Roach     *
276f3281ad6SGreg Roach     * @return string
277f3281ad6SGreg Roach     */
2788f53f488SRico Sonntag    public static function make($name, $data = []): string
279c1010edaSGreg Roach    {
280f3281ad6SGreg Roach        $view = new static($name, $data);
281f3281ad6SGreg Roach
28244116e73SGreg Roach        DebugBar::addView($name, $data);
28344116e73SGreg Roach
284f3281ad6SGreg Roach        return $view->render();
285f3281ad6SGreg Roach    }
286f3281ad6SGreg Roach}
287