level-2/transphporm

模板的新方法

v1.5.1 2020-08-30 18:48 UTC

README

Scrutinizer-CI

Gitter

Transphporm 是 PHP 模板的新方法。实话实说,PHP 的模板真的很糟糕,因为它涉及到像这样的代码

<ul>
<?php foreach ($users as $user) : ?>
	<li><?= $user->name; ?></li>
<?php endforeach; ?>
</ul>

或者是这种混乱的某种变体

<ul>
{% for user in users %}
	<li>{{ user.username|e }}</li>
{% endfor %}
</ul>

或者

<ul>
	{users}
		<li>{user.name}</li>
	{/users}
</ul>

为什么这很糟糕?因为它混合了逻辑和模板。模板中混入了处理指令。在中间的例子中,这只是稍微抽象的 PHP 代码。编写模板的人同时负责编写显示逻辑和理解提供的数据结构。

这样的模板系统仍然混合了逻辑和标记,这是它们试图避免的。

这相当于 <h1 style="font-weight:bold">标题</h1>,因为它混合了两个非常不同的关注点。

Transphporm 不同

项目目标

  1. 完全分离标记与处理逻辑。(模板中不使用 if 语句或循环!)
  2. 尽可能紧密地遵循 CSS 概念和语法。(这使得对于已经理解 CSS 的人来说,学习它变得非常容易。)

使用 Transphporm,设计师只需提供一些包含一些占位符数据的原始 HTML 或 XML。(设计师们更喜欢 lorem ipsum,而不是在他们的设计中看到 {{description}}!)

<ul>
	<li>User name</li>
</ul>

这是纯标记,没有任何处理指令。然后 Transphporm 会将标记中的占位符数据替换为您想要的真实数据。

但是处理指令在哪里?Transphporm 沿着 CSS 的领导。所有的处理逻辑都存储在外部的“转换样式表”中,这是一个完全独立的文件,包含完全可重用的处理指令。

在最基本的情况下,Transphporm 通过提供字符串形式的样式表和 HTML/XML 来工作。

Transphporm 允许您将内容插入页面上的任何元素。传统的模板引擎迫使您在标记中放置标记,这些标记将在内容内部被替换(本质上使用 str_replace)。

Transphporm 采取了不同的方法,允许您使用类似于 CSS 的语法插入内容。您不需要在模板中提供特殊的标记;模板是普通的 HTML,没有任何特殊语法。然后可以使用 CSS 样式选择器定位页面上的元素。

例如,以下样式表

h1 {content: "My Title";}

将任何 <h1> 标签的内容设置为 "我的标题"。给定以下代码

$xml = '<h1>Original Title</h1>';

$tss = 'h1 {content: "Replaced Title"; }';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

输出将是

<h1>Replaced Title</h1>

Transphporm.Builder 的参数可以是 HTML/XML 和 TSS 字符串,或者要加载的文件名。

//Load files instead of strings, the base path is the current working directory (getcwd())
$template = new \Transphporm\Builder('template.xml', 'stylesheet.tss');

这允许前所未有的灵活性。而不是考虑内容哪些部分可能是动态的,并在模板中正确位置添加如 {{user.name}} 这样的内容,这些关注点可以在设计模板时忽略,并用一些静态内容替换。然后 Transphporm 可以替换页面上的任何内容。这允许您重新使用模板。有时您可能替换一些内容,有时您可能使用模板的默认内容!

使用 Transphporm 的 5 个原因

  1. 将内容写入任意元素。在使用传统的模板引擎时,设计师需要在模板中放置标记,例如 {{name}},在需要将内容注入模板的任何位置。而Transphporm允许开发者将内容写入页面上的任何HTML元素,设计师可以专注于设计而不是担心可能添加的内容。

  2. 任何内容都可以是部分模板。传统的模板引擎要求你必须将每个部分放入自己的文件中。这对设计师来说很糟糕,因为他们不能快速轻松地看到部分在完整布局中的样子。使用Transphporm,设计师可以与完整的HTML文件一起工作,开发者可以从任何文件中提取任何元素作为部分。

  3. 可重用显示逻辑。因为显示逻辑放置在自己的外部文件中,你可以用相同的显示逻辑使用多个XML文件。这是外部CSS文件与HTML中的style=属性之间的区别。

  4. 在客户端或服务器上渲染模板。TSS格式并不是像传统模板引擎那样的稍微抽象的PHP代码;它是一个自定义格式,不依赖于PHP。因此,你可以使用XML文件和TSS文件,在服务器上使用Transphporm渲染,或者在浏览器中使用JavaScript实现Tranjsform渲染。

  5. 如果你使用过CSS,Transphporm很容易学习。Transphporm遵循CSS语法并使用一些相同的词汇。即使你对CSS只有基本的了解,你也能轻松学会使用Transphporm!

