xref: /webtrees/app/View.php (revision 9d627a9e6c00d03faa5d6a39e801ab0bb6f45d6d)
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 Throwable;
22use function array_filter;
23use function extract;
24use function implode;
25use function is_dir;
26use function is_file;
27use function ob_end_clean;
28use function ob_start;
29use function sha1;
30
31/**
32 * Simple view/template class.
33 */
34class View
35{
36    // Where do our templates live
37    private const TEMPLATE_PATH = 'resources/views/';
38
39    // File extension for our template files.
40    private const TEMPLATE_EXTENSION = '.phtml';
41
42    /**
43     * @var string The (file) name of the view.
44     */
45    private $name;
46
47    /**
48     * @var mixed[] Data to be inserted into the view.
49     */
50    private $data;
51
52    /**
53     * @var mixed[] Data to be inserted into all views.
54     */
55    private static $shared_data = [];
56
57    /**
58     * @var string Implementation of Blade "stacks".
59     */
60    private static $stack;
61
62    /**
63     * @var array[] Implementation of Blade "stacks".
64     */
65    private static $stacks = [];
66
67    /**
68     * Createa view from a template name and optional data.
69     *
70     * @param string $name
71     * @param array  $data
72     */
73    public function __construct(string $name, $data = [])
74    {
75        $this->name = $name;
76        $this->data = $data;
77    }
78
79    /**
80     * Shared data that is available to all views.
81     *
82     * @param string $key
83     * @param mixed  $value
84     *
85     * @return void
86     */
87    public static function share(string $key, $value)
88    {
89        self::$shared_data[$key] = $value;
90    }
91
92    /**
93     * Implementation of Blade "stacks".
94     *
95     * @see https://laravel.com/docs/5.5/blade#stacks
96     *
97     * @param string $stack
98     *
99     * @return void
100     */
101    public static function push(string $stack)
102    {
103        self::$stack = $stack;
104
105        ob_start();
106    }
107
108    /**
109     * Implementation of Blade "stacks".
110     *
111     * @return void
112     */
113    public static function endpush()
114    {
115        $content = ob_get_clean();
116
117        self::$stacks[self::$stack][] = $content;
118    }
119
120    /**
121     * Variant of push that will only add one copy of each item.
122     *
123     * @param string $stack
124     *
125     * @return void
126     */
127    public static function pushunique(string $stack)
128    {
129        self::$stack = $stack;
130
131        ob_start();
132    }
133
134    /**
135     * Variant of push that will only add one copy of each item.
136     *
137     * @return void
138     */
139    public static function endpushunique()
140    {
141        $content = ob_get_clean();
142
143        self::$stacks[self::$stack][sha1($content)] = $content;
144    }
145
146    /**
147     * Implementation of Blade "stacks".
148     *
149     * @param string $stack
150     *
151     * @return string
152     */
153    public static function stack(string $stack): string
154    {
155        $content = implode('', self::$stacks[$stack] ?? []);
156
157        self::$stacks[$stack] = [];
158
159        return $content;
160    }
161
162    /**
163     * Render a view.
164     *
165     * @return string
166     * @throws Throwable
167     */
168    public function render(): string
169    {
170        $variables_for_view = $this->data + self::$shared_data;
171        extract($variables_for_view);
172
173        try {
174            ob_start();
175            // Do not use require, so we can catch errors for missing files
176            include $this->getFilenameForView($this->name);
177
178            return ob_get_clean();
179        } catch (Throwable $ex) {
180            ob_end_clean();
181            throw $ex;
182        }
183    }
184
185    /**
186     * Allow a theme to override the default views.
187     *
188     * @param string $view_name
189     *
190     * @return string
191     * @throws Exception
192     */
193    public function getFilenameForView($view_name): string
194    {
195        foreach ($this->paths() as $path) {
196            $view_file = $path . $view_name . self::TEMPLATE_EXTENSION;
197
198            if (is_file($view_file)) {
199                return $view_file;
200            }
201        }
202
203        throw new Exception('View not found: ' . e($view_name));
204    }
205
206    /**
207     * Cerate and render a view in a single operation.
208     *
209     * @param string  $name
210     * @param mixed[] $data
211     *
212     * @return string
213     */
214    public static function make($name, $data = []): string
215    {
216        $view = new static($name, $data);
217
218        DebugBar::addView($name, $data);
219
220        return $view->render();
221    }
222
223    /**
224     * @return string[]
225     */
226    private function paths(): array
227    {
228        static $paths = [];
229
230        if (empty($paths)) {
231            // Module views
232            // @TODO - this includes disabled modules.
233            //$paths = glob(WT_ROOT . Webtrees::MODULES_PATH . '*/' . self::TEMPLATE_PATH);
234            // Theme views
235            // @TODO - this won't work during setup.
236            //$paths[] = WT_ROOT . Webtrees::THEMES_PATH . app(ModuleThemeInterface::class)->name() . '/' . self::TEMPLATE_PATH;
237            // Core views
238            $paths[] = WT_ROOT . self::TEMPLATE_PATH;
239
240            $paths = array_filter($paths, function (string $path): bool {
241                return is_dir($path);
242            });
243        }
244
245        return $paths;
246    }
247}
248