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