neoan3-apps/template

基于PHP DOMDocument的极简模板引擎

v2.1.6 2023-01-24 01:33 UTC

README

PHP模板引擎

Test Coverage Maintainability Build Status

从版本2开始,我们放弃了PHP 7.4的支持。您需要至少PHP 8.0或使用此包的v1.2.0版本。

安装/快速开始

composer require neoan3-apps/template

use Neoan3\Apps\Template\Constants;
use Neoan3\Apps\Template\Template;

require_once 'vendor/autoload.php';

// optional, if set, path defines the relative starting point to templates
Constants::setPath(__DIR__ . '/templates');

echo Template::embrace('<h1>{{test}}</h1>',['test'=>'Hello World']);

内容

模板化

neoan3-template不是一个完整的模板引擎,而是模板引擎应有的样子:使用现代JavaScript解决方案创建动态方法,neoan3-template专注于静态渲染的需求。

profile.html

<h1>{{user}}</h1>
<p>{{profile.name}}</p>
<p n-for="items as key => item" n-if="key > 0">{{item}}-{{key}}</p>

profile.php

$dynamicContent = [
    'user' => 'Test',
    'items' => ['one','two'],
    'profile' => [
        'name' => 'John Doe',
        ...
    ]
];
echo \Neoan3\Apps\Template\Template::embraceFromFile('profile.html',$dynamicContent);

输出

<h1>Test</h1>
<p>John Doe</p>
<p>two-1</p>

主要模板方法

embrace($string, $substitutionArray)

使用双大括号指示的数组键替换为适当的值

embraceFromFile($fileLocation, $substitutionArray)

读取文件内容并执行embrace函数。

迭代(n-for)

n-for循环评估为PHP的foreach,并使用相同的语法(排除$-符号)。为了访问键,使用PHP标记(items as key => value)来模拟$items作为$key => $value。如果没有必要访问键,则使用简单的标记items => item。

$parameters = [
    'items' => ['one', 'two']
];
$html = '<div n-for="items as item">{{item}}</div>';
echo \Neoan3\Apps\Template::embrace($html, $parameters);

输出

<div>one</div>
<div>two</div>

条件(n-if)