Transphporm为设计师和开发者提供了前所未有的灵活性,这是传统模板引擎所无法实现的。

安装

安装Transphporm的首选方法是通过Composer。Transphporm可以从Packagist获取,如下所示

level-2/transphporm

但是,如果你不想使用Composer,你可以手动安装Transphporm

  1. 下载并提取Transphporm到你的项目中
  2. 使用PSR-0兼容的自动加载器,如Axel
  3. 将Transphporm的src目录注册到自动加载器中。使用Axel,这可以通过以下方式完成
require_once 'axel/axel.php';
$axel = new \Axel\Axel;
$axel->addModule(new \Axel\Module\PSR0('./path/to/Transphporm/src', '\\Transphporm'));

数据

通常不可能在静态文件(如样式表)中指定内容。tss格式也允许引用外部数据。这些数据提供给模板构建器的output方法,并且可以在样式表中使用data()函数引用。这可以类比为CSS中的url()函数,因为它引用了外部资源。

$xml = '<h1>Original Title</h1>';

$data = 'My Title!';

$tss = 'h1 {content: data(); }';


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

输出

<h1>My Title!</h1>

大多数时候,你需要处理更复杂的数据结构。Transphporm允许使用内置的数据函数从数据结构中读取数据

$data = new stdclass;

$data->title = 'My Title!';
$data->description = 'Description of the page...';


$xml = '
	<h1>Example Title</h1>
	<p>Example Description</p>
	';

$tss = '
	h1 {content: data(title);}
	p {content: data(description);}
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

这将输出

<h1>My Title!</h1>
<p>Description of the page....</p>

内容

内容属性可以接受多个值,每个值可以是函数调用,如data,或引用的字符串,并且将连接任何提供的值

$xml = '<h1>Original Title</h1>';

$data = 'My Title!'

$tss = 'h1 {content: "Title: ", data(); }';


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

输出

<h1>Title: My Title!</h1>

有关插入内容的更多信息,请参阅维基页面基本用法:插入内容基本用法:外部数据

循环

回到用户列表示例,考虑以下数据结构

$users = [];

$user = new stdclass;
$user->name = 'Tom';
$user->email = 'tom@example.org';

$users[] = $user;


$user = new stdclass;
$user->name = 'Scott';
$user->email = 'scott@example.org';

$users[] = $user;

使用Transphporm,可以像这样生成用户列表

$xml = '<ul>
	<li>Name</li>
</ul>';


