xref: /webtrees/app/View.php (revision 3e4bf26f9f16d92152484c56e3629888fb6019d5)
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;
21*3e4bf26fSGreg Roachuse Illuminate\Support\Str;
22*3e4bf26fSGreg Roachuse InvalidArgumentException;
23*3e4bf26fSGreg Roachuse RuntimeException;
2470f31542SGreg Roachuse Throwable;
25*3e4bf26fSGreg Roachuse function array_key_exists;
26*3e4bf26fSGreg 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{
39*3e4bf26fSGreg 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    /**
55*3e4bf26fSGreg Roach     * @var string[] Where do the templates live, for each namespace.
56*3e4bf26fSGreg Roach     */
57*3e4bf26fSGreg Roach    private static $namespaces = [
58*3e4bf26fSGreg Roach        '' => WT_ROOT . 'resources/views/',
59*3e4bf26fSGreg Roach    ];
60*3e4bf26fSGreg Roach
61*3e4bf26fSGreg Roach    /**
62*3e4bf26fSGreg Roach     * @var string[] Modules can replace core views with their own.
63*3e4bf26fSGreg Roach     */
64*3e4bf26fSGreg Roach    private static $replacements = [];
65*3e4bf26fSGreg Roach
66*3e4bf26fSGreg 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     */
101c1010edaSGreg Roach    public static function share(string $key, $value)
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     */
115c1010edaSGreg Roach    public static function push(string $stack)
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     */
127c1010edaSGreg Roach    public static function endpush()
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     */
141d7952a34SGreg Roach    public static function pushunique(string $stack)
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     */
153d7952a34SGreg Roach    public static function endpushunique()
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;
1854a86d714SGreg Roach        extract($variables_for_view);
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    /**
200*3e4bf26fSGreg Roach     * @param string $namespace
201*3e4bf26fSGreg Roach     * @param string $path
202*3e4bf26fSGreg Roach     *
203*3e4bf26fSGreg Roach     * @throws InvalidArgumentException
204*3e4bf26fSGreg Roach     */
205*3e4bf26fSGreg Roach    public static function registerNamespace(string $namespace, string $path): void
206*3e4bf26fSGreg Roach    {
207*3e4bf26fSGreg Roach        if ($namespace === '') {
208*3e4bf26fSGreg Roach            throw new InvalidArgumentException('Cannot register the default namespace');
209*3e4bf26fSGreg Roach        }
210*3e4bf26fSGreg Roach
211*3e4bf26fSGreg Roach        if (!Str::endsWith($path, '/')) {
212*3e4bf26fSGreg Roach            throw new InvalidArgumentException('Paths must end with a directory separator');
213*3e4bf26fSGreg Roach        }
214*3e4bf26fSGreg Roach
215*3e4bf26fSGreg Roach        self::$namespaces[$namespace] = $path;
216*3e4bf26fSGreg Roach    }
217*3e4bf26fSGreg Roach
218*3e4bf26fSGreg Roach    /**
219*3e4bf26fSGreg Roach     * @param string $old
220*3e4bf26fSGreg Roach     * @param string $new
221*3e4bf26fSGreg Roach     *
222*3e4bf26fSGreg Roach     * @throws InvalidArgumentException
223*3e4bf26fSGreg Roach     */
224*3e4bf26fSGreg Roach    public static function registerCustomView(string $old, string $new): void
225*3e4bf26fSGreg Roach    {
226*3e4bf26fSGreg Roach        if (Str::contains($old, self::NAMESPACE_SEPARATOR) && Str::contains($new, self::NAMESPACE_SEPARATOR)) {
227*3e4bf26fSGreg Roach            self::$replacements[$old] = $new;
228*3e4bf26fSGreg Roach        } else {
229*3e4bf26fSGreg Roach            throw new InvalidArgumentException();
230*3e4bf26fSGreg Roach        }
231*3e4bf26fSGreg Roach    }
232*3e4bf26fSGreg Roach
233*3e4bf26fSGreg Roach    /**
234*3e4bf26fSGreg 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     */
241*3e4bf26fSGreg Roach    public function getFilenameForView(string $view_name): string
242c1010edaSGreg Roach    {
243*3e4bf26fSGreg Roach        if (!Str::contains($view_name, self::NAMESPACE_SEPARATOR)) {
244*3e4bf26fSGreg Roach            $view_name = self::NAMESPACE_SEPARATOR . $view_name;
245*3e4bf26fSGreg Roach        }
24675d70144SGreg Roach
247*3e4bf26fSGreg Roach        // Apply replacements / customisations
248*3e4bf26fSGreg Roach        while (array_key_exists($view_name, self::$replacements)) {
249*3e4bf26fSGreg Roach            $view_name = self::$replacements[$view_name];
250*3e4bf26fSGreg Roach        }
251*3e4bf26fSGreg Roach
252*3e4bf26fSGreg Roach        [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2);
253*3e4bf26fSGreg Roach
254*3e4bf26fSGreg Roach        if ((self::$namespaces[$namespace] ?? null) === null) {
255*3e4bf26fSGreg Roach            throw new RuntimeException('Namespace "' . e($namespace) .  '" not found.');
256*3e4bf26fSGreg Roach        }
257*3e4bf26fSGreg Roach
258*3e4bf26fSGreg Roach        $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION;
259*3e4bf26fSGreg Roach
260*3e4bf26fSGreg Roach        if (!is_file($view_file)) {
261*3e4bf26fSGreg Roach            throw new RuntimeException('View file not found: ' . e($view_file));
262*3e4bf26fSGreg Roach        }
263*3e4bf26fSGreg Roach
26450c68a25SGreg Roach        return $view_file;
265f21917b2SGreg Roach    }
26650c68a25SGreg Roach
267f3281ad6SGreg Roach    /**
268f3281ad6SGreg Roach     * Cerate and render a view in a single operation.
269f3281ad6SGreg Roach     *
270f3281ad6SGreg Roach     * @param string  $name
271f3281ad6SGreg Roach     * @param mixed[] $data
272f3281ad6SGreg Roach     *
273f3281ad6SGreg Roach     * @return string
274f3281ad6SGreg Roach     */
2758f53f488SRico Sonntag    public static function make($name, $data = []): string
276c1010edaSGreg Roach    {
277f3281ad6SGreg Roach        $view = new static($name, $data);
278f3281ad6SGreg Roach
27944116e73SGreg Roach        DebugBar::addView($name, $data);
28044116e73SGreg Roach
281f3281ad6SGreg Roach        return $view->render();
282f3281ad6SGreg Roach    }
283f3281ad6SGreg Roach}
284