您可以将n-if属性附加到任何标签上以便条件渲染。大括号不是必需的,嵌套遵循通用渲染的规则(即您将编写outer.inner以访问$parameters['outer']['inner']

n-if是类型感知的,因此是严格的。

条件在嵌套上下文中工作并继承命名。

n-for中的示例

$parameters = [
    'items' => ['one', 'two']
];
$html = '<div n-for="items as item"><span n-if="item != \'one\'">{{item}}</span></div>';
echo \Neoan3\Apps\Template::embrace($html, $parameters);

输出

<div></div>
<div><span>two<span></div>

自定义函数

与其他模板引擎不同,neoan3-apps/template不包含昂贵的附加功能。我们相信,其他模板引擎提供的许多功能不应包含在模板引擎中。但是,您可以传递自定义闭包以实现自定义转换或类似功能。

示例

$html = '<h3>{{headline(items.length)}}</h3><p n-for="items as item">{{toUpper(item)}}</p>';

$passIn = [
    'items'=>['chair', 'table']
];
// pluralize
\Neoan3\Apps\Template\Constants::addCustomFunction('headline', function ($input){
    return $input . ' item' . ($input>1 ? 's' : '');
});
// transform uppercase
\Neoan3\Apps\Template\Constants::addCustomFunction('toUpper', function ($input){
    return strtoupper($input);
});
echo \Neoan3\Apps\Template::embrace($html, $passIn);

输出

<h3>2 items</h3>
<p>CHAIR</p>
<p>TABLE</p>

您甚至可以使用这样的替换(如php-i18n-translate中使用的那样)

<h1>The current rating is {{rating [%format](% {{% round(percentage) %)}}</h1>
$data = [
 'percentage' => 8,332,
 'rating [%format%]'
];
\Neoan3\Apps\Template\Constants::addCustomFunction('round', function ($input){
    return round((float)$input) . '%';
});
echo \Neoan3\Apps\Template::embraceFromFile('/aboveHtml.html', $data);

输出

<h1>The current rating is 8%</h1>

自定义定界符

使用大括号的原因是:您可以利用一些值可能只由后端填充,并在前端值不存在于您的数据(尚未存在)时在前端处理的事实。但是,也有您想避免前端框架获取未填充变量的情况,或者您有特殊的解析需求以处理各种文件。因此,您可以通过提供所需的标记来使用自定义标识符,并将其提供给embraceembraceFromFile

示例

$html = '
<p>[[name]]</p>
<p>Here is content</p>
';

$substitutions = [
    'name' => 'neoan3'
];
// characters are escaped automatically
\Neoan3\Apps\Template\Constants::setDelimiter('[[',']]');

echo \Neoan3\Apps\Template\Template::embrace($html, $substitutions);

输出

<p>neoan3</p>
<p>Here is content</p>

注意:如果您的定界符是标签,则引擎将不会删除定界符

use \Neoan3\Apps\Template\Constants;
use \Neoan3\Apps\Template\Template;

Constants::setDelimiter('<translation>','<\/translation>');

$esperanto = [
    'hello' => 'saluton'
];

$user => ['userName' => 'Sammy', ...];

$html = "<h1><translation>hello</translation> {{userName}}</h1>";

$translated = Template::embrace($html, $esperanto);

Constants::setDelimiter('{{','}}');

echo Template::embrace($translated, $user);

输出

<h1><translation>salutaton</translation> Sammy</h1>

自定义属性

在底层,n-if和n-for只是自定义属性。您可以通过任何可调用的函数添加自己的属性并扩展引擎以满足您的需求。

use \Neoan3\Apps\Template\Constants;

class TranslateMe
{
    private string $language;
    
    // you will receive the native DOMAttr from DOMDocument
    // and the user-provided array
    function __invoke(\DOMAttr &$attr, $contextData = []): void
    {
        // here we are going to use the "flat" version of the context data
        // it translates something like 
        // ['en' => ['hallo'=>'hello']] to ['en.hallo' => 'hello]
        $flatValues = Constants::flattenArray($contextData[$this->language]);
    
        // if we find the content of the actual element in our translations:
        if(isset($flatValues[$attr->parentNode->nodeValue])){
            $attr->parentNode->nodeValue = $flatValues[$attr->parentNode->nodeValue];
        }
    }
    function __construct(string $lang)
    {
        $this->language = $lang;
    }
}
<!-- main.html -->
<p translate>hallo</p>
use \Neoan3\Apps\Template\Constants;
use \Neoan3\Apps\Template\Template;
...
$translations = [
    'en' => [
        'hallo' => 'hello',
        ...
    ],
    'es' => [
        'hallo' => 'hola',
        ...
    ]
];

$userLang = 'en';

Constants::addCustomAttribute('translate', new TranslateMe($userLang));
echo Template::embraceFromFile('/main.html', $translations)

面向对象

到目前为止,我们使用的是纯静态方法。但是,“模板”方法仅仅是初始化解释器的门面。如果您需要更多控制正在发生的事情,或者它更适合您的环境,您可以直接使用它。

$html = file_get_contents(__DIR__ . '/test.html);

$contextData = [
    'my' => 'value'
];
$templating = new \Neoan3\Apps\Template\Interpreter($html, $contextData);


// at this point, nothing is parsed or set if we wanted to use the attributes n-if or n-for, we would have to set it
// note how we are free to change the naming now

\Neoan3\Apps\Template\Constants::addCustomAttribute('only-if', new \Neoan3\Apps\Attributes\NIf());

// Let's parse in one step:
$templating->parse();

// And output

echo $templating->asHtml();

提示

有一些逻辑上合理但不是很明显的事情

循环的父元素

由于分层处理,许多内部解析操作可以导致竞争条件。想象以下HTML

// ['items' => ['one','two'], 'name' => 'John']
<div>
    <p n-for="items as item">{{item}} {{name}}</p>
    <p>{{name}}</p>
</div>

在此场景中,解析器首先会命中属性 n-for 并将 p 标签添加到父节点(此处为 div)。现在,n-for 控制这个父节点并解释其子节点。由于每次循环都会重新评估上下文,但第二个 p 标签不会迭代,因此生成的输出将是

<div>
    <p>{{name}}</p>
    <p>one John</p>
    <p>two John</p>
</div>

因此建议在使用属性方法时使用 一个 独特的父节点

// ['items' => ['one','two'], 'name' => 'John']
<div>
    <p n-for="items as item">{{item}} {{name}}</p>
</div>
<div>
    <p>{{name}}</p>
</div>

扁平数组属性

解释器会将给定的上下文数组“扁平化”,以便允许使用点符号。在这个过程中会添加泛型值

// ['items' => ['one','two'], 'deepAssoc' => ['name' => 'Tim']]
<p>{{items}}</p>
<p>{{items.0}}</p>
<p>{{items.length}}</p>
<p>{{deepAssoc.name}}</p>

输出

<p>Array</p>
<p>one</p>
<p>2</p>
<p>Tim</p>

如有需要,您可以在逻辑中使用此功能

<div n-if="items == 'Array'">
    <ul>
        <li n-for="items as item">{{item}}</li>
    </ul>
</div>