forkbb/app/Core/View.php

300 lines
8.1 KiB
PHP
Raw Normal View History

2017-02-14 13:05:26 +00:00
<?php
2020-12-21 10:40:19 +00:00
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
/**
* based on Dirk <https://github.com/artoodetoo/dirk>
*
* @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
* @license The MIT License (MIT)
*/
2017-02-14 13:05:26 +00:00
2020-10-14 13:01:43 +00:00
declare(strict_types=1);
2017-02-14 13:05:26 +00:00
namespace ForkBB\Core;
use ForkBB\Core\View\Compiler;
2017-11-03 13:06:22 +00:00
use ForkBB\Models\Page;
2017-02-14 13:05:26 +00:00
use RuntimeException;
class View
2017-02-14 13:05:26 +00:00
{
protected string $ext = '.forkbb.php';
protected string $preFile = '';
2017-02-14 13:05:26 +00:00
protected ?Compiler $compilerObj;
protected string $compilerClass = Compiler::class;
2023-10-04 12:28:30 +00:00
protected string $cacheDir;
protected string $defaultDir;
protected string $defaultHash;
2017-12-14 12:16:09 +00:00
protected array $other = [];
protected array $composers = [];
protected array $blocks = [];
protected array $blockStack = [];
protected array $templates = [];
2020-10-15 13:02:24 +00:00
public function __construct(string|array $config, mixed $views)
{
if (\is_array($config)) {
2023-10-04 12:28:30 +00:00
$this->cacheDir = $config['cache'];
$this->defaultDir = $config['defaultDir'];
2017-12-14 12:16:09 +00:00
2023-09-20 14:01:08 +00:00
if (! empty($config['userDir'])) {
2023-10-04 12:28:30 +00:00
$this->addTplDir($config['userDir'], 10);
2023-09-20 14:01:08 +00:00
}
2017-12-14 12:16:09 +00:00
if (! empty($config['composers'])) {
foreach ($config['composers'] as $name => $composer) {
$this->composer($name, $composer);
}
}
if (! empty($config['compiler'])) {
$this->compilerClass = $config['compiler'];
}
if (! empty($config['preFile'])) {
$this->preFile = $config['preFile'];
}
} else {
// для rev. 68 и ниже
2023-10-04 12:28:30 +00:00
$this->cacheDir = $config;
$this->defaultDir = $views;
2017-02-14 13:05:26 +00:00
}
2020-07-03 08:28:10 +00:00
$this->defaultHash = \hash('md5', $this->defaultDir);
2017-02-14 13:05:26 +00:00
}
2023-10-04 12:28:30 +00:00
/**
* Добавляет новый каталог шаблонов $pathToDir.
* Сортирует список каталогов в соответствии с приоритетом $priority. По убыванию.
*/
public function addTplDir(string $pathToDir, int $priority): View
{
$this->other[\hash('md5', $pathToDir)] = [$pathToDir, $priority];
if (\count($this->other) > 1) {
\uasort($this->other, function (array $a, array $b) {
return $b[1] <=> $a[1];
});
}
return $this;
}
2017-11-03 13:06:22 +00:00
/**
* Возвращает отображение страницы $p или null
2017-11-03 13:06:22 +00:00
*/
public function rendering(Page $p): ?string
2017-02-14 13:05:26 +00:00
{
2017-11-03 13:06:22 +00:00
if (null === $p->nameTpl) {
$this->sendHttpHeaders($p);
2017-02-14 13:05:26 +00:00
return null;
}
2017-11-03 13:06:22 +00:00
$p->prepare();
$this->templates[] = $p->nameTpl;
2018-03-04 05:30:07 +00:00
while ($_name = \array_shift($this->templates)) {
2017-11-03 13:06:22 +00:00
$this->beginBlock('content');
2017-11-03 13:06:22 +00:00
foreach ($this->composers as $_cname => $_cdata) {
2018-03-04 05:30:07 +00:00
if (\preg_match($_cname, $_name)) {
2017-11-03 13:06:22 +00:00
foreach ($_cdata as $_citem) {
2018-03-04 05:30:07 +00:00
\extract((\is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
2017-11-03 13:06:22 +00:00
}
}
}
require $this->prepare($_name);
2017-11-03 13:06:22 +00:00
$this->endBlock(true);
}
2020-07-03 08:28:10 +00:00
$this->sendHttpHeaders($p);
2017-11-03 13:06:22 +00:00
return $this->block('content');
2017-02-14 13:05:26 +00:00
}
/**
* Отправляет HTTP заголовки
*/
protected function sendHttpHeaders(Page $p): void
{
foreach ($p->httpHeaders as $catHeader) {
foreach ($catHeader as $header) {
\header($header[0], $header[1]);
}
}
}
/**
* Возвращает отображение шаблона $name
*/
public function fetch(string $name, array $data = []): string
{
$this->templates[] = $name;
if (! empty($data)) {
\extract($data);
}
while ($_name = \array_shift($this->templates)) {
$this->beginBlock('content');
foreach ($this->composers as $_cname => $_cdata) {
if (\preg_match($_cname, $_name)) {
foreach ($_cdata as $_citem) {
\extract((\is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
}
}
}
require $this->prepare($_name);
$this->endBlock(true);
}
return $this->block('content');
}
/**
* Add view composer
* @param mixed $name template name or array of names
* @param mixed $composer data in the same meaning as for fetch() call, or callable returning such data
*/
public function composer(string|array $name, mixed $composer): void
{
if (\is_array($name)) {
foreach ($name as $n) {
$this->composer($n, $composer);
}
} else {
$p = '~^'
. \str_replace('\*', '[^' . $this->separator . ']+', \preg_quote($name, $this->separator . '~'))
. '$~';
$this->composers[$p][] = $composer;
}
}
/**
* Подготавливает файл для подключения
*/
protected function prepare(string $name): string
{
$st = \preg_replace('%\W%', '-', $name);
foreach ($this->other as $hash => $cur) {
2023-09-20 13:51:21 +00:00
if (\file_exists($tpl = "{$cur[0]}/{$name}{$this->ext}")) {
2023-10-04 12:28:30 +00:00
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
if (
! \file_exists($php)
|| \filemtime($tpl) > \filemtime($php)
) {
$this->create($php, $tpl, $name);
}
return $php;
}
}
$hash = $this->defaultHash;
$tpl = "{$this->defaultDir}/{$name}{$this->ext}";
2023-10-04 12:28:30 +00:00
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
if (
! \file_exists($php)
|| \filemtime($tpl) > \filemtime($php)
) {
$this->create($php, $tpl, $name);
}
return $php;
}
/**
* Удаляет файлы кэша для шаблона $name
*/
public function delete(string $name): void
{
$st = \preg_replace('%\W%', '-', $name);
\array_map('\\unlink', \glob("{$this->cacheDir}/_{$st}-*.php"));
}
/**
* Генерирует $php файл на основе шаблона $tpl
*/
protected function create(string $php, string $tpl, string $name): void
{
if (empty($this->compilerObj)) {
$this->compilerObj = new $this->compilerClass($this->preFile);
}
$text = $this->compilerObj->create($name, \file_get_contents($tpl), \hash('fnv1a32', $tpl));
if (false === \file_put_contents($php, $text, \LOCK_EX)) {
throw new RuntimeException("Failed to write {$php} file");
}
if (\function_exists('\\opcache_invalidate')) {
\opcache_invalidate($php, true);
}
}
/**
* Задает родительский шаблон
*/
protected function extend(string $name): void
{
$this->templates[] = $name;
}
/**
* Возвращает содержимое блока или $default
*/
protected function block(string $name, string $default = ''): string
{
return \array_key_exists($name, $this->blocks)
? $this->blocks[$name]
: $default;
}
/**
* Задает начало блока
*/
protected function beginBlock(string $name): void
{
$this->blockStack[] = $name;
\ob_start();
}
/**
* Задает конец блока
*/
protected function endBlock(bool $overwrite = false): string
{
$name = \array_pop($this->blockStack);
if (
$overwrite
|| ! \array_key_exists($name, $this->blocks)
) {
$this->blocks[$name] = \ob_get_clean();
} else {
$this->blocks[$name] .= \ob_get_clean();
}
return $name;
}
2017-02-14 13:05:26 +00:00
}