Skip to content

Instantly share code, notes, and snippets.

@wastemobile
Forked from brendo/EventTutorial.md
Created August 18, 2012 06:31
Show Gist options
  • Save wastemobile/3384871 to your computer and use it in GitHub Desktop.
Save wastemobile/3384871 to your computer and use it in GitHub Desktop.
Symphony Events: A Detailed Look

表單(Forms)是網站互動最基本的元件,使用者填表後送出(submit),主機會進行一些動作(action),儲存了資料,並將結果呈現給使用者。 Symphony CMS 提供了事件(Events)這個功能來進行表單處理。

這份教學假設您已經熟悉 Symphony CMS 的使用 (萬一不是, 這裡有對 Symphony CMS 最基本的介紹) ,而且還要能夠寫一點 PHP 程式碼。教學中觸及 Symphony CMS Events 比較少被人談到的進階功能,包含了事件的優先權、事件的串接,也談到一點點如何寫一個自製的事件。

開始

劇本概觀

我們的客戶需要讓用戶提交他們剛買的新車,主要是提供新車的資料,但也需要紀錄從哪裡買到這輛車的資訊。因此這份表單必須一次更新 Symphony 的兩個內容表(Sections):車,與經銷商。

設定

我已經建立了下列的測試內容表,為了教學起見,資料儘量維持簡要。

車輛資訊

Make : Text Input
Year Model : Select Box [2008-2011]
Manufacturer : Text Input

經銷商

Name | Text Input
Suburb | Text Input

現在讓我們建立兩個事件,每一份內容資料表對應一個事件:Create CarCreate Dealer。接著建立一個名為 New Car 的頁面,將兩個事件都附加上去(直接拷貝 Events 下方提供的範例表單就行了)。

先離題一下,來瞭解 load__trigger

使用編輯器(Sublime Text 2)打開事件檔案瞧瞧。這裡要看兩個重要功能:load__triggerload 函式像是個「條件(condition)」,在每一個附加了該事件的頁面載入時,檢查是否應該呼叫 __trigger 函式,讓實際的事件邏輯得以運行。對所有 Symphony 預設的事件來說,這個 load 檢查 action 是否存在(例如: isset($_POST['action']['create-car']) ,如果表單存在,action 的對象設定錯誤,依然不會成功運行);如果有正確的 action 設定,系統就會呼叫 __trigger 函式。這樣的運作邏輯,讓開發者獲得更多可能:「事件不一定只能用表單提交觸發」。你可以立即檢查 $_SESSION 中特定的值,然後進行 x 的動作,或是當用戶來自特定 IP 位置範圍時,將他們轉到特定的網域,或許多語系功能就能這麼進行。

回來繼續教程

由於兩個內容資料表的欄位都是唯一(沒有重複名稱的欄位),因此我們可以只使用一個 form,一股腦把兩個事件的資料通通擺進去,再將其中一個(Creater Dealer)的 Submit type 設定成 hidden,這樣用戶就只會看到一個「提交」按鍵。當用戶按下提交,實際上一次送出了兩個事件,分別在兩個內容資料表中各建立了一筆資料。

但事情沒有這麼簡單。當欄位名稱重複時,儲存的結果會變得不可預期,有時這個表單成功、另一個不會,有時結果又相反。

事情必須乾淨解決。

不重複的欄位名稱

在每一個專案中,要求所有內容資料表的欄位名稱都不重複,想起來有些不切實際。為了預防這種狀況發生,我們可以在寫表單時麻煩一點,在 name= 設定時加上資料表的前綴,例如把 <select name="fields[year]"> 變成 <select name="create-car[fields][year]"> 或是將 <input name="fields[name]" type="text" /> 寫成 <input name="create-dealer[fields][name]" type="text" />

還有另外兩處需要改動,將事件以程式編輯器打開(我有說要寫一點點 PHP 程式碼,對吧?),找到 load 函式的位置,加上 $this->post = $_POST; ,就像這樣:(ps.有兩個事件,要改哪一個?)

public function load(){
	$this->post = $_POST;
	if(isset($_POST['action'][self::ROOTELEMENT])) return $this->__trigger();
}

