lucinda/view-language

针对PHP应用的高性能视图模板API,受Java JSTL和C编译器启发,使用标签和表达式

v4.0.4 2023-12-26 12:24 UTC

README

目录

关于

此API是ViewLanguage模板引擎的PHP编译器,一种受Java JSP&JSTL启发的标记语言,作为HTML标准的扩展,旨在通过通过以下方式完全消除视图中的脚本需求:

  • 通过通过表达式来接口变量。
  • 通过通过标签来接口逻辑(控制结构、重复的HTML段)

diagram

为了实现其目标,需要遵循以下步骤

API完全遵循PSR-4规范,仅需要PHP8.1+解释器和SimpleXML扩展。要快速了解其工作原理,请查看

  • 安装:描述如何在您的计算机上安装API,考虑到上述步骤
  • 单元测试:API具有100%的单元测试覆盖率,使用UnitTest API而不是PHPUnit,以获得更大的灵活性
  • 示例:展示了如何使用ViewLanguage进行模板化,包括每个步骤的解释
  • 参考指南:显示了API附带的所有标签列表

表达式

一个表达式是ViewLanguage中表示脚本变量的表示。表达式的语法是

${variableName}

其中variableName可以是

一个非常强大的功能是嵌套表达式的能力:编写其键本身是表达式的表达式。这可以任何深度进行,当迭代多个列表并将一个与另一个的键/值关联链接时非常有用

标签

一个标签是ViewLanguage中表示脚本逻辑的表示。所有标签都像HTML标准的扩展一样工作,因此它们有名称、可选属性和主体。ViewLanguage已知有两种类型的标签

  • 宏标签:API定义的、在内容编译之前处理的标签。
  • 库标签:属于库并具有在该库中唯一名称的API/用户定义的标签,这些标签需要编译。

一个非常强大的功能是标签的递归能力:允许在视图语言标签内嵌套视图语言标签。每当发生这种情况时,编译器会逐步深入,直到没有标签为止!

宏标签

宏标签的工作方式类似于C宏:在代码编译之前,它们会被读取并“展开”,以便编译可以在整个源代码上运行。语法与普通HTML标签相同。

<NAME ATTRIBUTE="value" .../>

位置

  • NAME:执行单个逻辑操作的标签名称。
  • ATTRIBUTE:配置标签行为。

API定义以下宏标签

  • escape:编译器会忽略其主体的标签。这在标记内容时不希望被解析时是必要的。
  • import:编译器会用其“file”属性指向的文件的主体替换其声明。这对于布局/模板化至关重要。
  • namespace:编译器会告知其在哪里查找默认文件夹中找不到的标签库。

目前,用户无法定义自己的宏标签!

库标签

库标签是可以编译的API/用户定义的HTML片段,预期在视图语言应用中实现脚本逻辑。它们是依赖于变量且无法通过 . 加载的非静态重复模板(HTML)代码片段。

它们的语法扩展了HTML标准

<LIBRARY:TAG ATTRIBUTE="value" ...>...</LIBRARY:TAG>

如果没有主体

<LIBRARY:TAG ATTRIBUTE="value" .../>

位置

  • LIBRARY:包含在模板上执行相关逻辑操作的命名空间。规则
    • 值必须为小写字母和数字。
    • "-"符号也可以用于替换多词值中的空格
  • TAG:执行单个逻辑操作的标签名称。规则
    • 值必须为小写字母和数字。
    • 符号也可以用于替换多词值中的空格
  • ATTRIBUTE:配置标签行为(可以是零个或多个)。规则
    • 名称必须为小写字母和数字。
    • "_"符号也可以用于替换多词名称中的空格
    • 值只能是原始数据(字符串或数字)或视图语言表达式。
    • 与标准HTML不同,目前属性不能多行。

标准标签

API包括一个包含标签的标准库,其中LIBRARY为空时,用于编程语言指令。

  • :for:遍历列表中的数字。
  • :foreach:通过键和值遍历字典。
  • :if:评估符合条件的主体。
  • :elseif:评估不符合先前if/elseif条件的条件主体。
  • :else:评估不符合先前if/else if的条件主体。
  • :set:创建变量并/或为其设置值。
  • :unset:取消变量。
  • :while:在条件上执行循环。
  • :break:结束循环。
  • :continue:跳过当前循环的其余评估并移动到下一个迭代。

标准标签与以下类型的ATTRIBUTE值一起工作

  • 标量:字符串或整数
  • 表达式:ViewLanguage 表达式。如果将辅助函数用作属性值,则必须将其保持未装饰:count(${asd}) 而不是 ${count(${asd})}
  • 条件:预期语法类似于 C 语言,但最终匹配编译成语言代码的语法(在我们的例子中是 PHP)。例如:${x}==true 在 PHP 中表示 $x==true

