Bicycle/System/Classes/View.php
Egor Isaev c9b81cdcd1 dev
2026-08-14 13:09:54 +03:00

215 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @package Bicycle
* @author Egor Isaev
* @description View.php
* @copyright (c) 03/06/2026
*/
namespace System\Classes;
use Throwable;
/**
* Рендеринг .html-шаблонов через ob_start + extract + include.
*/
class View
{
/**
* @var string Полный путь к найденному файлу шаблона
*/
public string $_file;
/**
* @var array Данные, доступные внутри шаблона
*/
public array $_data = [];
/**
* @var array<int,string> Зарегистрированные пути CSS-файлов — общие для всех View за
* один запрос (content рендерится раньше layout, см. Controller::render())
*/
private static array $_styles = [];
/**
* @var array<int,array{src:string,type:string}> Зарегистрированные JS-файлы
*/
private static array $_scripts = [];
/**
* @var string|null Путь к зарегистрированному manifest-файлу
*/
private static ?string $_manifest = null;
/**
* @param string|null $file Имя файла шаблона без расширения
* @param string $dir Каталог шаблона относительно базовых путей
* @param array|null $data Данные для шаблона
*/
public function __construct(?string $file = null, string $dir = '', ?array $data = null)
{
if ($file !== null) {
$this->setFilename($file, $dir);
}
if ($data !== null) {
// Добавить значения к текущим данным
$this->_data = $data + $this->_data;
}
}
/**
* Устанавливает файл шаблона, проверяя его существование.
*
* @param string $file Имя файла шаблона без расширения
* @param string $dir Каталог шаблона (обязателен)
* @return View
*/
public function setFilename(string $file, string $dir = ''): self
{
if ($dir === '') {
throw new MyException("Параметр dir не передан в View::setFilename для файла '$file'");
}
if (($path = Core::findFile($dir, $file, 'html')) === false) {
throw new MyException("При запросе view файл $dir/$file не найден");
}
// Store the file path locally
$this->_file = $path;
return $this;
}
/**
* Выполняет шаблон и возвращает результат как строку.
*
* @return string Отрендеренный HTML
* @throws MyException Если файл не установлен или ошибка при рендеринге
*/
public function render(): string
{
if (empty($this->_file)) {
throw new MyException('Нужно установить файл для использования во view перед преобразованием');
}
ob_start();
extract($this->_data, EXTR_SKIP);
try {
include $this->_file;
return ob_get_clean();
} catch (Throwable $e) {
ob_end_clean();
throw new MyException(
"Ошибка при render файла: {$e->getFile()} на строке {$e->getLine()} - {$e->getMessage()}", null, 0, $e
);
}
}
/**
* Регистрирует CSS-файл из App/media/css — вызывается прямо из шаблона:
* <?php $this->setStyle('about.css'); ?>. Выводится позже через getStyles()
* (обычно из layout.html, после того как content уже отрендерен).
*
* @param string $file Имя файла с расширением, например 'about.css'
* @return void
*/
public function setStyle(string $file): void
{
$name = pathinfo($file, PATHINFO_FILENAME);
$path = Core::findFile('media/css', $name, 'css');
if ($path === false) {
echo '<div class="alert alert-danger" role="alert">Файл \'' .
htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '\' в media/css не найден</div>';
return;
}
// ?v=mtime — иначе браузер может годами отдавать закэшированную старую версию файла
// после правки, без единого признака ошибки на сервере (см. feedback в памяти проекта).
self::$_styles[] = "/App/media/css/$name.css?v=" . filemtime($path);
}
/**
* Регистрирует JS-файл из App/media/js — вызывается прямо из шаблона:
* <?php $this->setScript('about.js'); ?>. Выводится позже через getScripts().
*
* @param string $file Имя файла с расширением, например 'about.js'
* @param string $type Атрибут type у <script>
* @return void
*/
public function setScript(string $file, string $type = 'text/javascript'): void
{
$name = pathinfo($file, PATHINFO_FILENAME);
$path = Core::findFile('media/js', $name, 'js');
if ($path === false) {
echo '<div class="alert alert-danger" role="alert">Файл \'' .
htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '\' в media/js не найден</div>';
return;
}
// ?v=mtime — та же причина, что и в setStyle().
self::$_scripts[] = ['src' => "/App/media/js/$name.js?v=" . filemtime($path), 'type' => $type];
}
/**
* Регистрирует manifest (PWA) из App/media — <?php $this->setManifest('manifest.json'); ?>.
*
* @param string $file Имя файла, например 'manifest.json' или 'manifest.webmanifest'
* @return void
*/
public function setManifest(string $file): void
{
$name = pathinfo($file, PATHINFO_FILENAME);
$ext = pathinfo($file, PATHINFO_EXTENSION) ?: 'json';
if (Core::findFile('media', $name, $ext) === false) {
echo '<div class="alert alert-danger" role="alert">Файл \'' .
htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '\' в media не найден</div>';
return;
}
self::$_manifest = "/App/media/$name.$ext";
}
/**
* Выводит <link> для manifest и всех зарегистрированных стилей — вызывать в <head> layout.html:
* <?php $this->getStyles(); ?>.
*
* @return void
*/
public function getStyles(): void
{
if (self::$_manifest !== null) {
echo '<link rel="manifest" href="' . htmlspecialchars(self::$_manifest, ENT_QUOTES, 'UTF-8') . "\">\n";
}
foreach (self::$_styles as $href) {
echo '<link rel="stylesheet" href="' . htmlspecialchars($href, ENT_QUOTES, 'UTF-8') . "\">\n";
}
}
/**
* Выводит <script> для всех зарегистрированных скриптов — вызывать перед </body> layout.html:
* <?php $this->getScripts(); ?>.
*
* @return void
*/
public function getScripts(): void
{
foreach (self::$_scripts as $script) {
echo '<script type="' . htmlspecialchars($script['type'], ENT_QUOTES, 'UTF-8') . '" src="' .
htmlspecialchars($script['src'], ENT_QUOTES, 'UTF-8') . "\"></script>\n";
}
}
}