1f3281ad6SGreg Roach<?php 23976b470SGreg Roach 3f3281ad6SGreg Roach/** 4f3281ad6SGreg Roach * webtrees: online genealogy 5*d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 6f3281ad6SGreg Roach * This program is free software: you can redistribute it and/or modify 7f3281ad6SGreg Roach * it under the terms of the GNU General Public License as published by 8f3281ad6SGreg Roach * the Free Software Foundation, either version 3 of the License, or 9f3281ad6SGreg Roach * (at your option) any later version. 10f3281ad6SGreg Roach * This program is distributed in the hope that it will be useful, 11f3281ad6SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12f3281ad6SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13f3281ad6SGreg Roach * GNU General Public License for more details. 14f3281ad6SGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16f3281ad6SGreg Roach */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 20f3281ad6SGreg Roachnamespace Fisharebest\Webtrees; 21f3281ad6SGreg Roach 2250c68a25SGreg Roachuse Exception; 233e4bf26fSGreg Roachuse InvalidArgumentException; 24e50f5fc6SGreg Roachuse LogicException; 253e4bf26fSGreg Roachuse RuntimeException; 2670f31542SGreg Roachuse Throwable; 273976b470SGreg Roach 283e4bf26fSGreg Roachuse function array_key_exists; 293e4bf26fSGreg Roachuse function explode; 30d7952a34SGreg Roachuse function extract; 31d7952a34SGreg Roachuse function implode; 32d7952a34SGreg Roachuse function is_file; 33d7952a34SGreg Roachuse function ob_end_clean; 34d3da353bSGreg Roachuse function ob_get_level; 35d7952a34SGreg Roachuse function ob_start; 36d7952a34SGreg Roachuse function sha1; 37dec352c1SGreg Roachuse function str_contains; 38dec352c1SGreg Roachuse function str_ends_with; 39721424feSGreg Roachuse function strlen; 40721424feSGreg Roachuse function strncmp; 4150c68a25SGreg Roach 42ef5d23f1SGreg Roachuse const EXTR_OVERWRITE; 43ef5d23f1SGreg Roach 44f3281ad6SGreg Roach/** 45f3281ad6SGreg Roach * Simple view/template class. 46f3281ad6SGreg Roach */ 47c1010edaSGreg Roachclass View 48c1010edaSGreg Roach{ 493e4bf26fSGreg Roach public const NAMESPACE_SEPARATOR = '::'; 50dd6b2bfcSGreg Roach 5116d6367aSGreg Roach private const TEMPLATE_EXTENSION = '.phtml'; 52dd6b2bfcSGreg Roach 5343f2f523SGreg Roach private string $name; 54f3281ad6SGreg Roach 5543f2f523SGreg Roach /** @var array<mixed> */ 5643f2f523SGreg Roach private array $data; 57f3281ad6SGreg Roach 58f3281ad6SGreg Roach /** 5909482a55SGreg Roach * @var array<string> Where do the templates live, for each namespace. 603e4bf26fSGreg Roach */ 615f2657cbSGreg Roach private static array $namespaces = [ 62f397d0fdSGreg Roach '' => Webtrees::ROOT_DIR . 'resources/views/', 633e4bf26fSGreg Roach ]; 643e4bf26fSGreg Roach 653e4bf26fSGreg Roach /** 6609482a55SGreg Roach * @var array<string> Modules can replace core views with their own. 673e4bf26fSGreg Roach */ 685f2657cbSGreg Roach private static array $replacements = []; 693e4bf26fSGreg Roach 703e4bf26fSGreg Roach /** 71ecf66805SGreg Roach * @var string Implementation of Blade "stacks". 72ecf66805SGreg Roach */ 7333c746f1SGreg Roach private static string $stack; 74ecf66805SGreg Roach 75ecf66805SGreg Roach /** 7633c746f1SGreg Roach * @var array<array<string>> Implementation of Blade "stacks". 77ecf66805SGreg Roach */ 7833c746f1SGreg Roach private static array $stacks = []; 79ecf66805SGreg Roach 80ecf66805SGreg Roach /** 81f3281ad6SGreg Roach * Create a view from a template name and optional data. 82f3281ad6SGreg Roach * 83fa4036e8SGreg Roach * @param string $name 845d4b7ec2SGreg Roach * @param array<mixed> $data 85f3281ad6SGreg Roach */ 8673d58381SGreg Roach public function __construct(string $name, array $data = []) 87c1010edaSGreg Roach { 88f3281ad6SGreg Roach $this->name = $name; 89f3281ad6SGreg Roach $this->data = $data; 90f3281ad6SGreg Roach } 91f3281ad6SGreg Roach 92f3281ad6SGreg Roach /** 93ecf66805SGreg Roach * Implementation of Blade "stacks". 94ecf66805SGreg Roach * 95ecf66805SGreg Roach * @see https://laravel.com/docs/5.5/blade#stacks 96962e29c9SGreg Roach * 97962e29c9SGreg Roach * @param string $stack 98fa4036e8SGreg Roach * 99fa4036e8SGreg Roach * @return void 100ecf66805SGreg Roach */ 101e364afe4SGreg Roach public static function push(string $stack): void 102c1010edaSGreg Roach { 103ecf66805SGreg Roach self::$stack = $stack; 104d7952a34SGreg Roach 105ecf66805SGreg Roach ob_start(); 106ecf66805SGreg Roach } 107ecf66805SGreg Roach 108ecf66805SGreg Roach /** 109ecf66805SGreg Roach * Implementation of Blade "stacks". 110fa4036e8SGreg Roach * 111fa4036e8SGreg Roach * @return void 112ecf66805SGreg Roach */ 113e364afe4SGreg Roach public static function endpush(): void 114c1010edaSGreg Roach { 11588de55fdSRico Sonntag $content = ob_get_clean(); 11688de55fdSRico Sonntag 117e50f5fc6SGreg Roach if ($content === false) { 118e50f5fc6SGreg Roach throw new LogicException('found endpush(), but did not find push()'); 119e50f5fc6SGreg Roach } 120e50f5fc6SGreg Roach 121d7952a34SGreg Roach self::$stacks[self::$stack][] = $content; 122d7952a34SGreg Roach } 123d7952a34SGreg Roach 124d7952a34SGreg Roach /** 125d7952a34SGreg Roach * Variant of push that will only add one copy of each item. 126d7952a34SGreg Roach * 127d7952a34SGreg Roach * @param string $stack 128d7952a34SGreg Roach * 129d7952a34SGreg Roach * @return void 130d7952a34SGreg Roach */ 131e364afe4SGreg Roach public static function pushunique(string $stack): void 132d7952a34SGreg Roach { 133d7952a34SGreg Roach self::$stack = $stack; 134d7952a34SGreg Roach 135d7952a34SGreg Roach ob_start(); 136d7952a34SGreg Roach } 137d7952a34SGreg Roach 138d7952a34SGreg Roach /** 139d7952a34SGreg Roach * Variant of push that will only add one copy of each item. 140d7952a34SGreg Roach * 141d7952a34SGreg Roach * @return void 142d7952a34SGreg Roach */ 143e364afe4SGreg Roach public static function endpushunique(): void 144d7952a34SGreg Roach { 145d7952a34SGreg Roach $content = ob_get_clean(); 146d7952a34SGreg Roach 147e50f5fc6SGreg Roach if ($content === false) { 148e50f5fc6SGreg Roach throw new LogicException('found endpushunique(), but did not find pushunique()'); 149e50f5fc6SGreg Roach } 150e50f5fc6SGreg Roach 151d7952a34SGreg Roach self::$stacks[self::$stack][sha1($content)] = $content; 152ecf66805SGreg Roach } 153ecf66805SGreg Roach 154ecf66805SGreg Roach /** 155ecf66805SGreg Roach * Implementation of Blade "stacks". 156ecf66805SGreg Roach * 157962e29c9SGreg Roach * @param string $stack 158962e29c9SGreg Roach * 159ecf66805SGreg Roach * @return string 160ecf66805SGreg Roach */ 161c1010edaSGreg Roach public static function stack(string $stack): string 162c1010edaSGreg Roach { 163ecf66805SGreg Roach $content = implode('', self::$stacks[$stack] ?? []); 164ecf66805SGreg Roach 165ecf66805SGreg Roach self::$stacks[$stack] = []; 166ecf66805SGreg Roach 167ecf66805SGreg Roach return $content; 168ecf66805SGreg Roach } 169ecf66805SGreg Roach 170ecf66805SGreg Roach /** 171f3281ad6SGreg Roach * Render a view. 172f3281ad6SGreg Roach * 173f3281ad6SGreg Roach * @return string 17470f31542SGreg Roach * @throws Throwable 175f3281ad6SGreg Roach */ 1768f53f488SRico Sonntag public function render(): string 177c1010edaSGreg Roach { 178ef5d23f1SGreg Roach extract($this->data, EXTR_OVERWRITE); 179f3281ad6SGreg Roach 18070f31542SGreg Roach try { 181f3281ad6SGreg Roach ob_start(); 182bc241c54SGreg Roach // Do not use require, so we can catch errors for missing files 183bc241c54SGreg Roach include $this->getFilenameForView($this->name); 18475d70144SGreg Roach 185ac701fbdSGreg Roach return (string) ob_get_clean(); 18670f31542SGreg Roach } catch (Throwable $ex) { 187d3da353bSGreg Roach while (ob_get_level() > 0) { 18870f31542SGreg Roach ob_end_clean(); 189d3da353bSGreg Roach } 19070f31542SGreg Roach throw $ex; 19170f31542SGreg Roach } 192f3281ad6SGreg Roach } 193f3281ad6SGreg Roach 194f3281ad6SGreg Roach /** 1953e4bf26fSGreg Roach * @param string $namespace 1963e4bf26fSGreg Roach * @param string $path 1973e4bf26fSGreg Roach * 1983e4bf26fSGreg Roach * @throws InvalidArgumentException 1993e4bf26fSGreg Roach */ 2003e4bf26fSGreg Roach public static function registerNamespace(string $namespace, string $path): void 2013e4bf26fSGreg Roach { 2023e4bf26fSGreg Roach if ($namespace === '') { 2033e4bf26fSGreg Roach throw new InvalidArgumentException('Cannot register the default namespace'); 2043e4bf26fSGreg Roach } 2053e4bf26fSGreg Roach 206dec352c1SGreg Roach if (!str_ends_with($path, '/')) { 2073e4bf26fSGreg Roach throw new InvalidArgumentException('Paths must end with a directory separator'); 2083e4bf26fSGreg Roach } 2093e4bf26fSGreg Roach 2103e4bf26fSGreg Roach self::$namespaces[$namespace] = $path; 2113e4bf26fSGreg Roach } 2123e4bf26fSGreg Roach 2133e4bf26fSGreg Roach /** 2143e4bf26fSGreg Roach * @param string $old 2153e4bf26fSGreg Roach * @param string $new 2163e4bf26fSGreg Roach * 2173e4bf26fSGreg Roach * @throws InvalidArgumentException 2183e4bf26fSGreg Roach */ 2193e4bf26fSGreg Roach public static function registerCustomView(string $old, string $new): void 2203e4bf26fSGreg Roach { 221dec352c1SGreg Roach if (str_contains($old, self::NAMESPACE_SEPARATOR) && str_contains($new, self::NAMESPACE_SEPARATOR)) { 2223e4bf26fSGreg Roach self::$replacements[$old] = $new; 2233e4bf26fSGreg Roach } else { 2243e4bf26fSGreg Roach throw new InvalidArgumentException(); 2253e4bf26fSGreg Roach } 2263e4bf26fSGreg Roach } 2273e4bf26fSGreg Roach 2283e4bf26fSGreg Roach /** 2293e4bf26fSGreg Roach * Find the file for a view. 230f3281ad6SGreg Roach * 231f3281ad6SGreg Roach * @param string $view_name 232f3281ad6SGreg Roach * 23375d70144SGreg Roach * @return string 23450c68a25SGreg Roach * @throws Exception 235f3281ad6SGreg Roach */ 2363e4bf26fSGreg Roach public function getFilenameForView(string $view_name): string 237c1010edaSGreg Roach { 238dec352c1SGreg Roach // If we request "::view", then use it explicitly. Don't allow replacements. 239721424feSGreg Roach // NOTE: cannot use str_starts_with() as it wasn't available in 2.0.6, and is called by the upgrade wizard. 240721424feSGreg Roach $explicit = strncmp($view_name, self::NAMESPACE_SEPARATOR, strlen(self::NAMESPACE_SEPARATOR)) === 0; 241fdbcd0efSGreg Roach 242dec352c1SGreg Roach if (!str_contains($view_name, self::NAMESPACE_SEPARATOR)) { 2433e4bf26fSGreg Roach $view_name = self::NAMESPACE_SEPARATOR . $view_name; 2443e4bf26fSGreg Roach } 24575d70144SGreg Roach 246dec352c1SGreg Roach // Apply replacements / customizations 247fdbcd0efSGreg Roach while (!$explicit && array_key_exists($view_name, self::$replacements)) { 2483e4bf26fSGreg Roach $view_name = self::$replacements[$view_name]; 2493e4bf26fSGreg Roach } 2503e4bf26fSGreg Roach 2513e4bf26fSGreg Roach [$namespace, $view_name] = explode(self::NAMESPACE_SEPARATOR, $view_name, 2); 2523e4bf26fSGreg Roach 2533e4bf26fSGreg Roach if ((self::$namespaces[$namespace] ?? null) === null) { 2543e4bf26fSGreg Roach throw new RuntimeException('Namespace "' . e($namespace) . '" not found.'); 2553e4bf26fSGreg Roach } 2563e4bf26fSGreg Roach 2573e4bf26fSGreg Roach $view_file = self::$namespaces[$namespace] . $view_name . self::TEMPLATE_EXTENSION; 2583e4bf26fSGreg Roach 2593e4bf26fSGreg Roach if (!is_file($view_file)) { 2603e4bf26fSGreg Roach throw new RuntimeException('View file not found: ' . e($view_file)); 2613e4bf26fSGreg Roach } 2623e4bf26fSGreg Roach 26350c68a25SGreg Roach return $view_file; 264f21917b2SGreg Roach } 26550c68a25SGreg Roach 266f3281ad6SGreg Roach /** 267219fc02dSGreg Roach * Create and render a view in a single operation. 268f3281ad6SGreg Roach * 269f3281ad6SGreg Roach * @param string $name 27009482a55SGreg Roach * @param array<mixed> $data 271f3281ad6SGreg Roach * 272f3281ad6SGreg Roach * @return string 273f3281ad6SGreg Roach */ 27424f2a3afSGreg Roach public static function make(string $name, array $data = []): string 275c1010edaSGreg Roach { 2761e752ebbSGreg Roach $view = new self($name, $data); 277f3281ad6SGreg Roach 278f3281ad6SGreg Roach return $view->render(); 279f3281ad6SGreg Roach } 280f3281ad6SGreg Roach} 281