$tss = '
	ul li {repeat: data(users); content: iteration(name);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

repeat 指示Transphporm对每个提供的数组重复选定的元素。

data(users) 读取PHP中提供的 $data['users']

iteration(name) 指向当前迭代的值,并读取 name 属性。此代码输出

<ul>
	<li>Tom</li>
	<li>Scott</li>
</ul>

同样,iteration 可以读取特定值并在嵌套节点中使用

$xml = '<ul>
	<li>
		<h3>Name</h3>
		<span>email</span>
	</li>
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li h3 {content: iteration(name);}
	ul li span {content: iteration(email);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

这将输出

<ul>
	<li>
		<h3>Tom</h3>
		<span>tom@example.org</span>
	</li>
	<li>
		<h3>Scott</h3>
		<span>scott@example.org</span>
	</li>
</ul>

有关循环的更多信息,请参阅 Wiki 页面:基本用法:循环

删除块

直接从CSS语法中提取,Transphporm支持 display: none,这实际上会完全从文档中删除元素。

$xml = '<ul>
	<li>
		<h3>Name</h3>
		<span>email</span>
	</li>
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li h3 {content: iteration(name);}
	ul li span {display: none}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

输出

<ul>
	<li>
		<h3>Tom</h3>
	</li>
	<li>
		<h3>Scott</h3>
	</li>
</ul>

注意,这在与迭代值伪元素一起使用时非常有用。有关条件逻辑的更多示例,请参阅 Wiki 页面:基本用法:条件逻辑

CSS选择器

Transphporm支持以下CSS选择器

tagName #id .className tagName.className direct > descendant [attribute] [attribute=value] [attribute!=value]

并且这些中的任何一个都可以链式使用

main .post > .author[data-admin=true] 将匹配任何具有类名 author 的元素,该元素的 data-admin 属性设置为true,并且直接位于类名为 post 的元素内部,而该元素位于 <main> 元素内部。

有关支持的完整选择器列表以及每个选择器的示例,请参阅 Wiki 页面:基本用法:CSS选择器

伪元素

Transphporm还支持几个伪元素

:before:after 允许将内容写入节点的开始或结束位置。

:nth-child(n) :nth-child(odd) :nth-child(even)

有关这些的示例,请参阅 Wiki 页面:基本用法:伪元素

迭代值

Transphporm还可以检查元素的迭代数据。当您想要根据迭代值的特定内容隐藏特定块时,这特别有用

格式为

element:iteration[name=value] {}

这将选择任何迭代内容 name 属性等于 value 的元素。

以下代码将隐藏任何类型为 'Admin' 的用户。

$users = [];

$user = new stdclass;
$user->name = 'Tom';
$user->email = 'tom@example.org';
$user->type = 'Admin';
$users[] = $user;


$user = new stdclass;
$user->name = 'Scott';
$user->email = 'scott@example.org';
$user->type = 'Standard';
$users[] = $user;

$user = new stdclass;
$user->name = 'Jo';
$user->email = 'jo@example.org';
$user->type = 'Standard';
$users[] = $user;



$xml = '
<ul>
	<li>
		<h3>Name</h3>
		<span>email</span>
	</li>
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li:iteration[type='Admin'] {display: none;}
	ul li h3 {content: iteration(name);}
	ul li span {content: iteration(email);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

输出

<ul>
	<li>
		<h3>Scott</h3>
		<span>scott@example.org</span>
	</li>
	<li>
		<h3>Jo</h3>
		<span>jo@example.org</span>
	</li>
</ul>

写入属性

与CSS不同,Transphporm选择器允许直接选择单个属性以设置其值。这是通过使用伪元素 :attr(name) 实现的,该伪元素选择匹配元素上的属性。

element:attr(id)

将选择元素的ID属性。

工作示例

$users = [];

$user = new stdclass;
$user->name = 'Tom';
$user->email = 'tom@example.org';
$users[] = $user;


$user = new stdclass;
$user->name = 'Scott';
$user->email = 'scott@example.org';
$users[] = $user;



$xml = '
<ul>
	<li>
		<h3>Name</h3>
		<a href="mailto:email">email</a>
	</li>
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li a {content: iteration(email);}
	ul li a:attr(href) {content: "mailto:", iteration(email);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

注意,这使用多个值来设置 content 属性,以将完整URL与 mailto 连接起来。

输出

<ul>
	<li>
		<h3>Tom</h3>
		<a href="mailto:Tom@example.org">Tom@example.org</a>
	</li>
	<li>
		<h3>Scott</h3>
		<a href="mailto:scott@example.org">scott@example.org</a>
	</li>
</ul>

从属性中读取

还可以使用内容属性中的 attr(name) 从属性中读取。

$xml = '
<h1 class="foo">bar</h1>
';

$tss = 'h1 {content: attr(class);}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

输出

<h1 class="foo">foo</h1>

HTTP头

Transphporm支持设置HTTP头。您必须定位页面上的元素,例如HTML,并可以使用 :header 伪元素来设置HTTP头。例如,可以这样执行重定向

html:header[location] {content: "/redirect-url"; }

Transphporm不会直接写入HTTP头。`output()` 函数的返回值是一个包含 bodyheaders 的数组。《body》是渲染的HTML代码,`headers` 包含已设置的任何HTTP头。

$xml = '<html><div>Foo</div></html>';
$tss = 'html:header[location] {content: "/redirect-url"; }';

$template = new \Transphporm\Builder($xml, $tss);

print_r($template->output());

将打印

Array (
	'body' => '<html><div>foo</div></html>',
	'headers' => Array (
		Array (
			[0] => 'location',
			[1] => '/redirect-url'
		)
	)
)

要实际上将头部发送到浏览器,您需要手动调用头部命令

foreach ($template->output()->headers as $header) {
	header($header[0] . ': ' . $header[1]);
}

条件应用HTTP头部

在大多数情况下,您可能希望有条件地显示一个头部。例如

  • 在成功时执行重定向。
  • 当找不到记录时发送404头部。

要执行此操作,您可以使用条件数据查找

class Model {
	public function getProduct() {
		return false;
	}
}



$tss = 'html:data[getProduct='']:header[status] {content: '404'}

$xml = '<html></html>';

$data = new Model;

$template = new \Transphporm\Builder($xml, $tss);

$output = $template->output($data);

print_r($output->headers)

打印

Array (
	[0] => 'status',
	[1] => '404'
)

要使用此功能,您应该调用内建的PHP http_response_code 函数并传递此状态

foreach ($template->output()->headers as $header) {
	if ($header[0] === 'status') http_response_code($header[1]);
	else header($header[0] . ': ' . $header[1]);
}

Transphporm不会发送任何头部信息

Transphporm默认不会将任何输出发送到浏览器。这是为了最大灵活性。您仍然需要手动发送头部信息并输出正文。

数据格式化

Transphporm支持将数据格式化为输出。格式化语法如下

h1 {content: "content of element"; format: [NAME-OF-FORMAT] [OPTIONAL ARGUMENT OF FORMAT];}

字符串格式化

Transphporm目前支持以下字符串格式

  • 大写
  • 小写
  • 标题大小写

示例

字符串格式:大写

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "TeSt"; format: uppercase}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

打印

<h1>TEST</h1>

字符串格式:小写

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "TeSt"; format: lowercase}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

打印

<h1>test</h1>

字符串格式:标题大小写

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "test"; format: titlecase}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

打印

<h1>Test</h1>

数字格式

Transphporm支持使用decimal格式将数字格式化为指定的小数位数。您可以指定小数位数

数字格式:decimal

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "11.234567"; format: decimal 2}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

