模板让网络运转起来。数据和结构合成内容。对于开发者来说,这是我们最酷的超能力——获取一些数据,然后让它为我们工作,以我们需要的任何形式呈现。一个对象数组可以变成一个表格、一张卡片列表、一个图表,或者任何我们认为对用户最有用的东西。无论数据是我们自己的Markdown文件中的博客文章,还是最新的全球汇率,标记和最终的用户体验都取决于我们前端开发者。
PHP是一种用于模板的强大语言,它提供了许多将数据与标记合并的方法。让我们来看一个使用数据构建HTML表单的例子。
想立即上手?跳转到实现部分。
在PHP中,我们可以将变量内联到使用双引号的字符串文字中,所以如果我们有一个变量$name = 'world'
,我们可以写echo "Hello, {$name}"
,它会打印预期的“Hello, world”。对于更复杂的模板,我们可以始终连接字符串,例如:echo "Hello, " . $name . "."
。
对于老程序员来说,还有printf("Hello, %s", $name)
。对于多行字符串,可以使用Heredoc(类似于Hello, = $name ?>
)。所有这些选项都很棒,但是当需要很多内联逻辑时,事情可能会变得混乱。如果我们需要构建复合HTML字符串,例如表单或导航,那么复杂性可能是无限的,因为HTML元素可以相互嵌套。
在我们继续做我们想做的事情之前,值得花一分钟时间考虑一下我们不想做的事情。考虑一下WordPress核心代码class-walker-nav-menu.php
第170-270行节选:
<?php // class-walker-nav-menu.php // ... $output .= $indent . '<li' . $id . $class_names . '?>'; // ... $item_output = $args->before; $item_output .= '<a .="">'; $item_output .= $args->link_before . $title . $args->link_after; $item_output .= '</a>'; $item_output .= $args->after; // ... $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args ); // ... $output .= "{$n}";
为了构建导航,在这个函数中,我们使用一个变量$output
,这是一个很长的字符串,我们不断向其中添加内容。这种类型的代码具有非常具体和有限的操作顺序。如果我们想向<a></a>
添加一个属性,我们必须在它运行之前访问$attributes
。如果我们想在<a></a>
中可选地嵌套一个<span></span>
或一个<img alt="使用Domdocument在PHP中构建表格" >
,我们需要编写一个全新的代码块,用大约4-10行新代码替换第7行的中间部分,具体取决于我们想要添加的内容。现在想象一下,您需要可选地添加<span></span>
,然后可选地添加<img alt="使用Domdocument在PHP中构建表格" >
,或者在<span></span>
内部或之后添加。这本身就是三个if语句,使代码更难以阅读。当像这样连接字符串时,很容易最终得到字符串意大利面,这说起来很有趣,但维护起来却很痛苦。
问题的实质是,当我们试图推断HTML元素时,我们不是在考虑字符串。浏览器恰好消耗字符串,PHP输出字符串。但是我们的心理模型更像是DOM——元素排列成树状结构,每个节点都有许多潜在的属性、特性和子节点。
如果有一种结构化、表达性强的构建树的方法,那不是很棒吗?
输入……
PHP 5向其非严格类型化类型的阵容中添加了DOM模块。它的主要入口点是DOMDocument类,它有意与Web API的JavaScript DOM类似。如果您曾经使用过document.createElement
,或者对于我们某些年龄段的人来说,使用过jQuery的$('<p>Hi there!</p>')
语法,这可能会感觉非常熟悉。我们首先初始化一个新的DOMDocument:
$dom = new DOMDocument();
现在我们可以向其中添加一个DOMElement:
$p = $dom->createElement('p');
字符串'p'表示我们想要的元素类型,因此其他有效的字符串可以是'div'、'img'等。
一旦我们有了元素,我们就可以设置它的属性:
$p->setAttribute('class', 'headline');
我们可以向其中添加子节点:
$span = $dom->createElement('span', 'This is a headline'); // 第二个参数填充元素的textContent $p->appendChild($span);
最后,一次性获取完整的HTML字符串:
$dom->appendChild($p); $htmlString = $dom->saveHTML(); echo $htmlString;
请注意,这种编码风格使我们的代码按照我们的心理模型进行组织——文档包含元素;元素可以具有任意数量的属性;元素相互嵌套而无需了解彼此的任何信息。“HTML只是一个字符串”的部分在最后出现,一旦我们的结构到位。“文档”在这里与实际的DOM略有不同,因为它不需要表示整个文档,而只是一个HTML块。事实上,如果您需要创建两个相似的元素,您可以使用saveHTML()
保存一个HTML字符串,进一步修改DOM“文档”,然后通过再次调用saveHTML()
保存一个新的HTML字符串。
假设我们需要使用来自CRM提供商的数据和我们自己的标记在服务器上构建一个表单。来自CRM的API响应如下所示:
{ "submit_button_label": "Submit now!", "fields": [ { "id": "first-name", "type": "text", "label": "First name", "required": true, "validation_message": "First name is required.", "max_length": 30 }, { "id": "category", "type": "multiple_choice", "label": "Choose all categories that apply", "required": false, "field_metadata": { "multi_select": true, "values": [ { "value": "travel", "label": "Travel" }, { "value": "marketing", "label": "Marketing" } ] } } ] }
此示例不使用任何特定CRM的确切数据结构,但它具有相当的代表性。
并且假设我们希望我们的标记如下所示:
<label> <input type="text" placeholder=" " id="first-name" required maxlength="30"> First name <em>First name is required.</em> </label> <label> <fieldset> <div> <input type="checkbox" value="travel" id="category-travel" name="category"> <label for="category-travel">Travel</label> </div> <div> <input type="checkbox" value="marketing" id="category-marketing" name="category"> <label for="category-marketing">Marketing</label> </div> </fieldset> Choose all categories that apply </label>
占位符“ ”是什么?这是一个小技巧,它允许我们在CSS中跟踪字段是否为空,而无需使用JavaScript。只要输入为空,它就匹配input:placeholder-shown
,但用户不会看到任何可见的占位符文本。这正是我们控制标记时可以做的事情!
既然我们知道我们想要的结果是什么,那么这就是游戏计划:
因此,让我们构建我们的流程并解决一些技术问题:
<?php function renderForm ($endpoint) { // 从API获取数据并将其转换为PHP对象 $formResult = file_get_contents($endpoint); $formContent = json_decode($formResult); $formFields = $formContent->fields; // 开始构建DOM $dom = new DOMDocument(); $form = $dom->createElement('form'); // 迭代字段并构建每个字段 foreach ($formFields as $field) { // TODO:对字段数据执行某些操作 } // 获取HTML输出 $dom->appendChild($form); $htmlString = $dom->saveHTML(); echo $htmlString; }
到目前为止,我们已经获取并解析了数据,初始化了我们的DOMDocument并回显了它的输出。我们想对每个字段做什么?首先,让我们构建容器元素,在我们的示例中,它应该是<label></label>
,以及所有字段类型通用的标签:
<?php // ... // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $label = null; // 如果设置了标签,则添加`<span>` if ($field->label) { $label = $dom->createElement('span', $field->label); $label->setAttribute('class', 'label'); } // 将标签添加到`<label>` if ($label) $element->appendChild($label); }</label></span>
由于我们处于循环中,并且PHP不会在循环中作用域变量,因此我们在每次迭代时都会重置$label
元素。然后,如果字段有标签,我们构建该元素。最后,我们将其附加到容器元素。
请注意,我们使用setAttribute
方法设置类。与Web API不同,不幸的是,没有对类列表的特殊处理。它们只是另一个属性。如果我们有一些非常复杂的类逻辑,因为它是Just PHP™,我们可以创建一个数组然后将其内联:$label->setAttribute('class', implode($labelClassList))
。
由于我们知道API只会返回特定字段类型,因此我们可以切换类型并为每个类型编写特定代码:
<?php // ... // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $input = null; $label = null; // 如果设置了标签,则添加`<span>` // ... // 构建输入元素 switch ($field->type) { case 'text': case 'email': case 'telephone': $input = $dom->createElement('input'); $input->setAttribute('placeholder', ' '); if ($field->type === 'email') $input->setAttribute('type', 'email'); if ($field->type === 'telephone') $input->setAttribute('type', 'tel'); break; } }</span>
现在让我们处理文本区域、单个复选框和隐藏字段:
<?php // ... // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $input = null; $label = null; // 如果设置了标签,则添加`<span>` // ... // 构建输入元素 switch ($field->type) { //... case 'text_area': $input = $dom->createElement('textarea'); $input->setAttribute('placeholder', ' '); if ($rows = $field->field_metadata->rows) $input->setAttribute('rows', $rows); break; case 'checkbox': $element->setAttribute('class', 'field single-checkbox'); $input = $dom->createElement('input'); $input->setAttribute('type', 'checkbox'); if ($field->field_metadata->initially_checked === true) $input->setAttribute('checked', 'checked'); break; case 'hidden': $input = $dom->createElement('input'); $input->setAttribute('type', 'hidden'); $input->setAttribute('value', $field->field_metadata->value); $element->setAttribute('hidden', 'hidden'); $element->setAttribute('style', 'display: none;'); $label->textContent = ''; break; } }</span>
注意我们对复选框和隐藏情况所做的一些新操作?我们不仅创建了<input>
元素;我们还在更改容器<label></label>
元素!对于单个复选框字段,我们想修改容器的类,以便我们可以水平对齐复选框和标签;隐藏的<label></label>
的容器也应该完全隐藏。
现在,如果我们只是连接字符串,那么此时就无法更改。我们必须在块的顶部添加一堆关于元素类型及其元数据的if语句。或者,也许更糟糕的是,我们更早地开始switch,然后在每个分支之间复制粘贴大量公共代码。
而这就是使用像DOMDocument这样的构建器的真正好处——在我们点击saveHTML()
之前,一切仍然是可编辑的,一切仍然是有结构的。
让我们添加<select></select>
元素的逻辑:
<?php // ... // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $input = null; $label = null; // 如果设置了标签,则添加`<span>` // ... // 构建输入元素 switch ($field->type) { //... case 'select': $element->setAttribute('class', 'field select'); $input = $dom->createElement('select'); $input->setAttribute('required', 'required'); if ($field->field_metadata->multi_select === true) $input->setAttribute('multiple', 'multiple'); $options = []; // 跟踪是否存在预选选项 $optionSelected = false; foreach ($field->field_metadata->values as $value) { $option = $dom->createElement('option', htmlspecialchars($value->label)); // 如果没有值,则跳过 if (!$value->value) continue; // 设置预选选项 if ($value->selected === true) { $option->setAttribute('selected', 'selected'); $optionSelected = true; } $option->setAttribute('value', $value->value); $options[] = $option; } // 如果没有预选选项,则构建一个空的占位符选项 if ($optionSelected === false) { $emptyOption = $dom->createElement('option'); // 将选项设置为隐藏、禁用和选中 foreach (['hidden', 'disabled', 'selected'] as $attribute) $emptyOption->setAttribute($attribute, $attribute); $input->appendChild($emptyOption); } // 将数组中的选项添加到`<select>` foreach ($options as $option) { $input->appendChild($option); } break; } }</select></span>
好的,这里有很多事情要做,但是底层逻辑是一样的。在设置外部<label></label>
之后,我们创建一个<option></option>
数组,将其附加到其中。
我们在这里也做了一些<select></select>
特有的技巧:如果没有预选选项,我们会添加一个已经选中的空占位符选项,但用户无法选中它。目标是使用CSS将我们的<label></label>
作为“占位符”,但这项技术可用于各种设计。通过在附加其他选项之前将其附加到$input
,我们确保它是标记中的第一个选项。
现在让我们处理<fieldset></fieldset>
中的单选按钮和复选框:
<?php // ... // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $input = null; $label = null; // 如果设置了标签,则添加`<span>` // ... // 构建输入元素 switch ($field->type) { // ... case 'multiple_choice': $choiceType = $field->field_metadata->multi_select === true ? 'checkbox' : 'radio'; $element->setAttribute('class', "field {$choiceType}-group"); $input = $dom->createElement('fieldset'); // 为fieldset中的每个选项构建一个选择`<div>` foreach ($field->field_metadata->values as $choiceValue) { $choiceField = $dom->createElement('div'); $choiceField->setAttribute('class', 'choice'); // 使用字段ID 选择ID设置唯一ID $choiceID = "{$field->id}-{$choiceValue->value}"; // 构建`<input>`元素 $choice = $dom->createElement('input'); $choice->setAttribute('type', $choiceType); $choice->setAttribute('value', $choiceValue->value); $choice->setAttribute('id', $choiceID); $choice->setAttribute('name', $field->id); $choiceField->appendChild($choice); // 构建`<label>`元素 $choiceLabel = $dom->createElement('label', $choiceValue->label); $choiceLabel->setAttribute('for', $choiceID); $choiceField->appendChild($choiceLabel); $input->appendChild($choiceField); } break; } } <p>所以,首先我们确定字段集应该是复选框还是单选按钮。然后我们相应地设置容器类,并构建<code><fieldset></fieldset></code>。之后,我们迭代可用的选项并为每个选项构建一个<code><div>,其中包含一个<code><input></code>和一个<code><label></label></code>。请注意,我们使用常规PHP字符串插值在第21行设置容器类,并在第30行为每个选项创建唯一ID。 <h3>片段</h3> <p>我们必须添加的最后一种类型比看起来稍微复杂一些。许多表单包含说明字段,这些字段不是输入,而只是我们需要在其他字段之间打印的一些HTML。</p> <p>我们需要使用另一个DOMDocument方法<code>createDocumentFragment()</code>。这允许我们添加任意HTML而无需使用DOM结构:</p> <pre class="brush:php;toolbar:false"><?php // ... // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $input = null; $label = null; // 如果设置了标签,则添加`<span>` // ... // 构建输入元素 switch ($field->type) { //... case 'instruction': $element->setAttribute('class', 'field text'); $fragment = $dom->createDocumentFragment(); $fragment->appendXML($field->text); $input = $dom->createElement('p'); $input->appendChild($fragment); break; } }</span>
此时您可能想知道我们是如何得到一个名为$input
的对象的,它实际上表示一个静态<p></p>
元素。目标是为字段循环的每次迭代使用一个通用的变量名,因此最后我们可以始终使用$element->appendChild($input)
添加它,而不管实际的字段类型如何。所以,是的,命名事物很难。
我们正在使用的API为每个必需字段提供了单独的验证消息。如果出现提交错误,我们可以将错误与字段一起内联显示,而不是在底部显示通用的“糟糕,你的错”消息。
让我们将验证文本添加到每个元素:
<?php // ... // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $input = null; $label = null; $validation = null; // 如果设置了标签,则添加`<span>` // ... // 如果设置了验证消息,则添加`<em>` if (isset($field->validation_message)) { $validation = $dom->createElement('em'); $fragment = $dom->createDocumentFragment(); $fragment->appendXML($field->validation_message); $validation->appendChild($fragment); $validation->setAttribute('class', 'validation-message'); $validation->setAttribute('hidden', 'hidden'); // 最初隐藏,如果字段有错误,则将使用Javascript取消隐藏 } // 构建输入元素 switch ($field->type) { // ... } }</em></span>
这就是全部!无需处理字段类型逻辑——只需有条件地为每个字段构建一个元素。
那么在我们构建所有字段元素之后会发生什么?我们需要将$input
、$label
和$validation
对象添加到我们正在构建的DOM树中。我们还可以利用这个机会添加公共属性,例如required
。然后我们将添加提交按钮,在本API中它与字段是分开的。
<?php function renderForm ($endpoint) { // 从API获取数据并将其转换为PHP对象 // ... // 开始构建DOM $dom = new DOMDocument(); $form = $dom->createElement('form'); // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); // 重置输入值 $input = null; $label = null; $validation = null; // 如果设置了标签,则添加`<span>` // ... // 如果设置了验证消息,则添加`<em>` // ... // 构建输入元素 switch ($field->type) { // ... } // 添加输入元素 if ($input) { $input->setAttribute('id', $field->id); if ($field->required) $input->setAttribute('required', 'required'); if (isset($field->max_length)) $input->setAttribute('maxlength', $field->max_length); $element->appendChild($input); if ($label) $element->appendChild($label); if ($validation) $element->appendChild($validation); $form->appendChild($element); } } // 构建提交按钮 $submitButtonLabel = $formContent->submit_button_label; $submitButtonField = $dom->createElement('div'); $submitButtonField->setAttribute('class', 'field submit'); $submitButton = $dom->createElement('button', $submitButtonLabel); $submitButtonField->appendChild($submitButton); $form->appendChild($submitButtonField); // 获取HTML输出 $dom->appendChild($form); $htmlString = $dom->saveHTML(); echo $htmlString; }</em></span></label>
为什么我们要检查$input
是否为真?由于我们在循环顶部将其重置为null,并且只有在类型符合我们预期的switch case时才构建它,因此这确保我们不会意外地包含我们的代码无法正确处理的意外元素。
瞧,一个自定义HTML表单!
如您所知,许多表单构建器允许作者为字段设置行和列。例如,一行可能同时包含名字和姓氏字段,每个字段都在一个单独的50%宽度的列中。那么,您可能会问,我们该如何实现呢?当然,通过举例说明DOMDocument的循环友好性!
我们的API响应包含如下所示的网格数据:
{ "submit_button_label": "Submit now!", "fields": [ { "id": "first-name", "type": "text", "label": "First name", "required": true, "validation_message": "First name is required.", "max_length": 30, "row": 1, "column": 1 }, { "id": "category", "type": "multiple_choice", "label": "Choose all categories that apply", "required": false, "field_metadata": { "multi_select": true, "values": [ { "value": "travel", "label": "Travel" }, { "value": "marketing", "label": "Marketing" } ] }, "row": 2, "column": 1 } ] }
我们假设添加data-column
属性足以设置宽度,但每一行都需要它自己的元素(即没有CSS网格)。
在我们深入研究之前,让我们考虑一下为了添加行我们需要什么。基本逻辑是这样的:
现在,如果我们正在连接字符串,我们该怎么做?可能是在每次到达新行时添加一个类似于'
<div>。就像很有趣,但恰恰相反。那么,处理这个问题的结构化方法是什么?谢谢你的提问。首先,让我们在循环之前添加行跟踪,并构建一个额外的行容器元素。然后,我们将确保将每个容器<code>$element
附加到它的$rowElement
,而不是直接附加到$form
。
<?php function renderForm ($endpoint) { // 从API获取数据并将其转换为PHP对象 // ... // 开始构建DOM $dom = new DOMDocument(); $form = $dom->createElement('form'); // 初始化行跟踪 $row = 0; $rowElement = $dom->createElement('div'); $rowElement->setAttribute('class', 'field-row'); // 迭代字段并构建每个字段 foreach ($formFields as $field) { // 构建容器`<label>` $element = $dom->createElement('label'); $element->setAttribute('class', 'field'); $element->setAttribute('data-row', $field->row); $element->setAttribute('data-column', $field->column); // 将输入元素添加到行 if ($input) { // ... $rowElement->appendChild($element); $form->appendChild($rowElement); } } // ... }</label>
到目前为止,我们只是在字段周围添加了另一个<div>。让我们在循环内为每一行构建一个<em>新的</em>行元素:
<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false"><?php // ...
// 初始化行跟踪
$row = 0;
$rowElement = $dom->createElement('div');
$rowElement->setAttribute('class', 'field-row');
// 迭代字段并构建每个字段
foreach ($formFields as $field) {
// ...
// 如果我们到达新行,则创建一个新的$rowElement
if ($field->row > $row) {
$row = $field->row;
$rowElement = $dom->createElement('div');
$rowElement->setAttribute('class', 'field-row');
}
// 构建输入元素
switch ($field->type) {
// ...
// 将输入元素添加到行
if ($input) {
// ...
$rowElement->appendChild($element);
// 自动去重
$form->appendChild($rowElement);
}
}
}</pre><div class="contentsignin">登录后复制</div></div>
<p>我们只需要将<code>$rowElement
对象覆盖为一个新的DOM元素,PHP将其视为一个新的唯一对象。因此,在每次循环结束时,我们只需附加当前的$rowElement
——如果它与前一次迭代中的元素相同,则更新表单;如果它是一个新元素,则将其附加到末尾。
表单是面向对象模板的一个很好的用例。考虑到WordPress核心代码中的片段,可以认为嵌套菜单也是一个很好的用例。任何标记遵循复杂逻辑的任务都是这种方法的良好候选者。DOMDocument可以输出任何XML,因此您也可以使用它从帖子数据构建RSS feed。
这是我们表单的完整代码片段。随意将其调整为任何您发现自己正在处理的表单API。这是官方文档,它对于了解可用的API很有帮助。
我们甚至没有提到DOMDocument可以解析现有的HTML和XML。然后,您可以使用XPath API查找元素,这与document.querySelector
或Node.js上的cheerio有点类似。学习曲线有点陡峭,但它是一个非常强大的API,用于处理外部内容。
有趣的事实:以x结尾的Microsoft Office文件(例如.xlsx)是XML文件。不要告诉市场营销部门,但可以在服务器上解析Word文档并输出HTML。
最重要的是要记住,模板是一种超能力。能够为正确的情况构建正确的标记可能是获得出色用户体验的关键。
以上是使用Domdocument在PHP中构建表格的详细内容。更多信息请关注PHP中文网其他相关文章!