xref: /webtrees/app/View.php (revision a1afa4f8c102b72d0aa44e205cefd03971140b52)
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        self::$stacks[self::$stack][] = ob_get_clean();
109    }
110
111    /**
112     * Implementation of Blade "stacks".
113     *
114     * @param string $stack
115     *
116     * @return string
117     */
118    public static function stack(string $stack): string
119    {
120        $content = implode('', self::$stacks[$stack] ?? []);
121
122        self::$stacks[$stack] = [];
123
124        return $content;
125    }
126
127    /**
128     * Render a view.
129     *
130     * @return string
131     * @throws Throwable
132     */
133    public function render(): string
134    {
135        $variables_for_view = $this->data + self::$shared_data;
136        extract($variables_for_view);
137
138        try {
139            ob_start();
140            // Do not use require, so we can catch errors for missing files
141            include $this->getFilenameForView($this->name);
142
143            return ob_get_clean();
144        } catch (Throwable $ex) {
145            ob_end_clean();
146            throw $ex;
147        }
148    }
149
150    /**
151     * Allow a theme to override the default views.
152     *
153     * @param string $view_name
154     *
155     * @return string
156     * @throws Exception
157     */
158    public function getFilenameForView($view_name): string
159    {
160        foreach ($this->paths() as $path) {
161            $view_file = $path . $view_name . self::TEMPLATE_EXTENSION;
162
163            if (is_file($view_file)) {
164                return $view_file;
165            }
166        }
167
168        throw new Exception('View not found: ' . e($view_name));
169    }
170
171    /**
172     * Cerate and render a view in a single operation.
173     *
174     * @param string  $name
175     * @param mixed[] $data
176     *
177     * @return string
178     */
179    public static function make($name, $data = []): string
180    {
181        $view = new static($name, $data);
182
183        DebugBar::addView($name, $data);
184
185        return $view->render();
186    }
187
188    /**
189     * @return string[]
190     */
191    private function paths(): array
192    {
193        static $paths = [];
194
195        if (empty($paths)) {
196            // Module views
197            // @TODO - this includes disabled modules.
198            $paths = glob(WT_ROOT . Webtrees::MODULES_PATH . '*/' . self::TEMPLATE_PATH);
199            // Theme views
200            $paths[] = WT_ROOT . Webtrees::THEMES_PATH . app()->make(ModuleThemeInterface::class)->name() . '/' . self::TEMPLATE_PATH;
201            // Core views
202            $paths[] = WT_ROOT . self::TEMPLATE_PATH;
203
204            $paths = array_filter($paths, function (string $path): bool {
205                return is_dir($path);
206            });
207        }
208
209        return $paths;
210    }
211}
212