Passed
Push — master ( cacefd...12662b )
by Greg
05:17
created

View::share()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Exception;
23
use Illuminate\Support\Str;
24
use InvalidArgumentException;
25
use LogicException;
26
use RuntimeException;
27
use Throwable;
28
29
use function array_key_exists;
30
use function explode;
31
use function extract;
32
use function implode;
33
use function is_file;
34
use function ob_end_clean;
35
use function ob_get_level;
36
use function ob_start;
37
use function sha1;
38
39
use const EXTR_OVERWRITE;
40
41
/**
42
 * Simple view/template class.
43
 */
44
class View
45
{
46
    public const NAMESPACE_SEPARATOR = '::';
47
48
    // File extension for our template files.
49
    private const TEMPLATE_EXTENSION = '.phtml';
50
51
    /**
52
     * @var string The (file) name of the view.
53
     */
54
    private $name;
55
56
    /**
57
     * @var mixed[] Data to be inserted into the view.
58
     */
59
    private $data;
60
61
    /**
62
     * @var string[] Where do the templates live, for each namespace.
63
     */
64
    private static $namespaces = [
65
        '' => Webtrees::ROOT_DIR . 'resources/views/',
66
    ];
67
68
    /**
69
     * @var string[] Modules can replace core views with their own.
70
     */
71
    private static $replacements = [];
72
73
    /**
74
     * @var string Implementation of Blade "stacks".
75
     */
76
    private static $stack;
77
78
    /**
79
     * @var array[] Implementation of Blade "stacks".
80
     */
81
    private static $stacks = [];
82
83
    /**
84
     * Createa view from a template name and optional data.
85
     *
86
     * @param string $name
87
     * @param array  $data
88
     */
89
    public function __construct(string $name, $data = [])
90
    {
91
        $this->name = $name;
92
        $this->data = $data;
93
    }
94
95
    /**
96
     * Implementation of Blade "stacks".
97
     *
98
     * @see https://laravel.com/docs/5.5/blade#stacks
99
     *
100
     * @param string $stack
101
     *
102
     * @return void
103
     */
104
    public static function push(string $stack): void
105
    {
106
        self::$stack = $stack;
107
108
        ob_start();
109
    }
110
111
    /**
112
     * Implementation of Blade "stacks".
113
     *
114
     * @return void
115
     */
116
    public static function endpush(): void
117
    {
118
        $content = ob_get_clean();
119
120
        if ($content === false) {
121
            throw new LogicException('found endpush(), but did not find push()');
122
        }
123
124
        self::$stacks[self::$stack][] = $content;
125
    }
126
127
    /**
128
     * Variant of push that will only add one copy of each item.
129
     *
130
     * @param string $stack
131
     *
132
     * @return void
133
     */
134
    public static function pushunique(string $stack): void
135
    {
136
        self::$stack = $stack;
137
138
        ob_start();
139
    }
140
141
    /**
142
     * Variant of push that will only add one copy of each item.
143
     *
144
     * @return void
145
     */
146
    public static function endpushunique(): void
147
    {
148
        $content = ob_get_clean();
149
150
        if ($content === false) {
151
            throw new LogicException('found endpushunique(), but did not find pushunique()');
152
        }
153
154
        self::$stacks[self::$stack][sha1($content)] = $content;
155
    }
156
157
    /**
158
     * Implementation of Blade "stacks".
159
     *
160
     * @param string $stack
161
     *
162
     * @return string
163
     */
164
    public static function stack(string $stack): string
165
    {
166
        $content = implode('', self::$stacks[$stack] ?? []);
167
168
        self::$stacks[$stack] = [];
169
170
        return $content;
171
    }
172
173
    /**
174
     * Render a view.
175
     *
176
     * @return string
177
     * @throws Throwable
178
     */
179
    public function render(): string
180
    {
181
        extract($this->data, EXTR_OVERWRITE);
182
183
        try {
184
            ob_start();
185
            // Do not use require, so we can catch errors for missing files
186
            include $this->getFilenameForView($this->name);
187
188
            return ob_get_clean();
189
        } catch (Throwable $ex) {
190
            while (ob_get_level() > 0) {
191
                ob_end_clean();
192
            }
193
            throw $ex;
194
        }
195
    }
196
197
    /**
198
     * @param string $namespace
199
     * @param string $path
200
     *
201
     * @throws InvalidArgumentException
202
     */
203
    public static function registerNamespace(string $namespace, string $path): void
204
    {
205
        if ($namespace === '') {
206
            throw new InvalidArgumentException('Cannot register the default namespace');
207
        }
208
209
        if (!Str::endsWith($path, '/')) {
210
            throw new InvalidArgumentException('Paths must end with a directory separator');
211
        }
212
213
        self::$namespaces[$namespace] = $path;
214
    }
215
216
    /**
217
     * @param string $old
218
     * @param string $new
219
     *
220
     * @throws InvalidArgumentException
221
     */
222
    public static function registerCustomView(string $old, string $new): void
223
    {
224
        if (Str::contains($old, self::NAMESPACE_SEPARATOR) && Str::contains($new, self::NAMESPACE_SEPARATOR)) {
225
            self::$replacements[$old] = $new;
226
        } else {
227
            throw new InvalidArgumentException();
228
        }
229
    }
230
231
    /**
232
     * Find the file for a view.
233
     *
234
     * @param string $view_name
235
     *
236
     * @return string
237
     * @throws Exception
238
     */
239
    public function getFilenameForView(string $view_name): string
240
    {
241
        // If we request "::view", then use it explicityly.  Don't allow replacements.
242
        $explicit = Str::startsWith($view_name, self::NAMESPACE_SEPARATOR);
243
244
        if (!Str::contains($view_name, self::NAMESPACE_SEPARATOR)) {
245
            $view_name = self::NAMESPACE_SEPARATOR . $view_name;
246
        }
247
248
        // Apply replacements / customisations
249
        while (!$explicit && array_key_exists($view_name, self::$replacements)) {
250
            $view_name = self::$replacements[$view_name];
251
        }
252
253
        [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2);
254
255
        if ((self::$namespaces[$namespace] ?? null) === null) {
256
            throw new RuntimeException('Namespace "' . e($namespace) .  '" not found.');
257
        }
258
259
        $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION;
260
261
        if (!is_file($view_file)) {
262
            throw new RuntimeException('View file not found: ' . e($view_file));
263
        }
264
265
        return $view_file;
266
    }
267
268
    /**
269
     * Cerate and render a view in a single operation.
270
     *
271
     * @param string  $name
272
     * @param mixed[] $data
273
     *
274
     * @return string
275
     */
276
    public static function make($name, $data = []): string
277
    {
278
        $view = new static($name, $data);
279
280
        DebugBar::addView($name, $data);
281
282
        return $view->render();
283
    }
284
}
285