接著再把 __trigger() 函式改成像下面這樣:(ps.有兩個事件,要改哪一個?)

protected function __trigger(){
	unset($_POST['fields']);
	$_POST['fields'] = $this->post[self::ROOTELEMENT]['fields'];

	include(TOOLKIT . '/events/event.section.php');
	return $result;
}

我們到底做了什麼?在 load 函式部分,我們儲存了 $_POST 陣列,留著晚點要用。記得 load 會在所有事件實際執行前先被呼叫,所以每一個應該要被執行的事件,都會有一份完整的 $_POST 陣列可供利用了。如果 __trigger() 被呼叫,我們就拿精準指定到當下事件的陣列資料,覆蓋掉 $_POST['fields'] 陣列。那什麼是 self::ROOTELEMENT?當世界被儲存時,一個 ROOTELEMENT 常數(constant)被建立來處理事件名稱,它是 Symphony 回傳事件 XML 時會使用的根節點名稱。使用這個常數的好處,就是當你改動事件名稱時,程式只需要修改一個地方。

當我們進行了上面的改動,還要記得去改每一個事件中 allowEditorToParse 函式的回傳值,從 true 改成 false。否則下一次你或其他有權限修改程式的開發者,在後台重新儲存一次事件時,我們的改動又會被系統預設值覆蓋掉。

第一個限制(不重複欄位名稱)已經解決了。現在只要在多個事件送出欄位前加上儲存對象 section 的前綴(名稱),使用相同的欄位名稱也不會發生不可預料的錯誤了。

ps. 但最好記得哪些事件被修改過了,而且未來都必須直接開啓編輯器修改才行(包含異動了欄位設定)。

我希望只有當 'x' 成功執行之後,'y' 才會被執行!

另一件事,或許對前面的範例不太重要,就是事件執行的順序。所有的事件都有 priority (優先權),原始設定是根據 low、normal 與 high 三個值來決定事件執行的順序。預設的事件通通都是 normal,相同優先權的事件會根據「英文字母」依序執行。下一段教程中,讓我們添加一個叫做 'Related Cars' 的 Select Box Link 欄位,連結到車輛資訊的 'Make' 欄位。

我們希望用戶在建立新車與經銷商資訊時,車輛資訊是與經銷商連結在一起的。因此修改事件的優先權,確保它們執行的順序正確。(其實在這個範例中,Create Dealer 字母序排在 Create Car 之後,因此預設會在 Create Car 事件完成後才會執行。)我們需要再次開啓編輯器來修改 Create Car 事件,加入下面段落:

public function priority(){
	return self::kHIGH;
}

現在的流程總是假想車輛資訊會成功建立,但實際狀況可能不是這樣。用戶可能輸入錯誤的資料,顯示了一些錯誤訊息,但同時執行的儲存經銷商動作依然會運作,就造成沒存成一筆新車資料,卻還是建立了一筆經銷商資料。在這種狀況,我們希望能在經銷商資料被建立之前,就判讀事件回傳的訊息。因此,讓我們先移除之前的隱藏提交欄位 (<input name="action[create-dealer]" type="hidden" value="Submit" />)。在 Create Car 事件回傳 return $result 之前,我們要檢查事件提供的 XML 結果,我們要找的是 'success',如果找到了,就要複製一份我們的隱藏提交欄位到 $_POST 資料中,讓 Create Dealer 事件能夠順利驅動。因為 Select Box Link 使用系統的 Entry ID 來關聯紀錄,所以我們把新車的 entry id 加到 Create Dealer 事件的 $_POST 陣列中,就能讓兩筆資料建立正確的關聯。

// Check that this event returned successfully, if it did,
// inject the Create Dealer event and Car's entry ID
if($result->getAttribute('result') == "success") {
	$_POST['action']['create-dealer'] = 'Submit';
	$_POST['create-dealer']['fields']['related-car'] = $result->getAttribute('id');
}

return $result;

哇!我們順利讓兩個事件串接在一起,第二個事件只會在第一個事件成功執行之後才會被驅動。