用户自定义标签

为了将 HTML 响应分解成离散的单位,开发者必须创建自己的库和标签。用户自定义标签根据以下规则在磁盘上找到

  • 库的名称必须是编译时提供的 tags 文件夹内的一个文件夹
  • 标签代码必须是库文件夹内与 name 名称相同的 HTML 文件

配置

为了配置此 API,您必须在 XML 中包含一个 templating 标签

<templating compilations_path="..." tags_path="..." templates_path="..." templates_extension="..." />

位置

  • compilations_path:(必填)保存 ViewLanguage 模板 PHP 编译的路径
  • tags_path:(可选)ViewLanguage 标签库的位置路径
  • templates_path:(可选)模板的位置路径
  • templates_extension:(可选)模板文件的扩展名。如果没有设置,则假设为 "html"!

示例

<templating compilations_path="compilations" tags_path="application/taglib" templates_path="application/views" templates_extension="html"/>

编译

完成上述步骤后,您需要实例化 Lucinda\Templating\Wrapper,以便以后能够编译模板

$wrapper = new Lucinda\SQL\Wrapper(simplexml_load_file(XML_FILE_NAME));

对象有以下方法,可用于编译一个或多个模板

模板是如何编译的?

与任何其他模板语言一样,编译首先遍历依赖关系的树形结构,并进一步组装结果到 PHP 文件中,然后通过绑定用户接收到的数据生成 HTML。因此,它涉及以下步骤

  • 如果存在 $template 参数的 PHP 编译,则检查自上次更新以来内部引用的元素是否已更改。如果它不存在或已更改
    • 递归解析 标签(在编译文件中备份 标签体以排除解析)并将结果追加到编译文件中
    • 解析模板中定义的 标签,以了解在哪里定位未在默认标签库文件夹中定义的用户自定义标签库
    • 递归解析 库标签(在编译文件中备份 标签体以排除解析)并在编译文件中将它们替换为相关的 PHP/HTML 代码。
    • 解析 表达式 并在编译文件中将它们替换为相关的 PHP 代码。
    • 在编译文件中恢复备份的 标签体(如果有)
    • 将新的编译和其部分的校验和(模板、标签)缓存在磁盘上,以供未来的验证使用
  • 在输出缓冲区中,加载编译文件,将其绑定到用户提供的 $data,并从中生成最终的 HTML

由于整个过程对性能有一定的需求,PHP 编译文件将缓存在磁盘上,并在下一次请求中直接返回,除非其组件(模板或标签)之一已更改。这使得 API 能够以约 0.001 秒的平均时间编译,从而不会带来任何性能开销,但提供了优雅视图的所有优点!

安装

首先选择 API 将要安装的文件夹,然后在控制台中使用以下命令

composer require lucinda/templating

然后创建一个包含配置设置的 configuration.xml 文件(请参阅上面的 配置),并在项目根目录中创建一个 index.php 文件,其中包含以下代码

require(__DIR__."/vendor/autoload.php");
$wrapper = new Lucinda\SQL\Wrapper(simplexml_load_file("configuration.xml"));

要编译模板

$html = $wrapper->compile(TEMPLATE_NAME, USER_DATA);

位置

  • TEMPLATE_NAME 是必须遵守以下规则的 基础模板
    • 必须是位于 templates_path (见 配置) 的文件路径
    • 指向的文件必须具有 templates_extension (见 配置)
    • 由于上述原因,不能包含扩展名
  • USER_DATA 是从后端获取并在模板中访问的值列表,遵守以下规则
    • 必须是一个数组
    • 条目键必须是字符串或整数(通常是 PHP 的要求)
    • 条目值必须是标量或数组
    • 如果数组是多维的,同级中的键和值必须遵守上述相同的规则

为了显示结果

header("Content-Type: text/html; charset=UTF-8");
echo $html;

单元测试

对于测试和示例,请检查 API 源中的以下文件/文件夹

  • unit-tests.sql:在执行单元测试之前需要在服务器上运行一次的 SQL 命令(假设是 MySQL)
  • test.php:在控制台中运行单元测试
  • unit-tests.xml:设置单元测试并模拟 "sql" 标签
  • tests:来自 src 文件夹的类的单元测试

示例

假设 configuration.xml (见 配置安装) 是

<xml>
   <templating compilations_path="compilations" tags_path="application/taglib" templates_path="application/views" templates_extension="html"/>
</xml>

让我们创建一个具有以下主体的 application/views/index.html 模板

<import file="header"/>
Hello, dear ${data.author}! Author of:
<ul>
    <:foreach var="${data.apis}" key="name" val="url">
    <my:api id="${name}" link="${url}"/>
    </:foreach>