打印

<h1>1.23</h1>

区域设置

对于日期、时间和货币格式化,Transphporm支持区域设置。目前仅提供enGB,但您可以编写自己的。

要设置区域设置,请使用builder::setLocale方法。这可以接受区域设置名称,例如位于Formatter/Locale/{name}.json内的区域设置,例如:

$template = new \Transphporm\Builder($xml, $tss);
$template->setLocale('enGB');

目前仅支持enGB。或者,您可以提供一个与Formatter/Locale/enGB.json中使用的格式相匹配的数组。

日期格式

Transphporm支持格式化日期。您可以引用一个\DateTime对象或一个字符串。如果可能,字符串将被自动转换为日期

$xml = '
<div> </div>
';

$tss = 'div {content: "2015-12-22"; format: date}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

这将使用区域设置中指定的日期格式来格式化日期。对于enGB,这是d/m/Y

<div>22/12/2015</div>

或者,您可以指定格式作为格式化器的第二个参数

$xml = '
<div> </div>
';

$tss = 'div {content: "2015-12-22"; format: date "jS M Y"}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;
<div>22nd Dec 2015</div>

您也可以使用time进行格式化,默认为区域设置的H:i

$xml = '
<div> </div>
';

$tss = 'div {content: "2015-12-22 14:34"; format: time}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;
<div>14:34</div>

相对时间

您可以将relative格式化器提供给日期,它将显示如下内容:

  • "明天"
  • "昨天"
  • "两小时前"
  • "3周前"
  • "3个月后"
  • "10年后"

字符串由区域设置指定。

导入其他文件

像CSS一样,Transphporm支持导入其他TSS文件的@import

imported.tss

h1 {content: "From imported tss"}
$xml = '
<h1> </h1>
<div> </div>
';

$tss = "
	@import 'imported.tss';
	div {content: 'From main tss'}
";

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

输出

<h1>From imported tss</h1>
<div>From main tss</div>

缓存

Transphporm有两种类型的缓存,都需要启用

  1. 缓存TSS和XML文件。这可以防止它们在渲染模板时每次都被解析。即使您不打算使用update-frequency(见下文),启用此功能也是值得的。

  2. update-frequency这是一个属性,允许您以指定的时间间隔更新元素。

要启用缓存,您必须创建(或使用)一个实现PHP内置的\ArrayAccess接口的缓存类,例如Level-2/SimpleCache。然后将其实例分配给构建器

$cache = new \SimpleCache\SimpleCache('/tmp');

$template = new \Transphporm\Builder($xml, $tss);
$template->setCache($cache);

echo $template->output($data)->body;

