xref: /webtrees/app/View.php (revision 8fca99e0b8c5295ab363e2becb96e3f73b4ba786)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2019 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16declare(strict_types=1);
17
18namespace Fisharebest\Webtrees;
19
20use Exception;
21use Fisharebest\Webtrees\Module\ModuleThemeInterface;
22use function ob_end_clean;
23use Throwable;
24
25/**
26 * Simple view/template class.
27 */
28class View
29{
30    // Where do our templates live
31    private const TEMPLATE_PATH = 'resources/views/';
32
33    // File extension for our template files.
34    private const TEMPLATE_EXTENSION = '.phtml';
35
36    /**
37     * @var string The (file) name of the view.
38     */
39    private $name;
40
41    /**
42     * @var mixed[] Data to be inserted into the view.
43     */
44    private $data;
45
46    /**
47     * @var mixed[] Data to be inserted into all views.
48     */
49    private static $shared_data = [];
50
51    /**
52     * @var string Implementation of Blade "stacks".
53     */
54    private static $stack;
55
56    /**
57     * @var array[] Implementation of Blade "stacks".
58     */
59    private static $stacks = [];
60
61    /**
62     * Createa view from a template name and optional data.
63     *
64     * @param string $name
65     * @param array  $data
66     */
67    public function __construct(string $name, $data = [])
68    {
69        $this->name = $name;
70        $this->data = $data;
71    }
72
73    /**
74     * Shared data that is available to all views.
75     *
76     * @param string $key
77     * @param mixed  $value
78     *
79     * @return void
80     */
81    public static function share(string $key, $value)
82    {
83        self::$shared_data[$key] = $value;
84    }
85
86    /**
87     * Implementation of Blade "stacks".
88     *
89     * @see https://laravel.com/docs/5.5/blade#stacks
90     *
91     * @param string $stack
92     *
93     * @return void
94     */
95    public static function push(string $stack)
96    {
97        self::$stack = $stack;
98        ob_start();
99    }
100
101    /**
102     * Implementation of Blade "stacks".
103     *
104     * @return void
105     */
106    public static function endpush()
107    {
108        $content = ob_get_clean();
109        $hash    = sha1($content);
110
111        self::$stacks[self::$stack][$hash] = $content;
112    }
113
114    /**
115     * Implementation of Blade "stacks".
116     *
117     * @param string $stack
118     *
119     * @return string
120     */
121    public static function stack(string $stack): string
122    {
123        $content = implode('', self::$stacks[$stack] ?? []);
124
125        self::$stacks[$stack] = [];
126
127        return $content;
128    }
129
130    /**
131     * Render a view.
132     *
133     * @return string
134     * @throws Throwable
135     */
136    public function render(): string
137    {
138        $variables_for_view = $this->data + self::$shared_data;
139        extract($variables_for_view);
140
141        try {
142            ob_start();
143            // Do not use require, so we can catch errors for missing files
144            include $this->getFilenameForView($this->name);
145
146            return ob_get_clean();
147        } catch (Throwable $ex) {
148            ob_end_clean();
149            throw $ex;
150        }
151    }
152
153    /**
154     * Allow a theme to override the default views.
155     *
156     * @param string $view_name
157     *
158     * @return string
159     * @throws Exception
160     */
161    public function getFilenameForView($view_name): string
162    {
163        foreach ($this->paths() as $path) {
164            $view_file = $path . $view_name . self::TEMPLATE_EXTENSION;
165
166            if (is_file($view_file)) {
167                return $view_file;
168            }
169        }
170
171        throw new Exception('View not found: ' . e($view_name));
172    }
173
174    /**
175     * Cerate and render a view in a single operation.
176     *
177     * @param string  $name
178     * @param mixed[] $data
179     *
180     * @return string
181     */
182    public static function make($name, $data = []): string
183    {
184        $view = new static($name, $data);
185
186        DebugBar::addView($name, $data);
187
188        return $view->render();
189    }
190
191    /**
192     * @return string[]
193     */
194    private function paths(): array
195    {
196        static $paths = [];
197
198        if (empty($paths)) {
199            // Module views
200            // @TODO - this includes disabled modules.
201            $paths = glob(WT_ROOT . Webtrees::MODULES_PATH . '*/' . self::TEMPLATE_PATH);
202            // Theme views
203            $paths[] = WT_ROOT . Webtrees::THEMES_PATH . app()->make(ModuleThemeInterface::class)->name() . '/' . self::TEMPLATE_PATH;
204            // Core views
205            $paths[] = WT_ROOT . self::TEMPLATE_PATH;
206
207            $paths = array_filter($paths, function (string $path): bool {
208                return is_dir($path);
209            });
210        }
211
212        return $paths;
213    }
214}
215