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 Illuminate\Support\Str; 22use InvalidArgumentException; 23use RuntimeException; 24use Throwable; 25use function array_key_exists; 26use function explode; 27use function extract; 28use function implode; 29use function is_file; 30use function ob_end_clean; 31use function ob_start; 32use function sha1; 33 34/** 35 * Simple view/template class. 36 */ 37class View 38{ 39 public const NAMESPACE_SEPARATOR = '::'; 40 41 // File extension for our template files. 42 private const TEMPLATE_EXTENSION = '.phtml'; 43 44 /** 45 * @var string The (file) name of the view. 46 */ 47 private $name; 48 49 /** 50 * @var mixed[] Data to be inserted into the view. 51 */ 52 private $data; 53 54 /** 55 * @var string[] Where do the templates live, for each namespace. 56 */ 57 private static $namespaces = [ 58 '' => WT_ROOT . 'resources/views/', 59 ]; 60 61 /** 62 * @var string[] Modules can replace core views with their own. 63 */ 64 private static $replacements = []; 65 66 /** 67 * @var mixed[] Data to be inserted into all views. 68 */ 69 private static $shared_data = []; 70 71 /** 72 * @var string Implementation of Blade "stacks". 73 */ 74 private static $stack; 75 76 /** 77 * @var array[] Implementation of Blade "stacks". 78 */ 79 private static $stacks = []; 80 81 /** 82 * Createa view from a template name and optional data. 83 * 84 * @param string $name 85 * @param array $data 86 */ 87 public function __construct(string $name, $data = []) 88 { 89 $this->name = $name; 90 $this->data = $data; 91 } 92 93 /** 94 * Shared data that is available to all views. 95 * 96 * @param string $key 97 * @param mixed $value 98 * 99 * @return void 100 */ 101 public static function share(string $key, $value) 102 { 103 self::$shared_data[$key] = $value; 104 } 105 106 /** 107 * Implementation of Blade "stacks". 108 * 109 * @see https://laravel.com/docs/5.5/blade#stacks 110 * 111 * @param string $stack 112 * 113 * @return void 114 */ 115 public static function push(string $stack) 116 { 117 self::$stack = $stack; 118 119 ob_start(); 120 } 121 122 /** 123 * Implementation of Blade "stacks". 124 * 125 * @return void 126 */ 127 public static function endpush() 128 { 129 $content = ob_get_clean(); 130 131 self::$stacks[self::$stack][] = $content; 132 } 133 134 /** 135 * Variant of push that will only add one copy of each item. 136 * 137 * @param string $stack 138 * 139 * @return void 140 */ 141 public static function pushunique(string $stack) 142 { 143 self::$stack = $stack; 144 145 ob_start(); 146 } 147 148 /** 149 * Variant of push that will only add one copy of each item. 150 * 151 * @return void 152 */ 153 public static function endpushunique() 154 { 155 $content = ob_get_clean(); 156 157 self::$stacks[self::$stack][sha1($content)] = $content; 158 } 159 160 /** 161 * Implementation of Blade "stacks". 162 * 163 * @param string $stack 164 * 165 * @return string 166 */ 167 public static function stack(string $stack): string 168 { 169 $content = implode('', self::$stacks[$stack] ?? []); 170 171 self::$stacks[$stack] = []; 172 173 return $content; 174 } 175 176 /** 177 * Render a view. 178 * 179 * @return string 180 * @throws Throwable 181 */ 182 public function render(): string 183 { 184 $variables_for_view = $this->data + self::$shared_data; 185 extract($variables_for_view); 186 187 try { 188 ob_start(); 189 // Do not use require, so we can catch errors for missing files 190 include $this->getFilenameForView($this->name); 191 192 return ob_get_clean(); 193 } catch (Throwable $ex) { 194 ob_end_clean(); 195 throw $ex; 196 } 197 } 198 199 /** 200 * @param string $namespace 201 * @param string $path 202 * 203 * @throws InvalidArgumentException 204 */ 205 public static function registerNamespace(string $namespace, string $path): void 206 { 207 if ($namespace === '') { 208 throw new InvalidArgumentException('Cannot register the default namespace'); 209 } 210 211 if (!Str::endsWith($path, '/')) { 212 throw new InvalidArgumentException('Paths must end with a directory separator'); 213 } 214 215 self::$namespaces[$namespace] = $path; 216 } 217 218 /** 219 * @param string $old 220 * @param string $new 221 * 222 * @throws InvalidArgumentException 223 */ 224 public static function registerCustomView(string $old, string $new): void 225 { 226 if (Str::contains($old, self::NAMESPACE_SEPARATOR) && Str::contains($new, self::NAMESPACE_SEPARATOR)) { 227 self::$replacements[$old] = $new; 228 } else { 229 throw new InvalidArgumentException(); 230 } 231 } 232 233 /** 234 * Find the file for a view. 235 * 236 * @param string $view_name 237 * 238 * @return string 239 * @throws Exception 240 */ 241 public function getFilenameForView(string $view_name): string 242 { 243 if (!Str::contains($view_name, self::NAMESPACE_SEPARATOR)) { 244 $view_name = self::NAMESPACE_SEPARATOR . $view_name; 245 } 246 247 // Apply replacements / customisations 248 while (array_key_exists($view_name, self::$replacements)) { 249 $view_name = self::$replacements[$view_name]; 250 } 251 252 [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2); 253 254 if ((self::$namespaces[$namespace] ?? null) === null) { 255 throw new RuntimeException('Namespace "' . e($namespace) . '" not found.'); 256 } 257 258 $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION; 259 260 if (!is_file($view_file)) { 261 throw new RuntimeException('View file not found: ' . e($view_file)); 262 } 263 264 return $view_file; 265 } 266 267 /** 268 * Cerate and render a view in a single operation. 269 * 270 * @param string $name 271 * @param mixed[] $data 272 * 273 * @return string 274 */ 275 public static function make($name, $data = []): string 276 { 277 $view = new static($name, $data); 278 279 DebugBar::addView($name, $data); 280 281 return $view->render(); 282 } 283} 284