模板讓網絡運轉起來。數據和結構合成內容。對於開發者來說,這是我們最酷的超能力——獲取一些數據,然後讓它為我們工作,以我們需要的任何形式呈現。一個對像數組可以變成一個表格、一張卡片列表、一個圖表,或者任何我們認為對用戶最有用的東西。無論數據是我們自己的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的$('
Hi there!
$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> </label> <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> Choose all categories that apply
佔位符“ ”是什麼?這是一個小技巧,它允許我們在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><p>由於我們處於循環中,並且PHP不會在循環中作用域變量,因此我們在每次迭代時都會重置<code>$label</code>元素。然後,如果字段有標籤,我們構建該元素。最後,我們將其附加到容器元素。</p><p>請注意,我們使用<code>setAttribute</code>方法設置類。與Web API不同,不幸的是,沒有對類列表的特殊處理。它們只是另一個屬性。如果我們有一些非常複雜的類邏輯,因為它是Just PHP™,我們可以創建一個數組然後將其內聯: <code>$label->setAttribute('class', implode($labelClassList))</code> 。</p><h3>單個輸入</h3><p>由於我們知道API只會返回特定字段類型,因此我們可以切換類型並為每個類型編寫特定代碼:</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 '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><p>現在讓我們處理文本區域、單個複選框和隱藏字段:</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 '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><p>注意我們對複選框和隱藏情況所做的一些新操作?我們不僅創建了<code><input></code>元素;我們還在更改<em>容器</em><code><label></label></code>元素!對於單個複選框字段,我們想修改容器的類,以便我們可以水平對齊複選框和標籤;隱藏的<code><label></label></code>的容器也應該完全隱藏。</p><p>現在,如果我們只是連接字符串,那麼此時就無法更改。我們必須在塊的頂部添加一堆關於元素類型及其元數據的if語句。或者,也許更糟糕的是,我們更早地開始switch,然後在每個分支之間複製粘貼大量公共代碼。</p><p>而這就是使用像DOMDocument這樣的構建器的真正好處——在我們點擊<code>saveHTML()</code>之前,一切仍然是可編輯的,一切仍然是有結構的。</p><h3>嵌套循環元素</h3><p>讓我們添加<code><select></select></code>元素的邏輯:</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 '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><p>好的,這裡有很多事情要做,但是底層邏輯是一樣的。在設置外部<code><label></label></code>之後,我們創建一個<code><option></option></code>數組,將其附加到其中。</p><p>我們在這裡也做了一些<code><select></select></code>特有的技巧:如果沒有預選選項,我們會添加一個已經選中的空佔位符選項,但用戶無法選中它。目標是使用CSS將我們的<code><label></label></code>作為“佔位符”,但這項技術可用於各種設計。通過在附加其他選項之前將其附加到<code>$input</code> ,我們確保它是標記中的第一個選項。</p><p>現在讓我們處理<code><fieldset></fieldset></code>中的單選按鈕和復選框:</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 'multiple_choice': $choiceType = $field->field_metadata->multi_select === true ? 'checkbox' : 'radio'; $element->setAttribute('class', "field {$choiceType}-group"); $input = $dom->createElement('fieldset'); // 為fieldset中的每個選項構建一個選擇`</span> ` 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; } }</label><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><p>此時您可能想知道我們是如何得到一個名為<code>$input</code>的對象的,它實際上表示一個靜態<code><p>元素。目標是為字段循環的每次迭代使用一個通用的變量名,因此最後我們可以始終使用<code>$element->appendChild($input)</code>添加它,而不管實際的字段類型如何。所以,是的,命名事物很難。</p> <h3>驗證</h3> <p>我們正在使用的API為每個必需字段提供了單獨的驗證消息。如果出現提交錯誤,我們可以將錯誤與字段一起內聯顯示,而不是在底部顯示通用的“糟糕,你的錯”消息。</p> <p>讓我們將驗證文本添加到每個元素:</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; $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><p>這就是全部!無需處理字段類型邏輯——只需有條件地為每個字段構建一個元素。</p><h3>整合所有內容</h3><p>那麼在我們構建所有字段元素之後會發生什麼?我們需要將<code>$input</code> 、 <code>$label</code>和<code>$validation</code>對象添加到我們正在構建的DOM樹中。我們還可以利用這個機會添加公共屬性,例如<code>required</code> 。然後我們將添加提交按鈕,在本API中它與字段是分開的。</p><pre class="brush:php;toolbar:false"> <?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><p>為什麼我們要檢查<code>$input</code>是否為真?由於我們在循環頂部將其重置為null,並且只有在類型符合我們預期的switch case時才構建它,因此這確保我們不會意外地包含我們的代碼無法正確處理的意外元素。</p><p>瞧,一個自定義HTML表單!</p><h3>附加內容:行和列</h3><p>如您所知,許多表單構建器允許作者為字段設置行和列。例如,一行可能同時包含名字和姓氏字段,每個字段都在一個單獨的50%寬度的列中。那麼,您可能會問,我們該如何實現呢?當然,通過舉例說明DOMDocument的循環友好性!</p><p>我們的API響應包含如下所示的網格數據:</p><pre class="brush:php;toolbar:false"> { "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>'</div>
的字符串。這種“反向HTML字符串”對我來說總是非常令人困惑,所以我只能想像我的IDE是什麼感覺。最重要的是,由於瀏覽器自動關閉打開的標籤,一個簡單的錯字會導致無數嵌套的<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><p>到目前為止,我們只是在字段周圍添加了另一個<code><div>。讓我們在循環內為每一行構建一個<em>新的</em>行元素:<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); } } }
我們只需要將$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中文網其他相關文章!