</ul>
<import file="footer"/>

上面的操作做了以下事情

  • 加载 application/views/header.html 模板的主体
  • 通过 表达式 输出 $data 中 author 数组的值
  • 通过 <:foreach> 遍历 $data 中 apis 数组的值
  • 在每次迭代中,加载 application/taglib/my/api.html 模板的主体(用户标签),将属性绑定到值
  • 加载 application/views/footer.html 模板的主体

application/views/header.html 文件的内容

<html>
    <head>
        <title>View Language API Tutorial</title>
    </head>
    <body>

application/taglib/my/api.html 文件的内容

<li><a href="$[id]">$[link]</a></li>

application/views/footer.html 的内容

    </body>
</html/>

如上所示,模板依赖于从后端接收的变量(author & apis),这两个键都是 $data 数组的键。假设后者的值是

$data = [
	"author" => "Lucian Popescu", 
	"apis" => ["View Language API" => "https://www.lucinda-framework.com/view-language", "STDOUT MVC API" => "https://www.lucinda-framework.com/stdout-mvc"]
];

现在让我们编译 application/views/index.html 模板(见 编译)并将其绑定到 $data

require(__DIR__."/vendor/autoload.php");
$wrapper = new Lucinda\SQL\Wrapper(simplexml_load_file("configuration.xml"));
$html = $wrapper->compile("index", $data);

首先,ViewLanguage 基础模板被编译成 PHP,结果保存到 compilations/index.php 文件中(如果尚未存在),以下是其主体

<html>
    <head>
        <title>View Language API Tutorial</title>
    </head>
    <body>
        Hello, <?php echo $data["author"]; ?>, author of:
        <ul>
            <?php foreach($data["apis"] as $name=>$url) { ?>
            <li><a href="<?php echo $url; ?>"><?php echo $name; ?></a></li>
            <?php } ?>
        </ul>
    </body>
</html/>

然后,在输出缓冲区中加载上述文件,并与其绑定,因此最终返回的 HTML 将是

<html>
    <head>
        <title>View Language API Tutorial</title>
    </head>
    <body>
        Hello, Lucian Popescu, author of:
        <ul>
            <li><a href="https://www.lucinda-framework.com/view-language">View Language API</a></li>
            <li><a href="https://www.lucinda-framework.com/stdout-mvc">STDOUT MVC API</a></li>
        </ul>
    </body>
</html/>

参考指南

标签转义

标记标签体以被 ViewLanguage 编译器忽略。语法

<escape>
...
</escape>

如何将此标签编译成 PHP 的示例

标签导入

将另一个视图语言模板包含到当前模板中。语法

<import file="..."/>

属性

如何将此标签编译成 PHP 的示例

标签命名空间

标记用户定义标签库的定制位置(必须在后续声明之前放置)。语法

<namespace taglib="..." folder="..."/>

属性

如何将此标签编译成 PHP 的示例

标签 :for

创建一个 FOR 循环。语法

<:for var="..." start="..." end="..." (step="...")>
    ...
</:for>

属性

如何将此标签编译成 PHP 的示例

标签 :foreach

创建一个 FOR EACH 循环。语法

<:foreach var="..." (key="...") val="...">
    ...
</:foreach>

属性

如何将此标签编译成 PHP 的示例

标签 :if

创建一个 IF 条件。语法

<:if test="...">
    ...
</:if>

如果后面跟着 :else:elseif,则标签不得关闭!

属性

如何将此标签编译成 PHP 的示例

您也可以使用三元运算符从表达式中运行简单的 IF/ELSE 语句!

标签 :elseif

创建一个 ELSE IF 条件。语法

<:elseif test="...">
    ...
</:if>

如果后面跟着 :else:elseif,则标签不得关闭!

属性

如何将此标签编译成 PHP 的示例

标签 :else

创建一个ELSE条件。语法

<:else>
    ...
</:if>

如何将此标签编译成 PHP 的示例

标签 :set

将值赋给变量。语法

<:set var="..." val="..."/>

属性

如何将此标签编译成 PHP 的示例

标签 :unset

从内存中删除变量。语法

<:unset var="..."/>

属性

如何将此标签编译成 PHP 的示例

标签 :while

创建WHILE循环。语法

<:while test="...">
    ...
</:while>

属性

如何将此标签编译成 PHP 的示例

标签 :break

中断FOR/FOR EACH/WHILE语句循环。语法

<:break/>

如何将此标签编译成 PHP 的示例

标签 :continue

在FOR/FOR EACH/WHILE语句循环中继续到下一个步骤。语法

<:continue/>

如何将此标签编译成 PHP 的示例