first commit
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Config\Factories;
|
||||
use CodeIgniter\View\Cells\Cell as BaseCell;
|
||||
use CodeIgniter\View\Exceptions\ViewException;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Class Cell
|
||||
*
|
||||
* A simple class that can call any other class that can be loaded,
|
||||
* and echo out it's result. Intended for displaying small blocks of
|
||||
* content within views that can be managed by other libraries and
|
||||
* not require they are loaded within controller.
|
||||
*
|
||||
* Used with the helper function, it's use will look like:
|
||||
*
|
||||
* viewCell('\Some\Class::method', 'limit=5 sort=asc', 60, 'cache-name');
|
||||
*
|
||||
* Parameters are matched up with the callback method's arguments of the same name:
|
||||
*
|
||||
* class Class {
|
||||
* function method($limit, $sort)
|
||||
* }
|
||||
*
|
||||
* Alternatively, the params will be passed into the callback method as a simple array
|
||||
* if matching params are not found.
|
||||
*
|
||||
* class Class {
|
||||
* function method(array $params=null)
|
||||
* }
|
||||
*
|
||||
* @see \CodeIgniter\View\CellTest
|
||||
*/
|
||||
class Cell
|
||||
{
|
||||
/**
|
||||
* Instance of the current Cache Instance
|
||||
*
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Cell constructor.
|
||||
*/
|
||||
public function __construct(CacheInterface $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a cell, returning its body as a string.
|
||||
*
|
||||
* @param string $library Cell class and method name.
|
||||
* @param array<string, string>|string|null $params Parameters to pass to the method.
|
||||
* @param int $ttl Number of seconds to cache the cell.
|
||||
* @param string|null $cacheName Cache item name.
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function render(string $library, $params = null, int $ttl = 0, ?string $cacheName = null): string
|
||||
{
|
||||
[$instance, $method] = $this->determineClass($library);
|
||||
|
||||
$class = is_object($instance)
|
||||
? $instance::class
|
||||
: null;
|
||||
|
||||
$params = $this->prepareParams($params);
|
||||
|
||||
// Is the output cached?
|
||||
$cacheName ??= str_replace(['\\', '/'], '', $class) . $method . md5(serialize($params));
|
||||
|
||||
if ($output = $this->cache->get($cacheName)) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
if (method_exists($instance, 'initController')) {
|
||||
$instance->initController(service('request'), service('response'), service('logger'));
|
||||
}
|
||||
|
||||
if (! method_exists($instance, $method)) {
|
||||
throw ViewException::forInvalidCellMethod($class, $method);
|
||||
}
|
||||
|
||||
$output = $instance instanceof BaseCell
|
||||
? $this->renderCell($instance, $method, $params)
|
||||
: $this->renderSimpleClass($instance, $method, $params, $class);
|
||||
|
||||
// Can we cache it?
|
||||
if ($ttl !== 0) {
|
||||
$this->cache->save($cacheName, $output, $ttl);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the params attribute. If an array, returns untouched.
|
||||
* If a string, it should be in the format "key1=value key2=value".
|
||||
* It will be split and returned as an array.
|
||||
*
|
||||
* @param array<string, string>|string|null $params
|
||||
* @phpstan-param array<string, string>|string|float|null $params
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function prepareParams($params)
|
||||
{
|
||||
if (
|
||||
($params === null || $params === '' || $params === [])
|
||||
|| (! is_string($params) && ! is_array($params))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_string($params)) {
|
||||
$newParams = [];
|
||||
$separator = ' ';
|
||||
|
||||
if (str_contains($params, ',')) {
|
||||
$separator = ',';
|
||||
}
|
||||
|
||||
$params = explode($separator, $params);
|
||||
unset($separator);
|
||||
|
||||
foreach ($params as $p) {
|
||||
if ($p !== '') {
|
||||
[$key, $val] = explode('=', $p);
|
||||
|
||||
$newParams[trim($key)] = trim($val, ', ');
|
||||
}
|
||||
}
|
||||
|
||||
$params = $newParams;
|
||||
unset($newParams);
|
||||
}
|
||||
|
||||
if ($params === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the library string, attempts to determine the class and method
|
||||
* to call.
|
||||
*/
|
||||
protected function determineClass(string $library): array
|
||||
{
|
||||
// We don't want to actually call static methods
|
||||
// by default, so convert any double colons.
|
||||
$library = str_replace('::', ':', $library);
|
||||
|
||||
// controlled cells might be called with just
|
||||
// the class name, so add a default method
|
||||
if (! str_contains($library, ':')) {
|
||||
$library .= ':render';
|
||||
}
|
||||
|
||||
[$class, $method] = explode(':', $library);
|
||||
|
||||
if ($class === '') {
|
||||
throw ViewException::forNoCellClass();
|
||||
}
|
||||
|
||||
// locate and return an instance of the cell
|
||||
$object = Factories::cells($class, ['getShared' => false]);
|
||||
|
||||
if (! is_object($object)) {
|
||||
throw ViewException::forInvalidCellClass($class);
|
||||
}
|
||||
|
||||
if ($method === '') {
|
||||
$method = 'index';
|
||||
}
|
||||
|
||||
return [
|
||||
$object,
|
||||
$method,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a cell that extends the BaseCell class.
|
||||
*/
|
||||
final protected function renderCell(BaseCell $instance, string $method, array $params): string
|
||||
{
|
||||
// Only allow public properties to be set, or protected/private
|
||||
// properties that have a method to get them (get<Foo>Property())
|
||||
$publicProperties = $instance->getPublicProperties();
|
||||
$privateProperties = array_column($instance->getNonPublicProperties(), 'name');
|
||||
$publicParams = array_intersect_key($params, $publicProperties);
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$getter = 'get' . ucfirst((string) $key) . 'Property';
|
||||
if (in_array($key, $privateProperties, true) && method_exists($instance, $getter)) {
|
||||
$publicParams[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in any public properties that were passed in
|
||||
// but only ones that are in the $pulibcProperties array.
|
||||
$instance = $instance->fill($publicParams);
|
||||
|
||||
// If there are any protected/private properties, we need to
|
||||
// send them to the mount() method.
|
||||
if (method_exists($instance, 'mount')) {
|
||||
// if any $params have keys that match the name of an argument in the
|
||||
// mount method, pass those variables to the method.
|
||||
$mountParams = $this->getMethodParams($instance, 'mount', $params);
|
||||
$instance->mount(...$mountParams);
|
||||
}
|
||||
|
||||
return $instance->{$method}();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values from $params that match the parameters
|
||||
* for a method, in the order they are defined. This allows
|
||||
* them to be passed directly into the method.
|
||||
*/
|
||||
private function getMethodParams(BaseCell $instance, string $method, array $params): array
|
||||
{
|
||||
$mountParams = [];
|
||||
|
||||
try {
|
||||
$reflectionMethod = new ReflectionMethod($instance, $method);
|
||||
$reflectionParams = $reflectionMethod->getParameters();
|
||||
|
||||
foreach ($reflectionParams as $reflectionParam) {
|
||||
$paramName = $reflectionParam->getName();
|
||||
|
||||
if (array_key_exists($paramName, $params)) {
|
||||
$mountParams[] = $params[$paramName];
|
||||
}
|
||||
}
|
||||
} catch (ReflectionException) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return $mountParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the non-Cell class, passing in the string/array params.
|
||||
*
|
||||
* @todo Determine if this can be refactored to use $this-getMethodParams().
|
||||
*
|
||||
* @param object $instance
|
||||
*/
|
||||
final protected function renderSimpleClass($instance, string $method, array $params, string $class): string
|
||||
{
|
||||
// Try to match up the parameter list we were provided
|
||||
// with the parameter name in the callback method.
|
||||
$refMethod = new ReflectionMethod($instance, $method);
|
||||
$paramCount = $refMethod->getNumberOfParameters();
|
||||
$refParams = $refMethod->getParameters();
|
||||
|
||||
if ($paramCount === 0) {
|
||||
if ($params !== []) {
|
||||
throw ViewException::forMissingCellParameters($class, $method);
|
||||
}
|
||||
|
||||
$output = $instance->{$method}();
|
||||
} elseif (($paramCount === 1)
|
||||
&& ((! array_key_exists($refParams[0]->name, $params))
|
||||
|| (array_key_exists($refParams[0]->name, $params)
|
||||
&& count($params) !== 1))
|
||||
) {
|
||||
$output = $instance->{$method}($params);
|
||||
} else {
|
||||
$fireArgs = [];
|
||||
$methodParams = [];
|
||||
|
||||
foreach ($refParams as $arg) {
|
||||
$methodParams[$arg->name] = true;
|
||||
if (array_key_exists($arg->name, $params)) {
|
||||
$fireArgs[$arg->name] = $params[$arg->name];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($params) as $key) {
|
||||
if (! isset($methodParams[$key])) {
|
||||
throw ViewException::forInvalidCellParameter($key);
|
||||
}
|
||||
}
|
||||
|
||||
$output = $instance->{$method}(...array_values($fireArgs));
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View\Cells;
|
||||
|
||||
use CodeIgniter\Traits\PropertiesTrait;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Class Cell
|
||||
*
|
||||
* The base class that View Cells should extend.
|
||||
* Provides extended features for managing/rendering
|
||||
* a single cell's contents.
|
||||
*
|
||||
* @function mount()
|
||||
*/
|
||||
class Cell implements Stringable
|
||||
{
|
||||
use PropertiesTrait;
|
||||
|
||||
/**
|
||||
* The name of the view to render.
|
||||
* If empty, will be determined based
|
||||
* on the cell class' name.
|
||||
*/
|
||||
protected string $view = '';
|
||||
|
||||
/**
|
||||
* Responsible for converting the view into HTML.
|
||||
* Expected to be overridden by the child class
|
||||
* in many occasions, but not all.
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
if (! function_exists('decamelize')) {
|
||||
helper('inflector');
|
||||
}
|
||||
|
||||
return $this->view($this->view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the view to use when rendered.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setView(string $view)
|
||||
{
|
||||
$this->view = $view;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually renders the view, and returns the HTML.
|
||||
* In order to provide access to public properties and methods
|
||||
* from within the view, this method extracts $data into the
|
||||
* current scope and captures the output buffer instead of
|
||||
* relying on the view service.
|
||||
*
|
||||
* @throws LogicException
|
||||
*/
|
||||
final protected function view(?string $view, array $data = []): string
|
||||
{
|
||||
$properties = $this->getPublicProperties();
|
||||
$properties = $this->includeComputedProperties($properties);
|
||||
$properties = array_merge($properties, $data);
|
||||
|
||||
$view = (string) $view;
|
||||
|
||||
if ($view === '') {
|
||||
$viewName = decamelize(class_basename(static::class));
|
||||
$directory = dirname((new ReflectionClass($this))->getFileName()) . DIRECTORY_SEPARATOR;
|
||||
|
||||
$possibleView1 = $directory . substr($viewName, 0, strrpos($viewName, '_cell')) . '.php';
|
||||
$possibleView2 = $directory . $viewName . '.php';
|
||||
}
|
||||
|
||||
if ($view !== '' && ! is_file($view)) {
|
||||
$directory = dirname((new ReflectionClass($this))->getFileName()) . DIRECTORY_SEPARATOR;
|
||||
|
||||
$view = $directory . $view . '.php';
|
||||
}
|
||||
|
||||
$candidateViews = array_filter(
|
||||
[$view, $possibleView1 ?? '', $possibleView2 ?? ''],
|
||||
static fn (string $path): bool => $path !== '' && is_file($path)
|
||||
);
|
||||
|
||||
if ($candidateViews === []) {
|
||||
throw new LogicException(sprintf(
|
||||
'Cannot locate the view file for the "%s" cell.',
|
||||
static::class
|
||||
));
|
||||
}
|
||||
|
||||
$foundView = current($candidateViews);
|
||||
|
||||
return (function () use ($properties, $foundView): string {
|
||||
extract($properties);
|
||||
ob_start();
|
||||
include $foundView;
|
||||
|
||||
return ob_get_clean();
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides capability to render on string casting.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the developer to define computed properties
|
||||
* as methods with `get` prefixed to the protected/private property name.
|
||||
*/
|
||||
private function includeComputedProperties(array $properties): array
|
||||
{
|
||||
$reservedProperties = ['data', 'view'];
|
||||
$privateProperties = $this->getNonPublicProperties();
|
||||
|
||||
foreach ($privateProperties as $property) {
|
||||
$name = $property->getName();
|
||||
|
||||
// don't include any methods in the base class
|
||||
if (in_array($name, $reservedProperties, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$computedMethod = 'get' . ucfirst($name) . 'Property';
|
||||
|
||||
if (method_exists($this, $computedMethod)) {
|
||||
$properties[$name] = $this->{$computedMethod}();
|
||||
}
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\FrameworkException;
|
||||
|
||||
class ViewException extends FrameworkException
|
||||
{
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidCellMethod(string $class, string $method)
|
||||
{
|
||||
return new static(lang('View.invalidCellMethod', ['class' => $class, 'method' => $method]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forMissingCellParameters(string $class, string $method)
|
||||
{
|
||||
return new static(lang('View.missingCellParameters', ['class' => $class, 'method' => $method]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidCellParameter(string $key)
|
||||
{
|
||||
return new static(lang('View.invalidCellParameter', [$key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forNoCellClass()
|
||||
{
|
||||
return new static(lang('View.noCellClass'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidCellClass(?string $class = null)
|
||||
{
|
||||
return new static(lang('View.invalidCellClass', [$class]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forTagSyntaxError(string $output)
|
||||
{
|
||||
return new static(lang('View.tagSyntaxError', [$output]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidDecorator(string $className)
|
||||
{
|
||||
return new static(lang('View.invalidDecoratorClass', [$className]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
use NumberFormatter;
|
||||
|
||||
/**
|
||||
* View filters
|
||||
*/
|
||||
class Filters
|
||||
{
|
||||
/**
|
||||
* Returns $value as all lowercase with the first letter capitalized.
|
||||
*/
|
||||
public static function capitalize(string $value): string
|
||||
{
|
||||
return ucfirst(strtolower($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date into the given $format.
|
||||
*
|
||||
* @param int|string|null $value
|
||||
*/
|
||||
public static function date($value, string $format): string
|
||||
{
|
||||
if (is_string($value) && ! is_numeric($value)) {
|
||||
$value = strtotime($value);
|
||||
}
|
||||
|
||||
if ($value !== null) {
|
||||
$value = (int) $value;
|
||||
}
|
||||
|
||||
return date($format, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string or DateTime object, will return the date modified
|
||||
* by the given value. Returns the value as a unix timestamp
|
||||
*
|
||||
* Example:
|
||||
* my_date|date_modify(+1 day)
|
||||
*
|
||||
* @param int|string|null $value
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public static function date_modify($value, string $adjustment)
|
||||
{
|
||||
$value = static::date($value, 'Y-m-d H:i:s');
|
||||
|
||||
return strtotime($adjustment, strtotime($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given default value if $value is empty or undefined.
|
||||
*
|
||||
* @param bool|float|int|list<string>|object|resource|string|null $value
|
||||
*/
|
||||
public static function default($value, string $default): string
|
||||
{
|
||||
return empty($value) ? $default : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the given value with our `esc()` helper function.
|
||||
*
|
||||
* @param string $value
|
||||
* @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context
|
||||
*/
|
||||
public static function esc($value, string $context = 'html'): string
|
||||
{
|
||||
return esc($value, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an excerpt of the given string.
|
||||
*/
|
||||
public static function excerpt(string $value, string $phrase, int $radius = 100): string
|
||||
{
|
||||
helper('text');
|
||||
|
||||
return excerpt($value, $phrase, $radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights a given phrase within the text using '<mark></mark>' tags.
|
||||
*/
|
||||
public static function highlight(string $value, string $phrase): string
|
||||
{
|
||||
helper('text');
|
||||
|
||||
return highlight_phrase($value, $phrase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights code samples with HTML/CSS.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public static function highlight_code($value): string
|
||||
{
|
||||
helper('text');
|
||||
|
||||
return highlight_code($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the number of characters to $limit, and trails of with an ellipsis.
|
||||
* Will break at word break so may be more or less than $limit.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public static function limit_chars($value, int $limit = 500): string
|
||||
{
|
||||
helper('text');
|
||||
|
||||
return character_limiter($value, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the number of words to $limit, and trails of with an ellipsis.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public static function limit_words($value, int $limit = 100): string
|
||||
{
|
||||
helper('text');
|
||||
|
||||
return word_limiter($value, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the $value displayed in a localized manner.
|
||||
*
|
||||
* @param float|int $value
|
||||
*/
|
||||
public static function local_number($value, string $type = 'decimal', int $precision = 4, ?string $locale = null): string
|
||||
{
|
||||
helper('number');
|
||||
|
||||
$types = [
|
||||
'decimal' => NumberFormatter::DECIMAL,
|
||||
'currency' => NumberFormatter::CURRENCY,
|
||||
'percent' => NumberFormatter::PERCENT,
|
||||
'scientific' => NumberFormatter::SCIENTIFIC,
|
||||
'spellout' => NumberFormatter::SPELLOUT,
|
||||
'ordinal' => NumberFormatter::ORDINAL,
|
||||
'duration' => NumberFormatter::DURATION,
|
||||
];
|
||||
|
||||
return format_number((float) $value, $precision, $locale, ['type' => $types[$type]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the $value displayed as a currency string.
|
||||
*
|
||||
* @param float|int $value
|
||||
* @param int $fraction
|
||||
*/
|
||||
public static function local_currency($value, string $currency, ?string $locale = null, $fraction = null): string
|
||||
{
|
||||
helper('number');
|
||||
|
||||
$fraction ??= 0;
|
||||
|
||||
$options = [
|
||||
'type' => NumberFormatter::CURRENCY,
|
||||
'currency' => $currency,
|
||||
'fraction' => $fraction,
|
||||
];
|
||||
|
||||
return format_number((float) $value, 2, $locale, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with all instances of newline character (\n)
|
||||
* converted to an HTML <br> tag.
|
||||
*/
|
||||
public static function nl2br(string $value): string
|
||||
{
|
||||
$typography = service('typography');
|
||||
|
||||
return $typography->nl2brExceptPre($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a body of text and uses the auto_typography() method to
|
||||
* turn it into prettier, easier-to-read, prose.
|
||||
*/
|
||||
public static function prose(string $value): string
|
||||
{
|
||||
$typography = service('typography');
|
||||
|
||||
return $typography->autoTypography($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a given $value in one of 3 ways;
|
||||
*
|
||||
* - common Normal rounding
|
||||
* - ceil always rounds up
|
||||
* - floor always rounds down
|
||||
*
|
||||
* @param int|string $precision precision or type
|
||||
*
|
||||
* @return float|string
|
||||
*/
|
||||
public static function round(string $value, $precision = 2, string $type = 'common')
|
||||
{
|
||||
// In case that $precision is a type like `{ value1|round(ceil) }`
|
||||
if (! is_numeric($precision)) {
|
||||
$type = $precision;
|
||||
$precision = 2;
|
||||
} else {
|
||||
$precision = (int) $precision;
|
||||
}
|
||||
|
||||
return match ($type) {
|
||||
'common' => round((float) $value, $precision),
|
||||
'ceil' => ceil((float) $value),
|
||||
'floor' => floor((float) $value),
|
||||
// Still here, just return the value.
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "title case" version of the string.
|
||||
*/
|
||||
public static function title(string $value): string
|
||||
{
|
||||
return ucwords(strtolower($value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,768 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
use CodeIgniter\Autoloader\FileLocatorInterface;
|
||||
use CodeIgniter\View\Exceptions\ViewException;
|
||||
use Config\View as ViewConfig;
|
||||
use ParseError;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class for parsing pseudo-vars
|
||||
*
|
||||
* @phpstan-type parser_callable (callable(mixed): mixed)
|
||||
* @phpstan-type parser_callable_string (callable(mixed): mixed)&string
|
||||
*
|
||||
* @see \CodeIgniter\View\ParserTest
|
||||
*/
|
||||
class Parser extends View
|
||||
{
|
||||
use ViewDecoratorTrait;
|
||||
|
||||
/**
|
||||
* Left delimiter character for pseudo vars
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $leftDelimiter = '{';
|
||||
|
||||
/**
|
||||
* Right delimiter character for pseudo vars
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $rightDelimiter = '}';
|
||||
|
||||
/**
|
||||
* Left delimiter characters for conditionals
|
||||
*/
|
||||
protected string $leftConditionalDelimiter = '{';
|
||||
|
||||
/**
|
||||
* Right delimiter characters for conditionals
|
||||
*/
|
||||
protected string $rightConditionalDelimiter = '}';
|
||||
|
||||
/**
|
||||
* Stores extracted noparse blocks.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $noparseBlocks = [];
|
||||
|
||||
/**
|
||||
* Stores any plugins registered at run-time.
|
||||
*
|
||||
* @var array<string, callable|list<string>|string>
|
||||
* @phpstan-var array<string, array<parser_callable_string>|parser_callable_string|parser_callable>
|
||||
*/
|
||||
protected $plugins = [];
|
||||
|
||||
/**
|
||||
* Stores the context for each data element
|
||||
* when set by `setData` so the context is respected.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $dataContexts = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param FileLocatorInterface|null $loader
|
||||
*/
|
||||
public function __construct(
|
||||
ViewConfig $config,
|
||||
?string $viewPath = null,
|
||||
$loader = null,
|
||||
?bool $debug = null,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
// Ensure user plugins override core plugins.
|
||||
$this->plugins = $config->plugins;
|
||||
|
||||
parent::__construct($config, $viewPath, $loader, $debug, $logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a template
|
||||
*
|
||||
* Parses pseudo-variables contained in the specified template view,
|
||||
* replacing them with any data that has already been set.
|
||||
*
|
||||
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
|
||||
* it might be needed to pass additional info
|
||||
* to other template engines.
|
||||
*/
|
||||
public function render(string $view, ?array $options = null, ?bool $saveData = null): string
|
||||
{
|
||||
$start = microtime(true);
|
||||
if ($saveData === null) {
|
||||
$saveData = $this->config->saveData;
|
||||
}
|
||||
|
||||
$fileExt = pathinfo($view, PATHINFO_EXTENSION);
|
||||
$view = ($fileExt === '') ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3)
|
||||
|
||||
$cacheName = $options['cache_name'] ?? str_replace('.php', '', $view);
|
||||
|
||||
// Was it cached?
|
||||
if (isset($options['cache']) && ($output = cache($cacheName))) {
|
||||
$this->logPerformance($start, microtime(true), $view);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
$file = $this->viewPath . $view;
|
||||
|
||||
if (! is_file($file)) {
|
||||
$fileOrig = $file;
|
||||
$file = $this->loader->locateFile($view, 'Views');
|
||||
|
||||
// locateFile() will return false if the file cannot be found.
|
||||
if ($file === false) {
|
||||
throw ViewException::forInvalidFile($fileOrig);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->tempData === null) {
|
||||
$this->tempData = $this->data;
|
||||
}
|
||||
|
||||
$template = file_get_contents($file);
|
||||
$output = $this->parse($template, $this->tempData, $options);
|
||||
$this->logPerformance($start, microtime(true), $view);
|
||||
|
||||
if ($saveData) {
|
||||
$this->data = $this->tempData;
|
||||
}
|
||||
|
||||
$output = $this->decorateOutput($output);
|
||||
|
||||
// Should we cache?
|
||||
if (isset($options['cache'])) {
|
||||
cache()->save($cacheName, $output, (int) $options['cache']);
|
||||
}
|
||||
$this->tempData = null;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a String
|
||||
*
|
||||
* Parses pseudo-variables contained in the specified string,
|
||||
* replacing them with any data that has already been set.
|
||||
*
|
||||
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
|
||||
* it might be needed to pass additional info
|
||||
* to other template engines.
|
||||
*/
|
||||
public function renderString(string $template, ?array $options = null, ?bool $saveData = null): string
|
||||
{
|
||||
$start = microtime(true);
|
||||
if ($saveData === null) {
|
||||
$saveData = $this->config->saveData;
|
||||
}
|
||||
|
||||
if ($this->tempData === null) {
|
||||
$this->tempData = $this->data;
|
||||
}
|
||||
|
||||
$output = $this->parse($template, $this->tempData, $options);
|
||||
|
||||
$this->logPerformance($start, microtime(true), $this->excerpt($template));
|
||||
|
||||
if ($saveData) {
|
||||
$this->data = $this->tempData;
|
||||
}
|
||||
|
||||
$this->tempData = null;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets several pieces of view data at once.
|
||||
* In the Parser, we need to store the context here
|
||||
* so that the variable is correctly handled within the
|
||||
* parsing itself, and contexts (including raw) are respected.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param non-empty-string|null $context The context to escape it for.
|
||||
* If 'raw', no escaping will happen.
|
||||
* @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context
|
||||
*/
|
||||
public function setData(array $data = [], ?string $context = null): RendererInterface
|
||||
{
|
||||
if ($context !== null && $context !== '') {
|
||||
foreach ($data as $key => &$value) {
|
||||
if (is_array($value)) {
|
||||
foreach ($value as &$obj) {
|
||||
$obj = $this->objectToArray($obj);
|
||||
}
|
||||
} else {
|
||||
$value = $this->objectToArray($value);
|
||||
}
|
||||
|
||||
$this->dataContexts[$key] = $context;
|
||||
}
|
||||
}
|
||||
|
||||
$this->tempData ??= $this->data;
|
||||
$this->tempData = array_merge($this->tempData, $data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a template
|
||||
*
|
||||
* Parses pseudo-variables contained in the specified template,
|
||||
* replacing them with the data in the second param
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $options Future options
|
||||
*/
|
||||
protected function parse(string $template, array $data = [], ?array $options = null): string
|
||||
{
|
||||
if ($template === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove any possible PHP tags since we don't support it
|
||||
// and parseConditionals needs it clean anyway...
|
||||
$template = str_replace(['<?', '?>'], ['<?', '?>'], $template);
|
||||
|
||||
$template = $this->parseComments($template);
|
||||
$template = $this->extractNoparse($template);
|
||||
|
||||
// Replace any conditional code here so we don't have to parse as much
|
||||
$template = $this->parseConditionals($template);
|
||||
|
||||
// Handle any plugins before normal data, so that
|
||||
// it can potentially modify any template between its tags.
|
||||
$template = $this->parsePlugins($template);
|
||||
|
||||
// Parse stack for each parse type (Single and Pairs)
|
||||
$replaceSingleStack = [];
|
||||
$replacePairsStack = [];
|
||||
|
||||
// loop over the data variables, saving regex and data
|
||||
// for later replacement.
|
||||
foreach ($data as $key => $val) {
|
||||
$escape = true;
|
||||
|
||||
if (is_array($val)) {
|
||||
$escape = false;
|
||||
$replacePairsStack[] = [
|
||||
'replace' => $this->parsePair($key, $val, $template),
|
||||
'escape' => $escape,
|
||||
];
|
||||
} else {
|
||||
$replaceSingleStack[] = [
|
||||
'replace' => $this->parseSingle($key, (string) $val),
|
||||
'escape' => $escape,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Merge both stacks, pairs first + single stacks
|
||||
// This allows for nested data with the same key to be replaced properly
|
||||
$replace = array_merge($replacePairsStack, $replaceSingleStack);
|
||||
|
||||
// Loop over each replace array item which
|
||||
// holds all the data to be replaced
|
||||
foreach ($replace as $replaceItem) {
|
||||
// Loop over the actual data to be replaced
|
||||
foreach ($replaceItem['replace'] as $pattern => $content) {
|
||||
$template = $this->replaceSingle($pattern, $content, $template, $replaceItem['escape']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->insertNoparse($template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single key/value, extracting it
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function parseSingle(string $key, string $val): array
|
||||
{
|
||||
$pattern = '#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#')
|
||||
. '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?'
|
||||
. $this->rightDelimiter . '#ums';
|
||||
|
||||
return [$pattern => $val];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a tag pair
|
||||
*
|
||||
* Parses tag pairs: {some_tag} string... {/some_tag}
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function parsePair(string $variable, array $data, string $template): array
|
||||
{
|
||||
// Holds the replacement patterns and contents
|
||||
// that will be used within a preg_replace in parse()
|
||||
$replace = [];
|
||||
|
||||
// Find all matches of space-flexible versions of {tag}{/tag} so we
|
||||
// have something to loop over.
|
||||
preg_match_all(
|
||||
'#' . $this->leftDelimiter . '\s*' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '(.+?)' .
|
||||
$this->leftDelimiter . '\s*/' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '#us',
|
||||
$template,
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
);
|
||||
|
||||
/*
|
||||
* Each match looks like:
|
||||
*
|
||||
* $match[0] {tag}...{/tag}
|
||||
* $match[1] Contents inside the tag
|
||||
*/
|
||||
foreach ($matches as $match) {
|
||||
// Loop over each piece of $data, replacing
|
||||
// its contents so that we know what to replace in parse()
|
||||
$str = ''; // holds the new contents for this tag pair.
|
||||
|
||||
foreach ($data as $row) {
|
||||
// Objects that have a `toArray()` method should be
|
||||
// converted with that method (i.e. Entities)
|
||||
if (is_object($row) && method_exists($row, 'toArray')) {
|
||||
$row = $row->toArray();
|
||||
}
|
||||
// Otherwise, cast as an array and it will grab public properties.
|
||||
elseif (is_object($row)) {
|
||||
$row = (array) $row;
|
||||
}
|
||||
|
||||
$temp = [];
|
||||
$pairs = [];
|
||||
$out = $match[1];
|
||||
|
||||
foreach ($row as $key => $val) {
|
||||
// For nested data, send us back through this method...
|
||||
if (is_array($val)) {
|
||||
$pair = $this->parsePair($key, $val, $match[1]);
|
||||
|
||||
if ($pair !== []) {
|
||||
$pairs[array_keys($pair)[0]] = true;
|
||||
|
||||
$temp = array_merge($temp, $pair);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_object($val)) {
|
||||
$val = 'Class: ' . $val::class;
|
||||
} elseif (is_resource($val)) {
|
||||
$val = 'Resource';
|
||||
}
|
||||
|
||||
$temp['#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#') . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?' . $this->rightDelimiter . '#us'] = $val;
|
||||
}
|
||||
|
||||
// Now replace our placeholders with the new content.
|
||||
foreach ($temp as $pattern => $content) {
|
||||
$out = $this->replaceSingle($pattern, $content, $out, ! isset($pairs[$pattern]));
|
||||
}
|
||||
|
||||
$str .= $out;
|
||||
}
|
||||
|
||||
$escapedMatch = preg_quote($match[0], '#');
|
||||
|
||||
$replace['#' . $escapedMatch . '#us'] = $str;
|
||||
}
|
||||
|
||||
return $replace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any comments from the file. Comments are wrapped in {# #} symbols:
|
||||
*
|
||||
* {# This is a comment #}
|
||||
*/
|
||||
protected function parseComments(string $template): string
|
||||
{
|
||||
return preg_replace('/\{#.*?#\}/us', '', $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts noparse blocks, inserting a hash in its place so that
|
||||
* those blocks of the page are not touched by parsing.
|
||||
*/
|
||||
protected function extractNoparse(string $template): string
|
||||
{
|
||||
$pattern = '/\{\s*noparse\s*\}(.*?)\{\s*\/noparse\s*\}/ums';
|
||||
|
||||
/*
|
||||
* $matches[][0] is the raw match
|
||||
* $matches[][1] is the contents
|
||||
*/
|
||||
if (preg_match_all($pattern, $template, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
// Create a hash of the contents to insert in its place.
|
||||
$hash = md5($match[1]);
|
||||
$this->noparseBlocks[$hash] = $match[1];
|
||||
$template = str_replace($match[0], "noparse_{$hash}", $template);
|
||||
}
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-inserts the noparsed contents back into the template.
|
||||
*/
|
||||
public function insertNoparse(string $template): string
|
||||
{
|
||||
foreach ($this->noparseBlocks as $hash => $replace) {
|
||||
$template = str_replace("noparse_{$hash}", $replace, $template);
|
||||
unset($this->noparseBlocks[$hash]);
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses any conditionals in the code, removing blocks that don't
|
||||
* pass so we don't try to parse it later.
|
||||
*
|
||||
* Valid conditionals:
|
||||
* - if
|
||||
* - elseif
|
||||
* - else
|
||||
*/
|
||||
protected function parseConditionals(string $template): string
|
||||
{
|
||||
$leftDelimiter = preg_quote($this->leftConditionalDelimiter, '/');
|
||||
$rightDelimiter = preg_quote($this->rightConditionalDelimiter, '/');
|
||||
|
||||
$pattern = '/'
|
||||
. $leftDelimiter
|
||||
. '\s*(if|elseif)\s*((?:\()?(.*?)(?:\))?)\s*'
|
||||
. $rightDelimiter
|
||||
. '/ums';
|
||||
|
||||
/*
|
||||
* For each match:
|
||||
* [0] = raw match `{if var}`
|
||||
* [1] = conditional `if`
|
||||
* [2] = condition `do === true`
|
||||
* [3] = same as [2]
|
||||
*/
|
||||
preg_match_all($pattern, $template, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
// Build the string to replace the `if` statement with.
|
||||
$condition = $match[2];
|
||||
|
||||
$statement = $match[1] === 'elseif' ? '<?php elseif (' . $condition . '): ?>' : '<?php if (' . $condition . '): ?>';
|
||||
$template = str_replace($match[0], $statement, $template);
|
||||
}
|
||||
|
||||
$template = preg_replace(
|
||||
'/' . $leftDelimiter . '\s*else\s*' . $rightDelimiter . '/ums',
|
||||
'<?php else: ?>',
|
||||
$template
|
||||
);
|
||||
$template = preg_replace(
|
||||
'/' . $leftDelimiter . '\s*endif\s*' . $rightDelimiter . '/ums',
|
||||
'<?php endif; ?>',
|
||||
$template
|
||||
);
|
||||
|
||||
// Parse the PHP itself, or insert an error so they can debug
|
||||
ob_start();
|
||||
|
||||
if ($this->tempData === null) {
|
||||
$this->tempData = $this->data;
|
||||
}
|
||||
|
||||
extract($this->tempData);
|
||||
|
||||
try {
|
||||
eval('?>' . $template . '<?php ');
|
||||
} catch (ParseError) {
|
||||
ob_end_clean();
|
||||
|
||||
throw ViewException::forTagSyntaxError(str_replace(['?>', '<?php '], '', $template));
|
||||
}
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Over-ride the substitution field delimiters.
|
||||
*
|
||||
* @param string $leftDelimiter
|
||||
* @param string $rightDelimiter
|
||||
*/
|
||||
public function setDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): RendererInterface
|
||||
{
|
||||
$this->leftDelimiter = $leftDelimiter;
|
||||
$this->rightDelimiter = $rightDelimiter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Over-ride the substitution conditional delimiters.
|
||||
*
|
||||
* @param string $leftDelimiter
|
||||
* @param string $rightDelimiter
|
||||
*/
|
||||
public function setConditionalDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): RendererInterface
|
||||
{
|
||||
$this->leftConditionalDelimiter = $leftDelimiter;
|
||||
$this->rightConditionalDelimiter = $rightDelimiter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles replacing a pseudo-variable with the actual content. Will double-check
|
||||
* for escaping brackets.
|
||||
*
|
||||
* @param array|string $pattern
|
||||
* @param string $content
|
||||
* @param string $template
|
||||
*/
|
||||
protected function replaceSingle($pattern, $content, $template, bool $escape = false): string
|
||||
{
|
||||
$content = (string) $content;
|
||||
|
||||
// Replace the content in the template
|
||||
return preg_replace_callback($pattern, function ($matches) use ($content, $escape): string {
|
||||
// Check for {! !} syntax to not escape this one.
|
||||
if (
|
||||
str_starts_with($matches[0], $this->leftDelimiter . '!')
|
||||
&& substr($matches[0], -1 - strlen($this->rightDelimiter)) === '!' . $this->rightDelimiter
|
||||
) {
|
||||
$escape = false;
|
||||
}
|
||||
|
||||
return $this->prepareReplacement($matches, $content, $escape);
|
||||
}, (string) $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used during parse() to apply any filters to the value.
|
||||
*
|
||||
* @param list<string> $matches
|
||||
*/
|
||||
protected function prepareReplacement(array $matches, string $replace, bool $escape = true): string
|
||||
{
|
||||
$orig = array_shift($matches);
|
||||
|
||||
// Our regex earlier will leave all chained values on a single line
|
||||
// so we need to break them apart so we can apply them all.
|
||||
$filters = (isset($matches[1]) && $matches[1] !== '') ? explode('|', $matches[1]) : [];
|
||||
|
||||
if ($escape && $filters === [] && ($context = $this->shouldAddEscaping($orig))) {
|
||||
$filters[] = "esc({$context})";
|
||||
}
|
||||
|
||||
return $this->applyFilters($replace, $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the placeholder the view provided to see if we need to provide any autoescaping.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function shouldAddEscaping(string $key)
|
||||
{
|
||||
$escape = false;
|
||||
|
||||
$key = trim(str_replace(['{', '}'], '', $key));
|
||||
|
||||
// If the key has a context stored (from setData)
|
||||
// we need to respect that.
|
||||
if (array_key_exists($key, $this->dataContexts)) {
|
||||
if ($this->dataContexts[$key] !== 'raw') {
|
||||
return $this->dataContexts[$key];
|
||||
}
|
||||
}
|
||||
// No pipes, then we know we need to escape
|
||||
elseif (! str_contains($key, '|')) {
|
||||
$escape = 'html';
|
||||
}
|
||||
// If there's a `noescape` then we're definitely false.
|
||||
elseif (str_contains($key, 'noescape')) {
|
||||
$escape = false;
|
||||
}
|
||||
// If no `esc` filter is found, then we'll need to add one.
|
||||
elseif (preg_match('/\s+esc/u', $key) !== 1) {
|
||||
$escape = 'html';
|
||||
}
|
||||
|
||||
return $escape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of filters, will apply each of the filters in turn
|
||||
* to $replace, and return the modified string.
|
||||
*
|
||||
* @param list<string> $filters
|
||||
*/
|
||||
protected function applyFilters(string $replace, array $filters): string
|
||||
{
|
||||
// Determine the requested filters
|
||||
foreach ($filters as $filter) {
|
||||
// Grab any parameter we might need to send
|
||||
preg_match('/\([\w<>=\/\\\,:.\-\s\+]+\)/u', $filter, $param);
|
||||
|
||||
// Remove the () and spaces to we have just the parameter left
|
||||
$param = ($param !== []) ? trim($param[0], '() ') : null;
|
||||
|
||||
// Params can be separated by commas to allow multiple parameters for the filter
|
||||
if ($param !== null && $param !== '') {
|
||||
$param = explode(',', $param);
|
||||
|
||||
// Clean it up
|
||||
foreach ($param as &$p) {
|
||||
$p = trim($p, ' "');
|
||||
}
|
||||
} else {
|
||||
$param = [];
|
||||
}
|
||||
|
||||
// Get our filter name
|
||||
$filter = $param !== [] ? trim(strtolower(substr($filter, 0, strpos($filter, '(')))) : trim($filter);
|
||||
|
||||
if (! array_key_exists($filter, $this->config->filters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter it....
|
||||
// We can't know correct param types, so can't set `declare(strict_types=1)`.
|
||||
$replace = $this->config->filters[$filter]($replace, ...$param);
|
||||
}
|
||||
|
||||
return (string) $replace;
|
||||
}
|
||||
|
||||
// Plugins
|
||||
|
||||
/**
|
||||
* Scans the template for any parser plugins, and attempts to execute them.
|
||||
* Plugins are delimited by {+ ... +}
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function parsePlugins(string $template)
|
||||
{
|
||||
foreach ($this->plugins as $plugin => $callable) {
|
||||
// Paired tags are enclosed in an array in the config array.
|
||||
$isPair = is_array($callable);
|
||||
$callable = $isPair ? array_shift($callable) : $callable;
|
||||
|
||||
// See https://regex101.com/r/BCBBKB/1
|
||||
$pattern = $isPair
|
||||
? '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}(.+?)\{\+\s*/' . $plugin . '\s*\+\}#uims'
|
||||
: '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}#uims';
|
||||
|
||||
/**
|
||||
* Match tag pairs
|
||||
*
|
||||
* Each match is an array:
|
||||
* $matches[0] = entire matched string
|
||||
* $matches[1] = all parameters string in opening tag
|
||||
* $matches[2] = content between the tags to send to the plugin.
|
||||
*/
|
||||
if (preg_match_all($pattern, $template, $matches, PREG_SET_ORDER) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$params = [];
|
||||
|
||||
preg_match_all('/([\w-]+=\"[^"]+\")|([\w-]+=[^\"\s=]+)|(\"[^"]+\")|(\S+)/u', trim($match[1]), $matchesParams);
|
||||
|
||||
foreach ($matchesParams[0] as $item) {
|
||||
$keyVal = explode('=', $item);
|
||||
|
||||
if (count($keyVal) === 2) {
|
||||
$params[$keyVal[0]] = str_replace('"', '', $keyVal[1]);
|
||||
} else {
|
||||
$params[] = str_replace('"', '', $item);
|
||||
}
|
||||
}
|
||||
|
||||
$template = $isPair
|
||||
? str_replace($match[0], $callable($match[2], $params), $template)
|
||||
: str_replace($match[0], $callable($params), $template);
|
||||
}
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a new plugin available during the parsing of the template.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addPlugin(string $alias, callable $callback, bool $isPair = false)
|
||||
{
|
||||
$this->plugins[$alias] = $isPair ? [$callback] : $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a plugin from the available plugins.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removePlugin(string $alias)
|
||||
{
|
||||
unset($this->plugins[$alias]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object to an array, respecting any
|
||||
* toArray() methods on an object.
|
||||
*
|
||||
* @param array<string, mixed>|bool|float|int|object|string|null $value
|
||||
*
|
||||
* @return array<string, mixed>|bool|float|int|string|null
|
||||
*/
|
||||
protected function objectToArray($value)
|
||||
{
|
||||
// Objects that have a `toArray()` method should be
|
||||
// converted with that method (i.e. Entities)
|
||||
if (is_object($value) && method_exists($value, 'toArray')) {
|
||||
$value = $value->toArray();
|
||||
}
|
||||
// Otherwise, cast as an array and it will grab public properties.
|
||||
elseif (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
use CodeIgniter\HTTP\URI;
|
||||
|
||||
/**
|
||||
* View plugins
|
||||
*/
|
||||
class Plugins
|
||||
{
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @return string|URI
|
||||
*/
|
||||
public static function currentURL()
|
||||
{
|
||||
return current_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @return string|URI
|
||||
*/
|
||||
public static function previousURL()
|
||||
{
|
||||
return previous_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @param array{email?: string, title?: string, attributes?: array<string, string>|object|string} $params
|
||||
*/
|
||||
public static function mailto(array $params = []): string
|
||||
{
|
||||
$email = $params['email'] ?? '';
|
||||
$title = $params['title'] ?? '';
|
||||
$attrs = $params['attributes'] ?? '';
|
||||
|
||||
return mailto($email, $title, $attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @param array{email?: string, title?: string, attributes?: array<string, string>|object|string} $params
|
||||
*/
|
||||
public static function safeMailto(array $params = []): string
|
||||
{
|
||||
$email = $params['email'] ?? '';
|
||||
$title = $params['title'] ?? '';
|
||||
$attrs = $params['attributes'] ?? '';
|
||||
|
||||
return safe_mailto($email, $title, $attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @param array<int|string, string>|list<string> $params
|
||||
*/
|
||||
public static function lang(array $params = []): string
|
||||
{
|
||||
$line = array_shift($params);
|
||||
|
||||
return lang($line, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @param array{field?: string} $params
|
||||
*/
|
||||
public static function validationErrors(array $params = []): string
|
||||
{
|
||||
$validator = service('validation');
|
||||
if ($params === []) {
|
||||
return $validator->listErrors();
|
||||
}
|
||||
|
||||
return $validator->showError($params['field']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @param list<string> $params
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public static function route(array $params = [])
|
||||
{
|
||||
return route_to(...$params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap helper function to use as view plugin.
|
||||
*
|
||||
* @param list<string> $params
|
||||
*/
|
||||
public static function siteURL(array $params = []): string
|
||||
{
|
||||
return site_url(...$params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap csp_script_nonce() function to use as view plugin.
|
||||
*/
|
||||
public static function cspScriptNonce(): string
|
||||
{
|
||||
return csp_script_nonce();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap csp_style_nonce() function to use as view plugin.
|
||||
*/
|
||||
public static function cspStyleNonce(): string
|
||||
{
|
||||
return csp_style_nonce();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
/**
|
||||
* Interface RendererInterface
|
||||
*
|
||||
* The interface used for displaying Views and/or theme files.
|
||||
*/
|
||||
interface RendererInterface
|
||||
{
|
||||
/**
|
||||
* Builds the output based upon a file name and any
|
||||
* data that has already been set.
|
||||
*
|
||||
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
|
||||
* it might be needed to pass additional info
|
||||
* to other template engines.
|
||||
* @param bool $saveData Whether to save data for subsequent calls
|
||||
*/
|
||||
public function render(string $view, ?array $options = null, bool $saveData = false): string;
|
||||
|
||||
/**
|
||||
* Builds the output based upon a string and any
|
||||
* data that has already been set.
|
||||
*
|
||||
* @param string $view The view contents
|
||||
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
|
||||
* it might be needed to pass additional info
|
||||
* to other template engines.
|
||||
* @param bool $saveData Whether to save data for subsequent calls
|
||||
*/
|
||||
public function renderString(string $view, ?array $options = null, bool $saveData = false): string;
|
||||
|
||||
/**
|
||||
* Sets several pieces of view data at once.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param non-empty-string|null $context The context to escape it for.
|
||||
* If 'raw', no escaping will happen.
|
||||
* @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context
|
||||
*
|
||||
* @return RendererInterface
|
||||
*/
|
||||
public function setData(array $data = [], ?string $context = null);
|
||||
|
||||
/**
|
||||
* Sets a single piece of view data.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param non-empty-string|null $context The context to escape it for.
|
||||
* If 'raw', no escaping will happen.
|
||||
* @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context
|
||||
*
|
||||
* @return RendererInterface
|
||||
*/
|
||||
public function setVar(string $name, $value = null, ?string $context = null);
|
||||
|
||||
/**
|
||||
* Removes all of the view data from the system.
|
||||
*
|
||||
* @return RendererInterface
|
||||
*/
|
||||
public function resetData();
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
|
||||
/**
|
||||
* HTML Table Generating Class
|
||||
*
|
||||
* Lets you create tables manually or from database result objects, or arrays.
|
||||
*
|
||||
* @see \CodeIgniter\View\TableTest
|
||||
*/
|
||||
class Table
|
||||
{
|
||||
/**
|
||||
* Data for table rows
|
||||
*
|
||||
* @var list<array<string, string>>|list<list<array<string, string>>>
|
||||
*/
|
||||
public $rows = [];
|
||||
|
||||
/**
|
||||
* Data for table heading
|
||||
*
|
||||
* @var array<int, mixed>
|
||||
*/
|
||||
public $heading = [];
|
||||
|
||||
/**
|
||||
* Data for table footing
|
||||
*
|
||||
* @var array<int, mixed>
|
||||
*/
|
||||
public $footing = [];
|
||||
|
||||
/**
|
||||
* Whether or not to automatically create the table header
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $autoHeading = true;
|
||||
|
||||
/**
|
||||
* Table caption
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $caption;
|
||||
|
||||
/**
|
||||
* Table layout template
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $template;
|
||||
|
||||
/**
|
||||
* Newline setting
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $newline = "\n";
|
||||
|
||||
/**
|
||||
* Contents of empty cells
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $emptyCells = '';
|
||||
|
||||
/**
|
||||
* Callback for custom table layout
|
||||
*
|
||||
* @var callable|null
|
||||
*/
|
||||
public $function;
|
||||
|
||||
/**
|
||||
* Order each inserted row by heading keys
|
||||
*/
|
||||
private bool $syncRowsWithHeading = false;
|
||||
|
||||
/**
|
||||
* Set the template from the table config file if it exists
|
||||
*
|
||||
* @param array<string, string> $config (default: array())
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
// initialize config
|
||||
foreach ($config as $key => $val) {
|
||||
$this->template[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the template
|
||||
*
|
||||
* @param array<string, string> $template
|
||||
* @phpstan-param array<string, string>|string $template
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
{
|
||||
if (! is_array($template)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->template = $template;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table heading
|
||||
*
|
||||
* Can be passed as an array or discreet params
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function setHeading()
|
||||
{
|
||||
$this->heading = $this->_prepArgs(func_get_args());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table footing
|
||||
*
|
||||
* Can be passed as an array or discreet params
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function setFooting()
|
||||
{
|
||||
$this->footing = $this->_prepArgs(func_get_args());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set columns. Takes a one-dimensional array as input and creates
|
||||
* a multi-dimensional array with a depth equal to the number of
|
||||
* columns. This allows a single array with many elements to be
|
||||
* displayed in a table that has a fixed column count.
|
||||
*
|
||||
* @param list<string> $array
|
||||
* @param int $columnLimit
|
||||
*
|
||||
* @return array<int, mixed>|false
|
||||
*/
|
||||
public function makeColumns($array = [], $columnLimit = 0)
|
||||
{
|
||||
if (! is_array($array) || $array === [] || ! is_int($columnLimit)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Turn off the auto-heading feature since it's doubtful we
|
||||
// will want headings from a one-dimensional array
|
||||
$this->autoHeading = false;
|
||||
$this->syncRowsWithHeading = false;
|
||||
|
||||
if ($columnLimit === 0) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
$new = [];
|
||||
|
||||
do {
|
||||
$temp = array_splice($array, 0, $columnLimit);
|
||||
|
||||
if (count($temp) < $columnLimit) {
|
||||
for ($i = count($temp); $i < $columnLimit; $i++) {
|
||||
$temp[] = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
$new[] = $temp;
|
||||
} while ($array !== []);
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set "empty" cells
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function setEmpty($value)
|
||||
{
|
||||
$this->emptyCells = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a table row
|
||||
*
|
||||
* Can be passed as an array or discreet params
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function addRow()
|
||||
{
|
||||
$tmpRow = $this->_prepArgs(func_get_args());
|
||||
|
||||
if ($this->syncRowsWithHeading && $this->heading !== []) {
|
||||
// each key has an index
|
||||
$keyIndex = array_flip(array_keys($this->heading));
|
||||
|
||||
// figure out which keys need to be added
|
||||
$missingKeys = array_diff_key($keyIndex, $tmpRow);
|
||||
|
||||
// Remove all keys which don't exist in $keyIndex
|
||||
$tmpRow = array_filter($tmpRow, static fn ($k): bool => array_key_exists($k, $keyIndex), ARRAY_FILTER_USE_KEY);
|
||||
|
||||
// add missing keys to row, but use $this->emptyCells
|
||||
$tmpRow = array_merge($tmpRow, array_map(fn ($v): array => ['data' => $this->emptyCells], $missingKeys));
|
||||
|
||||
// order keys by $keyIndex values
|
||||
uksort($tmpRow, static fn ($k1, $k2): int => $keyIndex[$k1] <=> $keyIndex[$k2]);
|
||||
}
|
||||
$this->rows[] = $tmpRow;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if each row column should be synced by keys defined in heading.
|
||||
*
|
||||
* If a row has a key which does not exist in heading, it will be filtered out
|
||||
* If a row does not have a key which exists in heading, the field will stay empty
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSyncRowsWithHeading(bool $orderByKey)
|
||||
{
|
||||
$this->syncRowsWithHeading = $orderByKey;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prep Args
|
||||
*
|
||||
* Ensures a standard associative array format for all cell data
|
||||
*
|
||||
* @param array<int, mixed> $args
|
||||
*
|
||||
* @return array<string, array<string, mixed>>|list<array<string, mixed>>
|
||||
*/
|
||||
protected function _prepArgs(array $args)
|
||||
{
|
||||
// If there is no $args[0], skip this and treat as an associative array
|
||||
// This can happen if there is only a single key, for example this is passed to table->generate
|
||||
// array(array('foo'=>'bar'))
|
||||
if (isset($args[0]) && count($args) === 1 && is_array($args[0])) {
|
||||
$args = $args[0];
|
||||
}
|
||||
|
||||
foreach ($args as $key => $val) {
|
||||
if (! is_array($val)) {
|
||||
$args[$key] = ['data' => $val];
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a table caption
|
||||
*
|
||||
* @param string $caption
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function setCaption($caption)
|
||||
{
|
||||
$this->caption = $caption;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the table
|
||||
*
|
||||
* @param array<int, mixed>|BaseResult|null $tableData
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate($tableData = null)
|
||||
{
|
||||
// The table data can optionally be passed to this function
|
||||
// either as a database result object or an array
|
||||
if ($tableData !== null && $tableData !== []) {
|
||||
if ($tableData instanceof BaseResult) {
|
||||
$this->_setFromDBResult($tableData);
|
||||
} elseif (is_array($tableData)) {
|
||||
$this->_setFromArray($tableData);
|
||||
}
|
||||
}
|
||||
|
||||
// Is there anything to display? No? Smite them!
|
||||
if ($this->heading === [] && $this->rows === []) {
|
||||
return 'Undefined table data';
|
||||
}
|
||||
|
||||
// Compile and validate the template date
|
||||
$this->_compileTemplate();
|
||||
|
||||
// Validate a possibly existing custom cell manipulation function
|
||||
if (isset($this->function) && ! is_callable($this->function)) {
|
||||
$this->function = null;
|
||||
}
|
||||
|
||||
// Build the table!
|
||||
$out = $this->template['table_open'] . $this->newline;
|
||||
|
||||
// Add any caption here
|
||||
if (isset($this->caption) && $this->caption !== '') {
|
||||
$out .= '<caption>' . $this->caption . '</caption>' . $this->newline;
|
||||
}
|
||||
|
||||
// Is there a table heading to display?
|
||||
if ($this->heading !== []) {
|
||||
$headerTag = null;
|
||||
|
||||
if (preg_match('/(<)(td|th)(?=\h|>)/i', $this->template['heading_cell_start'], $matches) === 1) {
|
||||
$headerTag = $matches[0];
|
||||
}
|
||||
|
||||
$out .= $this->template['thead_open'] . $this->newline . $this->template['heading_row_start'] . $this->newline;
|
||||
|
||||
foreach ($this->heading as $heading) {
|
||||
$temp = $this->template['heading_cell_start'];
|
||||
|
||||
foreach ($heading as $key => $val) {
|
||||
if ($key !== 'data' && $headerTag !== null) {
|
||||
$temp = str_replace($headerTag, $headerTag . ' ' . $key . '="' . $val . '"', $temp);
|
||||
}
|
||||
}
|
||||
|
||||
$out .= $temp . ($heading['data'] ?? '') . $this->template['heading_cell_end'];
|
||||
}
|
||||
|
||||
$out .= $this->template['heading_row_end'] . $this->newline . $this->template['thead_close'] . $this->newline;
|
||||
}
|
||||
|
||||
// Build the table rows
|
||||
if ($this->rows !== []) {
|
||||
$out .= $this->template['tbody_open'] . $this->newline;
|
||||
|
||||
$i = 1;
|
||||
|
||||
foreach ($this->rows as $row) {
|
||||
// We use modulus to alternate the row colors
|
||||
$name = fmod($i++, 2) !== 0.0 ? '' : 'alt_';
|
||||
|
||||
$out .= $this->template['row_' . $name . 'start'] . $this->newline;
|
||||
|
||||
foreach ($row as $cell) {
|
||||
$temp = $this->template['cell_' . $name . 'start'];
|
||||
|
||||
foreach ($cell as $key => $val) {
|
||||
if ($key !== 'data') {
|
||||
$temp = str_replace('<td', '<td ' . $key . '="' . $val . '"', $temp);
|
||||
}
|
||||
}
|
||||
|
||||
$cell = $cell['data'] ?? '';
|
||||
$out .= $temp;
|
||||
|
||||
if ($cell === '') {
|
||||
$out .= $this->emptyCells;
|
||||
} elseif (isset($this->function)) {
|
||||
$out .= ($this->function)($cell);
|
||||
} else {
|
||||
$out .= $cell;
|
||||
}
|
||||
|
||||
$out .= $this->template['cell_' . $name . 'end'];
|
||||
}
|
||||
|
||||
$out .= $this->template['row_' . $name . 'end'] . $this->newline;
|
||||
}
|
||||
|
||||
$out .= $this->template['tbody_close'] . $this->newline;
|
||||
}
|
||||
|
||||
// Any table footing to display?
|
||||
if ($this->footing !== []) {
|
||||
$footerTag = null;
|
||||
|
||||
if (preg_match('/(<)(td|th)(?=\h|>)/i', $this->template['footing_cell_start'], $matches)) {
|
||||
$footerTag = $matches[0];
|
||||
}
|
||||
|
||||
$out .= $this->template['tfoot_open'] . $this->newline . $this->template['footing_row_start'] . $this->newline;
|
||||
|
||||
foreach ($this->footing as $footing) {
|
||||
$temp = $this->template['footing_cell_start'];
|
||||
|
||||
foreach ($footing as $key => $val) {
|
||||
if ($key !== 'data' && $footerTag !== null) {
|
||||
$temp = str_replace($footerTag, $footerTag . ' ' . $key . '="' . $val . '"', $temp);
|
||||
}
|
||||
}
|
||||
|
||||
$out .= $temp . ($footing['data'] ?? '') . $this->template['footing_cell_end'];
|
||||
}
|
||||
|
||||
$out .= $this->template['footing_row_end'] . $this->newline . $this->template['tfoot_close'] . $this->newline;
|
||||
}
|
||||
|
||||
// And finally, close off the table
|
||||
$out .= $this->template['table_close'];
|
||||
|
||||
// Clear table class properties before generating the table
|
||||
$this->clear();
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the table arrays. Useful if multiple tables are being generated
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
$this->rows = [];
|
||||
$this->heading = [];
|
||||
$this->footing = [];
|
||||
$this->autoHeading = true;
|
||||
$this->caption = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set table data from a database result object
|
||||
*
|
||||
* @param BaseResult $object Database result object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _setFromDBResult($object)
|
||||
{
|
||||
// First generate the headings from the table column names
|
||||
if ($this->autoHeading && $this->heading === []) {
|
||||
$this->heading = $this->_prepArgs($object->getFieldNames());
|
||||
}
|
||||
|
||||
foreach ($object->getResultArray() as $row) {
|
||||
$this->rows[] = $this->_prepArgs($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set table data from an array
|
||||
*
|
||||
* @param array<int, mixed> $data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _setFromArray($data)
|
||||
{
|
||||
if ($this->autoHeading && $this->heading === []) {
|
||||
$this->heading = $this->_prepArgs(array_shift($data));
|
||||
}
|
||||
|
||||
foreach ($data as &$row) {
|
||||
$this->addRow($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile Template
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _compileTemplate()
|
||||
{
|
||||
if ($this->template === null) {
|
||||
$this->template = $this->_defaultTemplate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->_defaultTemplate() as $field => $template) {
|
||||
if (! isset($this->template[$field])) {
|
||||
$this->template[$field] = $template;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Template
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function _defaultTemplate()
|
||||
{
|
||||
return [
|
||||
'table_open' => '<table border="0" cellpadding="4" cellspacing="0">',
|
||||
'thead_open' => '<thead>',
|
||||
'thead_close' => '</thead>',
|
||||
'heading_row_start' => '<tr>',
|
||||
'heading_row_end' => '</tr>',
|
||||
'heading_cell_start' => '<th>',
|
||||
'heading_cell_end' => '</th>',
|
||||
'tfoot_open' => '<tfoot>',
|
||||
'tfoot_close' => '</tfoot>',
|
||||
'footing_row_start' => '<tr>',
|
||||
'footing_row_end' => '</tr>',
|
||||
'footing_cell_start' => '<td>',
|
||||
'footing_cell_end' => '</td>',
|
||||
'tbody_open' => '<tbody>',
|
||||
'tbody_close' => '</tbody>',
|
||||
'row_start' => '<tr>',
|
||||
'row_end' => '</tr>',
|
||||
'cell_start' => '<td>',
|
||||
'cell_end' => '</td>',
|
||||
'row_alt_start' => '<tr>',
|
||||
'row_alt_end' => '</tr>',
|
||||
'cell_alt_start' => '<td>',
|
||||
'cell_alt_end' => '</td>',
|
||||
'table_close' => '</table>',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
use CodeIgniter\Autoloader\FileLocatorInterface;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Views;
|
||||
use CodeIgniter\Filters\DebugToolbar;
|
||||
use CodeIgniter\View\Exceptions\ViewException;
|
||||
use Config\Toolbar;
|
||||
use Config\View as ViewConfig;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class View
|
||||
*
|
||||
* @see \CodeIgniter\View\ViewTest
|
||||
*/
|
||||
class View implements RendererInterface
|
||||
{
|
||||
use ViewDecoratorTrait;
|
||||
|
||||
/**
|
||||
* Saved Data.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Data for the variables that are available in the Views.
|
||||
*
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
protected $tempData;
|
||||
|
||||
/**
|
||||
* The base directory to look in for our Views.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $viewPath;
|
||||
|
||||
/**
|
||||
* Data for rendering including Caching and Debug Toolbar data.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $renderVars = [];
|
||||
|
||||
/**
|
||||
* Instance of FileLocator for when
|
||||
* we need to attempt to find a view
|
||||
* that's not in standard place.
|
||||
*
|
||||
* @var FileLocatorInterface
|
||||
*/
|
||||
protected $loader;
|
||||
|
||||
/**
|
||||
* Logger instance.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Should we store performance info?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $debug = false;
|
||||
|
||||
/**
|
||||
* Cache stats about our performance here,
|
||||
* when CI_DEBUG = true
|
||||
*
|
||||
* @var list<array{start: float, end: float, view: string}>
|
||||
*/
|
||||
protected $performanceData = [];
|
||||
|
||||
/**
|
||||
* @var ViewConfig
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Whether data should be saved between renders.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $saveData;
|
||||
|
||||
/**
|
||||
* Number of loaded views
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $viewsCount = 0;
|
||||
|
||||
/**
|
||||
* The name of the layout being used, if any.
|
||||
* Set by the `extend` method used within views.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $layout;
|
||||
|
||||
/**
|
||||
* Holds the sections and their data.
|
||||
*
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
protected $sections = [];
|
||||
|
||||
/**
|
||||
* The name of the current section being rendered,
|
||||
* if any.
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected $currentSection;
|
||||
|
||||
/**
|
||||
* The name of the current section being rendered,
|
||||
* if any.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $sectionStack = [];
|
||||
|
||||
public function __construct(
|
||||
ViewConfig $config,
|
||||
?string $viewPath = null,
|
||||
?FileLocatorInterface $loader = null,
|
||||
?bool $debug = null,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->viewPath = rtrim($viewPath, '\\/ ') . DIRECTORY_SEPARATOR;
|
||||
$this->loader = $loader ?? service('locator');
|
||||
$this->logger = $logger ?? service('logger');
|
||||
$this->debug = $debug ?? CI_DEBUG;
|
||||
$this->saveData = (bool) $config->saveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the output based upon a file name and any
|
||||
* data that has already been set.
|
||||
*
|
||||
* Valid $options:
|
||||
* - cache Number of seconds to cache for
|
||||
* - cache_name Name to use for cache
|
||||
*
|
||||
* @param string $view File name of the view source
|
||||
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
|
||||
* it might be needed to pass additional info
|
||||
* to other template engines.
|
||||
* @param bool|null $saveData If true, saves data for subsequent calls,
|
||||
* if false, cleans the data after displaying,
|
||||
* if null, uses the config setting.
|
||||
*/
|
||||
public function render(string $view, ?array $options = null, ?bool $saveData = null): string
|
||||
{
|
||||
$this->renderVars['start'] = microtime(true);
|
||||
|
||||
// Store the results here so even if
|
||||
// multiple views are called in a view, it won't
|
||||
// clean it unless we mean it to.
|
||||
$saveData ??= $this->saveData;
|
||||
|
||||
$fileExt = pathinfo($view, PATHINFO_EXTENSION);
|
||||
// allow Views as .html, .tpl, etc (from CI3)
|
||||
$this->renderVars['view'] = ($fileExt === '') ? $view . '.php' : $view;
|
||||
|
||||
$this->renderVars['options'] = $options ?? [];
|
||||
|
||||
// Was it cached?
|
||||
if (isset($this->renderVars['options']['cache'])) {
|
||||
$cacheName = $this->renderVars['options']['cache_name']
|
||||
?? str_replace('.php', '', $this->renderVars['view']);
|
||||
$cacheName = str_replace(['\\', '/'], '', $cacheName);
|
||||
|
||||
$this->renderVars['cacheName'] = $cacheName;
|
||||
|
||||
if ($output = cache($this->renderVars['cacheName'])) {
|
||||
$this->logPerformance(
|
||||
$this->renderVars['start'],
|
||||
microtime(true),
|
||||
$this->renderVars['view']
|
||||
);
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
$this->renderVars['file'] = $this->viewPath . $this->renderVars['view'];
|
||||
|
||||
if (! is_file($this->renderVars['file'])) {
|
||||
$this->renderVars['file'] = $this->loader->locateFile(
|
||||
$this->renderVars['view'],
|
||||
'Views',
|
||||
($fileExt === '') ? 'php' : $fileExt
|
||||
);
|
||||
}
|
||||
|
||||
// locateFile() will return false if the file cannot be found.
|
||||
if ($this->renderVars['file'] === false) {
|
||||
throw ViewException::forInvalidFile($this->renderVars['view']);
|
||||
}
|
||||
|
||||
// Make our view data available to the view.
|
||||
$this->prepareTemplateData($saveData);
|
||||
|
||||
// Save current vars
|
||||
$renderVars = $this->renderVars;
|
||||
|
||||
$output = (function (): string {
|
||||
extract($this->tempData);
|
||||
ob_start();
|
||||
include $this->renderVars['file'];
|
||||
|
||||
return ob_get_clean() ?: '';
|
||||
})();
|
||||
|
||||
// Get back current vars
|
||||
$this->renderVars = $renderVars;
|
||||
|
||||
// When using layouts, the data has already been stored
|
||||
// in $this->sections, and no other valid output
|
||||
// is allowed in $output so we'll overwrite it.
|
||||
if ($this->layout !== null && $this->sectionStack === []) {
|
||||
$layoutView = $this->layout;
|
||||
$this->layout = null;
|
||||
// Save current vars
|
||||
$renderVars = $this->renderVars;
|
||||
$output = $this->render($layoutView, $options, $saveData);
|
||||
// Get back current vars
|
||||
$this->renderVars = $renderVars;
|
||||
}
|
||||
|
||||
$output = $this->decorateOutput($output);
|
||||
|
||||
$this->logPerformance(
|
||||
$this->renderVars['start'],
|
||||
microtime(true),
|
||||
$this->renderVars['view']
|
||||
);
|
||||
|
||||
// Check if DebugToolbar is enabled.
|
||||
$filters = service('filters');
|
||||
$requiredAfterFilters = $filters->getRequiredFilters('after')[0];
|
||||
if (in_array('toolbar', $requiredAfterFilters, true)) {
|
||||
$debugBarEnabled = true;
|
||||
} else {
|
||||
$afterFilters = $filters->getFiltersClass()['after'];
|
||||
$debugBarEnabled = in_array(DebugToolbar::class, $afterFilters, true);
|
||||
}
|
||||
|
||||
if (
|
||||
$this->debug && $debugBarEnabled
|
||||
&& (! isset($options['debug']) || $options['debug'] === true)
|
||||
) {
|
||||
$toolbarCollectors = config(Toolbar::class)->collectors;
|
||||
|
||||
if (in_array(Views::class, $toolbarCollectors, true)) {
|
||||
// Clean up our path names to make them a little cleaner
|
||||
$this->renderVars['file'] = clean_path($this->renderVars['file']);
|
||||
$this->renderVars['file'] = ++$this->viewsCount . ' ' . $this->renderVars['file'];
|
||||
|
||||
$output = '<!-- DEBUG-VIEW START ' . $this->renderVars['file'] . ' -->' . PHP_EOL
|
||||
. $output . PHP_EOL
|
||||
. '<!-- DEBUG-VIEW ENDED ' . $this->renderVars['file'] . ' -->' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
// Should we cache?
|
||||
if (isset($this->renderVars['options']['cache'])) {
|
||||
cache()->save(
|
||||
$this->renderVars['cacheName'],
|
||||
$output,
|
||||
(int) $this->renderVars['options']['cache']
|
||||
);
|
||||
}
|
||||
|
||||
$this->tempData = null;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the output based upon a string and any
|
||||
* data that has already been set.
|
||||
* Cache does not apply, because there is no "key".
|
||||
*
|
||||
* @param string $view The view contents
|
||||
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
|
||||
* it might be needed to pass additional info
|
||||
* to other template engines.
|
||||
* @param bool|null $saveData If true, saves data for subsequent calls,
|
||||
* if false, cleans the data after displaying,
|
||||
* if null, uses the config setting.
|
||||
*/
|
||||
public function renderString(string $view, ?array $options = null, ?bool $saveData = null): string
|
||||
{
|
||||
$start = microtime(true);
|
||||
$saveData ??= $this->saveData;
|
||||
$this->prepareTemplateData($saveData);
|
||||
|
||||
$output = (function (string $view): string {
|
||||
extract($this->tempData);
|
||||
ob_start();
|
||||
eval('?>' . $view);
|
||||
|
||||
return ob_get_clean() ?: '';
|
||||
})($view);
|
||||
|
||||
$this->logPerformance($start, microtime(true), $this->excerpt($view));
|
||||
$this->tempData = null;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first bit of a long string and add ellipsis
|
||||
*/
|
||||
public function excerpt(string $string, int $length = 20): string
|
||||
{
|
||||
return (strlen($string) > $length) ? substr($string, 0, $length - 3) . '...' : $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets several pieces of view data at once.
|
||||
*
|
||||
* @param non-empty-string|null $context The context to escape it for.
|
||||
* If 'raw', no escaping will happen.
|
||||
* @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context
|
||||
*/
|
||||
public function setData(array $data = [], ?string $context = null): RendererInterface
|
||||
{
|
||||
if ($context !== null) {
|
||||
$data = \esc($data, $context);
|
||||
}
|
||||
|
||||
$this->tempData ??= $this->data;
|
||||
$this->tempData = array_merge($this->tempData, $data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a single piece of view data.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param non-empty-string|null $context The context to escape it for.
|
||||
* If 'raw', no escaping will happen.
|
||||
* @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context
|
||||
*/
|
||||
public function setVar(string $name, $value = null, ?string $context = null): RendererInterface
|
||||
{
|
||||
if ($context !== null) {
|
||||
$value = esc($value, $context);
|
||||
}
|
||||
|
||||
$this->tempData ??= $this->data;
|
||||
$this->tempData[$name] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all of the view data from the system.
|
||||
*/
|
||||
public function resetData(): RendererInterface
|
||||
{
|
||||
$this->data = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current data that will be displayed in the view.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->tempData ?? $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies that the current view should extend an existing layout.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function extend(string $layout)
|
||||
{
|
||||
$this->layout = $layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts holds content for a section within the layout.
|
||||
*
|
||||
* @param string $name Section name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function section(string $name)
|
||||
{
|
||||
// Saved to prevent BC.
|
||||
$this->currentSection = $name;
|
||||
$this->sectionStack[] = $name;
|
||||
|
||||
ob_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the last section
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function endSection()
|
||||
{
|
||||
$contents = ob_get_clean();
|
||||
|
||||
if ($this->sectionStack === []) {
|
||||
throw new RuntimeException('View themes, no current section.');
|
||||
}
|
||||
|
||||
$section = array_pop($this->sectionStack);
|
||||
|
||||
// Ensure an array exists so we can store multiple entries for this.
|
||||
if (! array_key_exists($section, $this->sections)) {
|
||||
$this->sections[$section] = [];
|
||||
}
|
||||
|
||||
$this->sections[$section][] = $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a section's contents.
|
||||
*
|
||||
* @param bool $saveData If true, saves data for subsequent calls,
|
||||
* if false, cleans the data after displaying.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function renderSection(string $sectionName, bool $saveData = false)
|
||||
{
|
||||
if (! isset($this->sections[$sectionName])) {
|
||||
echo '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->sections[$sectionName] as $key => $contents) {
|
||||
echo $contents;
|
||||
if ($saveData === false) {
|
||||
unset($this->sections[$sectionName][$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used within layout views to include additional views.
|
||||
*
|
||||
* @param array<string, mixed>|null $options
|
||||
* @param bool $saveData
|
||||
*/
|
||||
public function include(string $view, ?array $options = null, $saveData = true): string
|
||||
{
|
||||
return $this->render($view, $options, $saveData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the performance data that might have been collected
|
||||
* during the execution. Used primarily in the Debug Toolbar.
|
||||
*
|
||||
* @return list<array{start: float, end: float, view: string}>
|
||||
*/
|
||||
public function getPerformanceData(): array
|
||||
{
|
||||
return $this->performanceData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs performance data for rendering a view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function logPerformance(float $start, float $end, string $view)
|
||||
{
|
||||
if ($this->debug) {
|
||||
$this->performanceData[] = [
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'view' => $view,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function prepareTemplateData(bool $saveData): void
|
||||
{
|
||||
$this->tempData ??= $this->data;
|
||||
|
||||
if ($saveData) {
|
||||
$this->data = $this->tempData;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
/**
|
||||
* View Decorators are simple classes that are given the
|
||||
* chance to modify the output from the view() calls
|
||||
* prior to it being cached.
|
||||
*/
|
||||
interface ViewDecoratorInterface
|
||||
{
|
||||
/**
|
||||
* Takes $html and has a chance to alter it.
|
||||
* MUST return the modified HTML.
|
||||
*/
|
||||
public static function decorate(string $html): string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\View;
|
||||
|
||||
use CodeIgniter\View\Exceptions\ViewException;
|
||||
use Config\View as ViewConfig;
|
||||
|
||||
trait ViewDecoratorTrait
|
||||
{
|
||||
/**
|
||||
* Runs the generated output through any declared
|
||||
* view decorators.
|
||||
*/
|
||||
protected function decorateOutput(string $html): string
|
||||
{
|
||||
$decorators = $this->config->decorators ?? config(ViewConfig::class)->decorators;
|
||||
|
||||
foreach ($decorators as $decorator) {
|
||||
if (! is_subclass_of($decorator, ViewDecoratorInterface::class)) {
|
||||
throw ViewException::forInvalidDecorator($decorator);
|
||||
}
|
||||
|
||||
$html = $decorator::decorate($html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user