On my most recent Yii-based Web site, I decided to use a custom Flash application for part of the back-end administration of the site. The justification was simple: what the administrator needed to do was a bit complicated; lots of information had to be available but the user interface had to remain uncluttered and natural. The specific Web site provided a way to create and take online quizzes. In a separate process, the administrator could add and edit questions. In the Flash-based process, the administrator could associate any number of questions with a quiz (actually, a pool of questions would be associated with a quiz and X number of random questions would be pulled from that pool). The main issue, from a user interface perspective, was how to display the questions in an intelligent way. With other examples, I might create a form for adding or editing quizzes and within that form, the questions would be listed within a dropdown menu (actually a SELECT menu that displayed and allowed for the selection of multiple options). However, with this particular example, there was no good way to display the questions in a SELECT menu. Each question had a unique primary key, of course, but that’s not very useful to the administrator. The questions did not have unique names (what name do you give a question?) and the best indicator of a question was its prompt, which would be HTML, possibly containing images and videos. So, again, how do I display possibly hundreds of questions in a meaningful way?
My solution was to show the list of questions by number and, when the admin clicks on a specific question, they’d see a preview of that question’s prompt (i.e., the HTML) in a separate interface. Now I’ve got three areas in my UI: one for adding or editing the other quiz attributes, one for listing every question, and a third for displaying the particulars of a single question. Given all this, I thought having the admin drag the questions from one area to another would be the best way to associate a question with a quiz. I could have done all this using jQuery (the JavaScript framework Yii uses by default), but that would have taken me days to figure out how to use jQuery in this way. On the other hand, I knew I could develop this using Flex (a framework for creating Flash applications) in no time. I just didn’t know how to get Yii and Flash to play ball. It turns out not to be that hard…
From a client-server perspective, the application required three services:
- The client fetches a list of questions from the server.
- The client fetches a list of quizzes from the server (so that the user can update existing quizzes).
- The client sends to the server a quiz-questions combination to be stored in the database.
To keep things relatively simple, I decided to go with basic HTTPService calls rather than AMF (Action Message Format). AMF is better and preferred in many ways, but it introduces some logistical issues I wasn’t prepared to deal with yet. So the first thing I had to do was create Controller and View code for returning lists of information (the Models were already defined and, as should be the case, were untouched by this new functionality). In my QuestionController file, I created this method:
public function actionXml() { $this->renderPartial('xml', array('models'=>Question::model()->findAll())); }
All this function does is render the xml View, passing to it all of the Question Models. The View itself has to output XML data. For that reason, I use renderPartial() instead of render(), as I just want the individual View file to be rendered without the rest of the layout (i.e., without all the HTML, CSS, and JavaScript). I’ve seen some examples online where people just outputted the XML from within the Controller, but you really shouldn’t do that as it breaks the MVC architecture.
The xml.php View file looks like this:
<?php header("Content-Type: text/xml"); echo '<?xml version="1.0" encoding="utf-8" ?>'; ?> <questions> <?php foreach ($models as $model):?> <question> <id><?php echo $model->id; ?></id> <prompt><![CDATA[<?php echo $model->prompt; ?>]]</prompt> <answer1><?php echo $model->answer1; ?></answer1> </question> <?php endforeach; ?> </questions>
The first line sends a Content-Type header to tell the client that the content is XML. Then the XML declaration is printed. Then the opening root tag is created, called questions. Next a foreach loop goes through each received Model. Within the loop I print whatever content needs to be sent to the client. This would be the question’s ID, its prompt, its possibly answers, etc. If the content might include any characters that are problematic in XML, including the ampersand, I would use this syntax, as in the prompt value:
<something><![CDATA[<?php echo $model->something; ?>]]></something>
The CDATA tags prevent text from being parsed as XML, thereby preventing problems with certain characters.
After I wrote this code for both of the services (the XML for the quizzes is quite similar), I tested the services in my Web browser by just going to http://hostname/question/xml and http://hostname/quiz/xml. By using a browser that understands XML, I can see the proper output before attempting to use my Flash client to connect to the service.
For the third service, data will be posted to the Yii-based site. In my QuizController file, I created a new method for this:
public function actionFlash() { }
What this method really does is the same thing as the auto-generated actionUpdate() and actionCreate() methods, but some of the code will be different based upon how I’m sending data here from the Flash client, so I decided to create a new method instead. All this method needs to do is receive the data, associate it with a Quiz Model, then save the Model and report upon the results. One complication, however, is that when Yii creates a form for a Model, it’ll use names such as Quiz[‘prompt’]. I didn’t know of an easy way to accomplish this when sending the data from Flash, so I just modified how the creation and updating happens. The first thing the method does is confirm that all of the values are received. In this hypothetical example, the values should be a quiz ID, a quiz name, and a list of questions associated with that quiz. So the method starts off with:
if (isset($_POST['id'], $_POST['name'], $_POST['questions'])) {
Next, the method checks if this is a new quiz (the Flash application allows for the creation of new quizzes) or an existing one. A new Model variable or existing Model is created depending upon this:
if ($_POST['id'] == 0) { $model=new Quiz; } else { $model=Quiz::model()->findbyPk($_POST['id']); }
Next, associate the incoming quiz name with the Model:
$model->name = $_POST['name'];
You would do this for any Model attribute that might have changed or be created for the first time. When using just Yii, the equivalent takes place using the Model’s attributes:
$model->attributes=$_POST['Quiz']; // Or whatever Model.
Then you save the Model:
if ($model->save()) {
This line will only work if all of the Model’s rules (i.e., its validations) are passed, so you don’t have to worry about invalid data getting into the system. Still, validation in the Flash client is a good idea, too.
The rest of the method just echo’s out messages based upon what happened:
- The quiz has been created.
- The quiz has been updated.
- The quiz could not be updated because of invalid data.
- And so forth.
There’s a bit more code in it as I the database has a QuizzesQuestions table which acts as an intermediary for the many-to-many relationship between the quizzes and the questions. (If you want to see the whole method, let me know.) This service, when called from the Flash client, will return a plain text message, which is why I just have the method echo out simple messages. Technically this also breaks the MVC architecture, but I didn’t feel like creating a new file that just said The quiz has been created.
At this point I’ve created three Controller methods that send and receive the necessary data. Next up, I created the Flash client using the Flex framework and the Flash Builder IDE. The client interface was pretty basic Flex stuff. I created a DataGrid for displaying the quizzes. When a quiz is selected in the DataGrid, its particulars are displayed and become editable in a form. The form includes a List into which questions can be dropped. The existing questions are displayed in a separate List. When a question is selected in that List, its particulars are displayed in a separate Panel.
When the application first loads, the two XML services are called to populate the quiz DataGrid and the questions List. When the quiz form Button is clicked, the actionFlash() service is invoked, saving or creating the quiz accordingly. (The Flex side of this only took me an afternoon to complete, by the way.)
The most interesting thing about all this was tapping into Yii’s access control rules. The way the entire Web site is written, only logged-in administrators can do things such as create or update quizzes. So how do I ensure that only logged-in administrators can use the Flash application? Tapping into Yii’s authentication from Flash isn’t easy but it turns out not to be necessary. To start, in the QuizController file, I restrict access to the Flash application’s View file to only logged-in administrators. This, though, does not prevent the Flash application from doing anything (in theory, someone could load the Flash application separately, if they knew of its existence). What I do as well is restrict access to the XML and Flash actions (in the appropriate Controller files) to only logged-in administrators. This will also automatically prevent the Flash application, if being run by a non-administrator somehow, from accessing those services. Here’s why this works: the Flash application is running within a Yii-based page, so any request that the Flash application makes is the same as any request the browser itself makes (this is also true for Ajax requests). So Yii-related cookies that are stored in the browser after the user logs in are also passed along as part of the request. Tada! By just using the access control rules in the Controller, the Flash application can only invoke those services if the user is logged in as an administrator. Very simple! The only catch was that when developing the Flex application in Flash Builder, I wouldn’t be running and testing the Flash application as a logged-in administrator, so I had to make the Controller’s actions public during that time.
So there’s that experience, then! I was pleased with how easy this was to pull off (i.e., I was pleased how little thinking I had to do to make this work) and my client was pleased with the result. I hope this helps you in your Flash+Yii projects. Thanks for reading and please let me know if you have any questions or comments.