可不可以只執行 'y' 事件?

還是有另外一種狀況可能發生:用戶輸入了正確的新車資訊,但經銷商資訊打錯了,上面的修改無法預防這種狀況,因為當 Create Car 已經順利執行,系統儲存了資料後又驅動 Create Dealer 事件,卻沒想到後面的事件執行失敗。

有兩種方法可以修正這個問題,很幸運的是,它們都只需要操作 XSLT 而不用碰 PHP。第一種選擇是重新呈現完整的表單(包含已經成功的建立新車表單欄位),但加入一個隱藏 id 欄位,這在 Symphony 事件操作中,就是修改現有的資料,而不會重新建立一筆。這方法的好處是讓用戶再看到一次建立新車欄位,還可以同時修改資料。第二種選擇是不呈現所有欄位,而只秀出那些與建立經銷商事件相關的欄位。

Symphony 提供了自動產生的 post-values,以及對應的事件回饋 XML。(每一個新建事件頁面最下方都有這些資訊。)

<create-car id="205" result="success" type="created">
    <message>Entry created successfully.</message>
    <post-values>
        <manufacturer>Nissan</manufacturer>
        <name>Pulsar</name>
        <year>2008</year>
    </post-values>
</create-car>
<create-dealer id="206" result="success" type="created">
    <message>Entry created successfully.</message>
    <post-values>
        <name>Tom Jones</name>
        <suburb>Burleigh Heads</suburb>
        <related-car>205</related-car>
    </post-values>
</create-dealer>

