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