Forms have been an integral part of any interactive site since the dawn of time — they promote interactivity and are usually the most common way users interact with a site. It's commonplace that when a form is submitted (input), the website will take 'action' and do something with the data (processing) and then provide a user with the result (output). Symphony provides this logic layer via events.
This tutorial is about customising Symphony events. You will learn about:
- event execution conditions (
load
and__trigger
) - field naming and the
$_POST
array - creating related entries in two or more sections at the same time, aka. event chaining
- event priority (execution order)
- entirely custom events
This tutorial assumes you have a basic understanding of how events work in Symphony (if not, this may be a good introduction) and are semi-comfortable writing some PHP code. The difficulty level progresses as we go through, but with any luck you'll be able to learn a thing or two :)
Our client requires a form that allows a user to submit some details about their new car purchase. The information is particular to the car, but there are a couple of questions about where they purchased it from, so the data needs to post to two sections in Symphony, Car and Dealers.
I've created the following test sections, kept really simple for the purposes of this tutorial.
Cars
Make Text Input Year Model Select Box [2008-2011] Manufacturer Text Input
Dealers
Name Text Input Suburb Text Input
Now let's create two events, one for each section. I've called mine Create Car
and Create Dealer
. We'll next create a page (I've called mine 'New Car') and attach our two events to this page.
To get us up and running quickly, let's copy the example form markup from the Create Car
event into our page XSL. This example form now allows us to create a new car entry in the Cars section when a user submits the form, but we also want to be able to create a new dealer entry from the same form so the experience is seamless. To do this, let's copy the field markup from the 'Create Dealer' event and paste it just above the current submit button. Instead of having two 'submit' buttons, lets change the Create Dealer
submit to be a hidden input field instead. This will just leave one Submit button, but to our events, there are actually two triggers on this page.
Let's open up our events in a text editor and take a look at something. There are two functions that are important here: load
and __trigger
.
The load
function acts as a condition — it runs on every page load on which the event is attached, and checks to see if it should call the __trigger
function (which is where the actual event logic takes place).
For all of Symphony's default events, this load
checks that the action exists (e.g. isset($_POST['action']['create-car'])
), and if it does, it proceeds to call the __trigger
function.
This opens up a number of possibilities for a developer as your events don't necessarily have to be triggered by a form submit ($_POST
). For instance you could check if a particular value was in the $_SESSION
array and then do x
, or if the user came from a specific IP address range redirect them to a particular domain, perhaps for localisation.
Just before I made a right turn, we had created and attached two events and attached them to our page. We finished setting up our page XSL with one form containing all of our fields, with two actions. You'll be pleasantly surprised that if you submit this form, a new entry will be created in both the Cars and Dealers sections. Victory!
Or not quite... there are a couple of caveats here that I haven't mentioned.
This worked well in this case because our field names were unique to each section, so there was no confusion as to what data should go where. In fact, if we had two Name
fields, the same input field would have populated both fields in both section (only the last instance of Name
in the $_POST
array would have been used). Depending on how you set up your section, you may have found that Create Dealer
executed, even though there was an error with the other event, or vice versa, Create Car
worked fine, Create Dealer
did not.
Let's tackle these one at a time.
Unique field names are never going to be a realistic situation for every project, so to prevent this from happening, let's prefix our fields by the event handles create-car
and create-dealer
:
from <select name="fields[year]">
to <select name="create-car[fields][year]">
from <input name="fields[name]" type="text" />
to <input name="create-dealer[fields][name]" type="text" />
.
There's another change we need to make now to our event classes. Open these up in your text editor again (hey I did say we'd be getting dirty with some PHP ;) and find the load
function and add $this->post = $_POST;
:
public function load(){
$this->post = $_POST;
if(isset($_POST['action'][self::ROOTELEMENT])) return $this->__trigger();
}
Now make your __trigger()
function look like this:
protected function __trigger(){
unset($_POST['fields']);
$_POST['fields'] = $this->post[self::ROOTELEMENT]['fields'];
include(TOOLKIT . '/events/event.section.php');
return $result;
}
What have we done? In the load
function, we've saved a copy of the $_POST
array to use later (in a property named post
), remembering that for each event load
gets first, before execution, so every event will have a full copy of the $_POST
array should it be executed.
If the __trigger()
is called, we override the $_POST['fields']
array with the fields that are specific to our current event. What is self::ROOTELEMENT
? When your events are saved, a ROOTELEMENT
constant is created as a handle of the event name. This is used by Symphony as the root node name when returning XML for your event. The benefit of using this constant is that if you change the name of your event, you only have to change it in one location.
While we're at it, change the return value of each event's allowEditorToParse
function to be false
, instead of true
. This will prevent you (or another developer) from overriding your changes should they try to re-save the event from the Symphony backend.
First limitation solved, you can now have the same field names across multiple sections and they will be added to their relevant section.
The other issue, while not important at the moment, is the order that the events are executed.
All events have a priority
, which is a crude system of low
, normal
and high
which determines what order events should be triggered. By default, all events are given the normal
priority, and then are executed in alphabetical order. For the next part of this tutorial, let's add a Select Box Link field to our Dealers section called "Related Cars" and link it to the "Make" field in the Cars section.
What we want to happen is that when a user creates both a Car and Dealer entry at the same time, the Car entry is linked to the Dealer. Let's change the priority of the events to ensure that it will always fire in the correct order. This is really for tutorial purposes, as in this case, Create Dealer is alphabetically after Create Car, so will already be called after the Create Car event anyway. Nevertheless, open up the Create Car event in your text editor and add the following:
public function priority(){
return self::kHIGH;
}
This event flow always assumes that the Car entry is created successfully, which isn't always the case. Users may enter some incorrect information which causes the entry to not save and instead the event output will have some error information to show the user.
In this case, we want the event to return before it even tries to create the Dealer entry. To do this, lets start by removing the hidden input field for the Create Dealer event from our HTML form (<input name="action[create-dealer]" type="hidden" value="Submit" />
).
Just before the return $result
of the Create Car event, let's check the status of the event by looking at the event XML result. What we are looking for is a status attribute of success
and if it exists, we want to replicate our hidden field in the $_POST
data so the Create Dealer event will fire. Also, the Select Box Link uses the entry ID to link entries together, so let's add the new Car's entry ID into the $_POST
array for the Create Dealer event, so our new Dealer entry will have a relationship.
// 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;
Tada! We now have two events chained together, with the second event only executing when the first one was successful.
There is another case: the Create Car event worked correctly, but the Create Dealer event did not. There are two different approaches you could employ here, both are based in XSLT and fortunately not in PHP.
The first option is to re-display the entire form (including the Create Car) fields but also add the ID of the Create Car entry as a hidden field. On re-submit the event will edit the existing entry instead of creating a new one. It's a useful approach if you want users to have a second chance at updating the information.
The second option is to not display any of the fields relating to the Create Car event, and instead just display those fields relating to the Create Dealer event.
Symphony makes it easy to auto-populate your forms with this information as it provides post-values
in the event response 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>
In my example, both events were successful. But if something went wrong...
<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>
In the attached XSLT, I've gone with the approach that if the Create Car event is successful but an error occurs in the Create Dealer event, it will display a summary to the user. But with the event XML it's possible to customise your frontend with XSLT to do whatever you like.
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.
The beauty of Symphony events is that they are really just a block of PHP that is called before your page renders, so it's possible to do almost anything your heart desires! To keep things simple, let's assume the following scenario:
I have a button that upon each click, increments a field's value one by one until it reaches 10. When it hits 10, we'll send an email to someone to let them know that this has happened. For tutorial purposes, we'll write this as a completely custom event.
Here's my test section:
Counter
Count Text Input Max Text Input Email Text Input Email Response Text Input
I've added one entry, setting the count to 1, max to 10, added my email address and left the Email Response field empty. I've also created a new event called Increment, and a new page with the following form XSLT (where 213
is the ID of entry I just created):
<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>
Now we'll jump to our eventincrement
Class (inside workspace/events/event.increment.php
). First things first, set the allowEditorToParse
function to return false
, then move to the __trigger()
function and remove the include
and we'll set the $result
to be an empty XMLElement
instance:
protected function __trigger(){
$result = new XMLElement(self::ROOTELEMENT);
return $result;
}
If you click our Submit button, you should get an empty response in the event XML
<increment />
Happy? Didn't think so. I've attached the __trigger()
function with comments to guide you through the rest of the custom event. There are a couple of big omissions here, such as ensuring user input is sanitized and checking that the data is correct which will need to be covered in a later tutorial, but hopefully this gives you a general idea of creating a custom event which involves reading an entry's current data, updating entry data and displaying a result to a user.
One last touch: remember how we set allowEditorToParse()
to return false
? This will now show the event's documentation()
return value when you click on the event in the backend. By default, Symphony uses this documentation to show the basic form HTML needed to populate the fields of the event (and may also 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 an XMLElement
object
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.