这样做将自动启用文件缓存。一旦分配了缓存,TSS文件只有在它们被更新时才会被解析。这可以节省在页面加载时解析TSS文件,即使您不使用update-frequency,这也是值得的。

update-frequency

update-frequency是TSS指令,描述了给定TSS规则应该运行的频率。在幕后,Transphporm每次渲染模板时都会将其最终输出保存到缓存中,并根据update-frequency对其进行更改。例如

ul li {repeat: data(users); update-frequency: 10m}

这将每10分钟运行一次TSS规则。幕后工作的方式是

  • 渲染的模板被存储在缓存中。
  • 下次页面加载时,加载之前渲染的模板。
  • 如果计时器已过期,将再次在缓存的模板上运行重复/内容等指令,并更新模板。

这允许页面不同部分以不同的频率更新。

MVC中的缓存

如果您使用的是真正的MVC(而不是PAC,大多数框架实际上使用的是PAC)并且您将模型传递到视图中,如果模型作为data参数传递并且有一个getUsers函数,Transphporm可以调用它,并且仅在模板更新时执行查询。

ul li {repeat: data(getUsers); update-frequency: 10m}

大多数框架不会将模型传递到视图中,然而对于这样做的情况,这允许两级缓存。查询仅在视图根据视图的超时更新时运行。

构建整个页面

Transphporm使用自上而下的方法构建页面。大多数框架需要编写布局模板,然后将内容拉入其中。这使得根据页面更改布局变得非常困难。(至少您需要向布局HTML添加一些代码)。Transphporm使用自上而下的方法,而不是流行的自下而上的方法,其中子模板在布局中的特定位置插入。

您仍然有两个文件,一个是布局,另一个是内容,但TSS应用于布局,这意味着TSS可以更改布局中的任何内容(添加脚本标签、添加CSS、更改页面标题和元标签等)。

layout.xml

<!DOCTYPE HTML>
<html>
	<head>
		<title>My Website</title>
	</head>
	<body>
		<header>
			<img src="logo.png" />
		</header>

		<nav>
			<ul>
				<li><a href="/">Home</a></li>
				<li><a href="about.html">About</a></li>
				<li><a href="contact.html">Contact</a></li>
			</ul>
		</nav>

		<main>
			Main content

		</main>

		<footer>
			Copyright <span>year</span>
		</footer>
	</body>
</html>

然后是home.xml

<?xml version="1.0"?>
<template>
	<p>Welcome to my website</p>
</template>

然后可以使用TSS文件将一个包含在另一个中

home.tss

title {content: "My Site"}

main {content: template("home.xml")}

footer span {content: "now"; format: date "Y"}

然后使用以下代码将<main>元素的内容设置为存储在home.xml中的模板内容

$template = new \Transphporm\Builder('layout.xml', 'home.tss');

echo $template->output()->body;

显然,您可以通过添加相关的about.xml和TSS来添加关于页面。

title {content: "About me"}

main {content: template("about.xml")}

footer span {content: "now"; format: date "Y"}
$template = new \Transphporm\Builder('layout.xml', 'about.tss');

echo $template->output()->body;

这里有一些重复,可以以两种方式解决。

1) 将布局规则放在自己的文件中,例如base.tss

footer span {content: "now"; format: date "Y"}

然后在about.tsshome.tss中导入它,例如。

@import "base.tss";

title {content: "About me"}

main {content: template("about.xml")}

2) 使用数据外部描述相关部分

page.tss

title {content: data(title);}

main {content: template(data(page));}

footer span {content: "now"; format: date "Y"}
//Home template:

$template = new \Transphporm\Builder('layout.xml', 'page.tss');

$template->output(['title' => 'My Website', 'page' => 'home.xml'])->body;



//About template:
$template = new \Transphporm\Builder('layout.xml', 'page.tss');

$template->output(['title' => 'About Me', 'page' => 'about.xml'])->body;

这允许自上而下的方法。大多数框架采用自下而上的方法,即构建布局,然后构建内容,将一个的输出放入另一个中。这带来了一个问题:如何设置每个页面的标题?或者也许在每页包含不同的侧边栏?框架倾向于使用本质上全局变量来存储页面标题和任何布局选项。TSS一次性构建整个页面,因此任何页面都可以更改布局的任何部分。

鸣谢

Transphporm最初由Tom Butler(@TomBZombie)开发,由Richard Sollee(@solleer)添加了额外功能实现、错误修复和建议。