上面的範例,兩個表單都儲存成功了。但如果有問題發生的話...(而且必然是第二個事件的錯誤,記得嗎?第一個事件如果出錯,錯誤訊息就會呈現,第二個事件也不會被驅動。

<create-car id="208" result="success" type="created">
    <message>Entry created successfully.</message>
    <post-values>
        <manufacturer>Nissan</manufacturer>
        <name>Pulsar</name>
        <year>2008</year>
    </post-values>
</create-car>
<create-dealer result="error">
    <message>Entry encountered errors when saving.</message>
    <suburb label="Suburb" type="missing" message="'Suburb' is a required field." />
    <post-values>
        <name>Tom Jones</name>
        <related-car>208</related-car>
    </post-values>
</create-dealer>

在附加的範例 XSL 中,我有呈現當新車儲存成功、建立經銷商卻失敗的匯總訊息,但是藉著事件 XML 的資訊,你可以用 XSLT 自由處理前端要呈現給用戶的訊息,以及後續處理方式。

Wrap up

Well I hope that has been helpful and given you a little bit of insight into some of the more mysterious aspects of Events. If this all seems a little too difficult, I suggest checking out the Event Ex extension, it's quite old, but is maintained by Nick Dunn, who also happens to like juggling fire in his spare time.

關於自製事件

Symphony 事件很棒的一點,就是它其實只是你的頁面組版前被呼叫的一小段 PHP 碼,因此幾乎可以做到任何你想做的事(那不就是要寫 PHP code 嗎...XD)。讓事情簡單一點,我們假設下面的一小段劇本:有一個前端的按鍵可以按,每次按下去就替欄位依序添值到上限 10,當到達上限的時候,我們會寄出一封電子郵件通知給某人。所以我們寫一個完整的自製事件:

設定

這是我的測試資料

Counter

Count : Text Input
Max : Text Input
Email : Text Input
Email Response : Text Input

我建立一筆資料,設定起算值為 1,上限為 10,加入我的電子郵件,先讓回應訊息留白。之後我建立一個名為 Increment 的事件,再建立一個包含下列 XSL 的範例頁面(裡面的 213 是我的 Symphony 中這筆資料的系統 id,你的系統呈現的應該不同)。

<form method="post">
  <label>Count
    <input name="fields[count]" type="text" />
  </label>
  <input type="hidden" name="id" value="213" />
  <input name="action[increment]" type="submit" value="Submit" />
</form>

我們的自動增值事件

現在我們跳到 eventincrement Class(在 workspace/events/event.increment.php 裡面)。記得第一件事:將 allowEditorToParse 函式的回傳值改成 false,然後移到 __trigger() 函式並移除 include,接著我們再把 $result 改成空白的 XMLElement instance:

protected function __trigger(){
	$result = new XMLElement(self::ROOTELEMENT);

	return $result;
}

當你按下提交,應該會在我們的 Event XML 得到一個空白回應,<increment />。後面的教程都寫在 the __trigger() function 檔案裡了,有足夠的附註能指引你完成這自製事件的後續程序。

有幾個比較重要的注意事項,例如確保用戶的輸入是 經過過濾的(sanitized) 以及檢查 資料正確(data is correct) 還需要額外的學習,但希望這文章能讓你了解建立自製事件,其實就是如何讀取用戶的資料輸入、更新資料,以及呈現結果,這些最基本的事務。

最後一點,記得我們都修改了 allowEditorToParse 成為 false 嗎?

One last touch, remember how we removed set allowEditorToParse to false? This will now show the Event's documentation() return value when you click on the Event in the backend. By default, Symphony generates this documentation to the default form needed to populate the fields of the Event and may include some additional information about the filters attached to your event. Because our event is custom, this information is irrelevant and it's considered best practice for you to update this to be a little more descriptive. It's helpful to your future self, who jumps back onto the project in 6 months and can't quite remember how the event works, and to whoever else may use your event (particularly if it's part of an Extension). The documentation() function returns either a string of HTML, or a XMLElement object

And that's that!

That's the end of this long tutorial folks, hopefully that gives you a little more insight into Events and has given you some ideas about how you can use them to deliver a rich, interactive experience on your Symphony website.

References

<?php
protected function __trigger(){
$result = new XMLElement(self::ROOTELEMENT);
$entry_manager = new EntryManager(Symphony::Engine());
$field_manager = $entry_manager->fieldManager;
$status = Field::__OK__;
// Check that we have an `$entry_id` set otherwise fail
$entry_id = (is_numeric($_POST['id'])) ? $_POST['id'] : null;
if(is_null($entry_id)) {
$result->setAttribute('result', 'error');
$result->appendChild(new XMLElement(
'message', 'No Entry ID specified'
));
return $result;
}
// Retrieve our current Entry using the EntryManager
// EntryManager returns an array of entries, so we'll want the first
// one using `current()`.
$entry = $entry_manager->fetch($entry_id);
$entry = current($entry);
// Get all the entry's data, which is an associative array of field ID => data
$entry_data = $entry->getData();
// Get a Field instance for the `count` field as we need to add data to it
$count_field = $field_manager->fetch(
$field_manager->fetchFieldIDFromElementName('count')
);
// We are using the `max` for readonly work, so just get the Field ID
$max_field_id = $field_manager->fetchFieldIDFromElementName('max');
// Get the current entry data
$current_count = $entry_data[$count_field->get('id')]['value'];
$max_count = $entry_data[$max_field_id]['value'];
// 1. Check that `count` is less than our `max`, otherwise return
if($current_count >= $max_count) {
$result->setAttribute('result', 'error');
$result->appendChild(new XMLElement(
'message', 'Count has reached it\'s max'
));
return $result;
}
// 2. If `count` is less, increment `count` by 1
$new_count = $current_count + 1;
$entry->setData(
$count_field->get('id'),
// I'm deliberately ignoring the `$status` result here for simplicity
// and just assuming everything will be ok. This means that if your
// data is coming from the user you should be running it against
// `Field->checkPostFieldData` first
$count_field->processRawFieldData($new_count, $status)
);
// 3. If `count` now equals our `max`, send email an email
if($new_count == $max_count) {
// Get our Email field ID (readonly)
$email_field_id = $field_manager->fetchFieldIDFromElementName('email');
// Get our Email Response Field (writing)
$email_response_field = $field_manager->fetch(
$field_manager->fetchFieldIDFromElementName('email-response')
);
// Create our Email instance from the Core Email API
$email = Email::create();
$email_sent = true;
// Try to send our email
// For more Core Email API information, check michael-e's guide
// https://github.com/michael-e/core-email-api-docs/blob/master/developer-documentation.markdown
try{
$email->recipients = array(
$entry_data[$email_field_id]['value']
);
$email->text_plain = 'Surprise, our counter reached ' . $new_count . ', now you can dance!';
$email->subject = 'Our Counter reached ' . $new_count . '!';
$email->send();
}
// If something goes wrong, lets save the Exception to the Email Response
catch(EmailException $ex) {
$email_sent = false;
$entry->setData(
$email_response_field->get('id'),
$email_response_field->processRawFieldData($ex->getMessage(), $status)
);
}
// Everything went swell, save 'Email sent' to our Email Response field
if($email_sent) {
$entry->setData(
$email_response_field->get('id'),
$email_response_field->processRawFieldData('Email sent', $status)
);
}
}
// Update our Entry record, again keeping this very simple and not checking for errors
if($entry->commit()) {
$result->setAttribute('result', 'success');
$result->appendChild(new XMLElement(
'message', 'Count is at ' . $new_count
));
}
else {
$result->setAttribute('result', 'error');
}
// This line is essential for the Event XML to appear in your `?debug`
return $result;
}
?>
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml"
doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
omit-xml-declaration="yes"
encoding="UTF-8"
indent="yes" />
<xsl:variable name='create-car' select='/data/events/create-car' />
<xsl:variable name='create-dealer' select='/data/events/create-dealer' />
<xsl:template match="/">
<h1><xsl:value-of select="$page-title"/></h1>
<form method="post">
<xsl:choose>
<xsl:when test='$create-car[@result = "success"]'>
<xsl:value-of select='$create-car/message' />
<dl>
<dt>Manufacturer</dt>
<dd>
<xsl:value-of select='$create-car/manufacturer' />
</dd>
<dt>Car - Model</dt>
<dd>
<xsl:value-of select='concat($create-car/post-values/car, " ", $create-car/post-values/year)' />
</dd>
</dl>
</xsl:when>
<xsl:otherwise>
<fieldset>
<legend>Car</legend>
<label>Manufacturer
<input name="create-car[fields][manufacturer]" type="text" />
</label>
<label>Make
<input name="create-car[fields][name]" type="text" />
</label>
<label>Year
<select name="create-car[fields][year]">
<option value="2008">2008</option>
<option value="2009">2009</option>
<option value="2010">2010</option>
<option value="2011">2011</option>
</select>
</label>
</fieldset>
</xsl:otherwise>
</xsl:choose>
<fieldset>
<legend>Dealer</legend>
<label>Name
<input name="create-dealer[fields][name]" type="text" value='{$create-dealer/post-values/name}' />
</label>
<label>Suburb
<input name="create-dealer[fields][suburb]" type="text" value='{$create-dealer/post-values/suburb}' />
</label>
<xsl:if test='$create-car[@result = "success"]'>
<input type='hidden' name="create-dealer[fields][related-car]" value='{$create-car/@id}' />
</xsl:if>
</fieldset>
<xsl:choose>
<xsl:when test='$create-car[@result = "success"] and $create-dealer[@result != "success"]'>
<input name="action[create-dealer]" type="submit" value="Submit" />
</xsl:when>
<xsl:otherwise>
<input name="action[create-car]" type="submit" value="Submit" />
</xsl:otherwise>
</xsl:choose>
</form>
<xsl:apply-templates select='$create-car' mode='event' />
<xsl:apply-templates select='$create-dealer' mode='event' />
</xsl:template>
<xsl:template match='*' mode='event'>
<div>
<h3>
<xsl:value-of select='local-name(.)' />
<xsl:choose>
<xsl:when test='@result = "error"'>
<span style='color:red'> error</span>
</xsl:when>
<xsl:otherwise>
<span style='color:green'> success</span>
</xsl:otherwise>
</xsl:choose>
</h3>
<xsl:copy-of select='message' />
<ul>
<xsl:apply-templates select='*[@message]' />
</ul>
</div>
</xsl:template>
<xsl:template match='*[@message]'>
<li><xsl:value-of select='@message' /></li>
</xsl:template>
</xsl:stylesheet>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment