Warning: file_exists() [function.file-exists]: open_basedir restriction in effect. File(/usr/lib/php5/pear/cake/libs/controller/components/session.php) is not within the allowed path(s): (/var/www/vhosts/rd11.com/subdomains/rdos/httpdocs:/tmp) in /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/basics.php on line 939

Warning: session_start() [function.session-start]: Cannot send session cookie - headers already sent by (output started at /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/basics.php:939) in /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/libs/session.php on line 140

Warning: session_start() [function.session-start]: Cannot send session cache limiter - headers already sent (output started at /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/basics.php:939) in /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/libs/session.php on line 140

Warning: file_exists() [function.file-exists]: open_basedir restriction in effect. File(/usr/lib/php5/pear/cake/libs/model/dbo/dbo_mysql.php) is not within the allowed path(s): (/var/www/vhosts/rd11.com/subdomains/rdos/httpdocs:/tmp) in /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/basics.php on line 939

Warning: file_exists() [function.file-exists]: open_basedir restriction in effect. File(/usr/lib/php5/pear/cake/config/tags.ini.php) is not within the allowed path(s): (/var/www/vhosts/rd11.com/subdomains/rdos/httpdocs:/tmp) in /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/basics.php on line 939

Warning: file_exists() [function.file-exists]: open_basedir restriction in effect. File(/usr/lib/php5/pear/cake/libs/view/helpers/html.php) is not within the allowed path(s): (/var/www/vhosts/rd11.com/subdomains/rdos/httpdocs:/tmp) in /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/basics.php on line 939

Warning: file_exists() [function.file-exists]: open_basedir restriction in effect. File(/usr/lib/php5/pear/cake/libs/view/helpers/javascript.php) is not within the allowed path(s): (/var/www/vhosts/rd11.com/subdomains/rdos/httpdocs:/tmp) in /var/www/vhosts/rd11.com/subdomains/rdos/httpdocs/cake/basics.php on line 939
CakePHP : A Rapid Development Framework :: Bulletin Boards

this content element requires the Flash 7.0 player. You can download it here

Part II: The AS code

written by: Seth Hillinger aka shi11

CakePHP->AMF(action messaging format)->Flash. AMFPHP is a missing link between PHP and Flash using Remote Objects. Without Patrick Mineault and his teams efforts there would be no open source AMFPHP available. Then combine that with an open source, MVC CakePHP framework using AMFPHP, and we are breaking down the XML barrier. This is a beautiful example of the DRY philosophy. No more serializing/deserializing. No longer will PHP need to convert it's object types to XML so that Flash can convert the XML back to it's AS2 type.

This example is an extension of the fine actionscript.org amfphp tutorial by Jesse Stratford. If you want to learn more, Jesse does a great job of explaining AMFPHP more indepth. And of course there's also amfphp.org



Getting Flash Setup:

  1. Download the Flash files

Unpack Flash files to the CakeAMFPHP directory. We should have the following structure:
flashStructure

In the directory structure, you'll see I have a flash directory called ria, which separates my .as ,.flas, and .swfs. Typically I export my swfs to the deploy directory so I've kept that there, but don't use it! For cake, export your swfs to the app/webroot/swfs directory. (I just keep deploy there for uniformity.)

The Code

Since this code is an extension of Jesse's tutorial I will highlight my changes and briefly cover their method functionality. This tutorial assumes you understand advanced AS2 code and already have understanding of AMFPHP although this may be your first time using it. There are two things to really take note of below: the reference to the cake_gateway.php file and the SERVICE_NAME. As you learned in part I, the gateway file is a modified gateway made specifically for cake. The SERVICE_NAME refers to the name of the PHP Class which you also created on the previous page.

//Import Remoting classes
import mx.remoting.*;
import mx.remoting.debug.*;
import mx.rpc.*;

//The helpful Delegate for UI component event listeners
import mx.utils.Delegate;

//For customizing the datagrid columns
import mx.controls.gridclasses.DataGridColumn;

//For data verification
import mx.controls.Alert;

class BulletinBoardsController
{
	//A reference to out root movie
	var root:MovieClip;
	
	//The offset the currently viewed post
	var offset:Number = 0;
	
	//The location of the gateway and the name of the service
	//static var GATEWAY_URL:String = "http://localhost/CakeAMFPHP/cake_gateway.php"
	//public service instead
	static var GATEWAY_URL:String = "http://yoursite.com/CakeAMFPHP/cake_gateway.php"

    //Name of Service
	static var SERVICE_NAME:String = "BulletinBoardsController"
	
	//Max number of posts per request
	static var MESSAGES_PER_PAGE:Number = 10;
	
	//The service reference 
	var service:Service;
	
	function BulletinBoardsController(root:MovieClip)
	{
		//Remember the root
		this.root = root;
		
		//Start doing the magic
		init();
	}
}
Next, in the init method below, initialize the NetDebugger.
The NetConnection debugger is located on
On Windows at C:\Documents and Settings\{Your username}\LocalSettings\Application Data\Macromedia\Flash {version}\{language}\configuration\WindowSWF\NetConnection Debugger.swf.
On OS X it should be in: /Users/{username}/Library/Application Support/Macromedia/Flash {version}/{language}/Configuration/WindowSWF\NetConnection Debugger.swf.

Jesse says*: since it's a very useful tool, you'll probably want to make a shortcut on your desktop to it so you'll have it handy whenever you need it.

amfPHP call and response

Following init() are the two workhorse Controller functions. Read() and Post(). Think of Cake as your Model. These Controller methods interact with the Model. service.amfRead calls the Cake BulletinBoardsController.php amfRead() function and service.amfCreate references it's Cake twin. Following the service calls is a RelayResponder to handle the results.
	///////////////////////////////////////////////////////////////////////////
	//                  Remoting-related methods (interesting stuff)
	///////////////////////////////////////////////////////////////////////////
	//This function creates the service, calls Read and waits for results
	function init()
	{
		//Start the NetConnection debugger
		NetDebug.initialize();
		//Create the service
		service = new Service(GATEWAY_URL, null, SERVICE_NAME);
		//Read the first set of posts
		Read(int(0));
		
		//Make the UI work (boring)
		initUI();
	}
	
	//Ask the remote server to read some posts
	function Read(start:Number)
	{
		//arg1: message start, arg2: message offset
		var pc:PendingCall = service.amfRead(start, MESSAGES_PER_PAGE);
		pc.responder = new RelayResponder(this, 'handleRead', null);
		//Remember the offset
		offset = start;
	}
	
	//Ask the remote server to insert a post
	function Post(message:Object,user:Object)
	{
		var pc:PendingCall = service.amfCreate(message,user);
		pc.responder = new RelayResponder(this, 'handlePost', 'handleRemotingError');
	}

Handling the results

Pretty self explanatory here. The response from CakeAMFPHP triggers one of the following. If the amfRead response has no errors, it binds the datagrid to the ResultEvents results. If a post is successful it hides the form and reloads the datagrid. If there are errors, it throws a trace to the NetDebugger.
//Get data back from server, show the post
function handleRead(re:ResultEvent)
{
	//Step 1: Take the returned RecordSet, and bind it to the datagrid
	root.readMessages.dgMessages.dataProvider =  re.result;
	//There is no step 2.... Mouhahah!
	NetDebug.trace(re.result.length);//use this for testing
	//Reset the selectedIndex of the datagrid to 0
	root.readMessages.dgMessages.selectedIndex = 0;
	//Fake as though the datagrid was clicked
	onDgChange({target:root.readMessages.dgMessages});
	
	//Enable/disable next and previous buttons
	root.readMessages.btnPrev.enabled = offset > 0;
	root.readMessages.btnNext.enabled = re.result.length == MESSAGES_PER_PAGE;
}

//Insert request was answered, hide the post interface
function handlePost(re:ResultEvent)
{
	resetPost(); //Clear for next time
	hidePost(); //Hide interface
	
	//Refresh posts
	Read(0);
}

// need to test this function. taken from drswank.
function handleRemotingError( fault:FaultEvent ):Void 
{
	NetDebug.trace({level:"None", message:"Error: " + fault.fault.faultstring });
}

The Contoller Events

When the datagrid changes it calls onDgChange() and fills in the textArea with Cake ModelName fields using modelName_fieldname syntax. onPost sends a message and user object to Post().
//Called when datagrid is clicked. Inspect the data and show
//it in the textarea below
function onDgChange(evtObj:Object)
{
	var datagrid = evtObj.target;
	var data:Object = datagrid.dataProvider.getItemAt(datagrid.selectedIndex);
	if(data == null)
	{
		root.readMessages.txtMessage.text = ""; //If empty, clear textarea
	}
	else
	{
         //Fill in the textarea with the data return from the CakeAMFPHP service
         //The data in the recordset has the ModelName underscore field(ie: BulletinBoard_field). This allows some Cake associations to work. Let us know if you get dot syntax to work.
		root.readMessages.txtMessage.text = 
			'' + data.BulletinBoard_message + '
----------------------------------------
' + 'Posted by ' + data.User_name + '' + ' of ' + data.BulletinBoard_url + '' + ' at '+data.BulletinBoard_created + ''; } } //Submit a post to the remote server function onPost(evtObj:Object) { //Gather info from UI var message = new Object(); var user = new Object(); user.name = root.postMessages.txtName.text; message.email = root.postMessages.txtEmail.text; message.url = root.postMessages.txtUrl.text; message.message = root.postMessages.txtMessage.text; //Check if data was input if(user.name == '' || message.email == '') { Alert.show('Please input name and email', 'Validation Error', Alert.OK); return; } //Now submit Post(message, user); } /////////////////////////////////////////////////////////////////////////// // UI-related methods (not-so interesting stuff) /////////////////////////////////////////////////////////////////////////// function onPrev() { Read(offset - MESSAGES_PER_PAGE); //Read the previous set of posts } function onNext() { Read(offset + MESSAGES_PER_PAGE); //Read the next set of posts } function onRefresh() { Read(offset); //Refresh the UI } function onNewPost() { root.postMessages._visible = true; //Make the post interface appear } function onReset() { resetPost(); //Reset the post interface } function onCancel() { hidePost(); //Hide the post interface on cancel }

Finally, the View

Although Jesse has this in one file, I would like to separate the following code into a BulletinBoardView.
function initUI()
{
	//Hide the insert insterface
	root.postMessages._visible = false;
	
	//Add an event listener so clicking the datagrid will show the
	//message in the textarea below
	root.readMessages.dgMessages.addEventListener('change', Delegate.create(this, onDgChange) );
	//Add various button click handlers
	root.readMessages.btnPrev.addEventListener('click', Delegate.create(this, onPrev));
	root.readMessages.btnNext.addEventListener('click', Delegate.create(this, onNext));
	root.readMessages.btnRefresh.addEventListener('click', Delegate.create(this, onRefresh));
	root.readMessages.btnNew.addEventListener('click', Delegate.create(this, onNewPost));
	
	//Same for the post interface
	root.postMessages.btnPost.addEventListener('click', Delegate.create(this, onPost));
	root.postMessages.btnReset.addEventListener('click', Delegate.create(this, onReset));
	root.postMessages.btnCancel.addEventListener('click', Delegate.create(this, onCancel));
	
	//Stop clicks from going through background
	root.postMessages.background.onPress = Delegate.create(this, null);
	root.postMessages.background.useHandCursor = false;
	
	//Set some styles
	_global.style.setStyle('themeColor', 'haloBlue');
	_global.style.setStyle('fontFamily', 'Verdana');
	_global.style.setStyle('fontSize', 10);
	
	//Set the columns for the datagrid // yo, down here.ok
	var dgc:DataGridColumn = new DataGridColumn();
	dgc.headerText = 'Posted';
	dgc.columnName = 'BulletinBoard_created';//data return from CakeAMFPHP recordset: notice the ModelName and underscore
	dgc.width = 140;
	
	root.readMessages.dgMessages.addColumn(dgc);
	
	var dgc:DataGridColumn = new DataGridColumn();
	dgc.headerText = 'Author';
	dgc.columnName = 'User_name';//data return from CakeAMFPHP recordset: notice the ModelName and underscore
	dgc.width = 120;
	
	root.readMessages.dgMessages.addColumn(dgc);
	
	var dgc:DataGridColumn = new DataGridColumn();
	dgc.headerText = 'Snippet';
	dgc.columnName = 'BulletinBoard_message';//data return from CakeAMFPHP recordset: notice the ModelName and underscore
	dgc.width = 240;
	
	root.readMessages.dgMessages.addColumn(dgc);
	root.readMessages.dgMessages.setStyle('hGridLines', true);
	root.readMessages.dgMessages.setStyle('hGridLineColor', 0xdddddd);
	
	//Set the message text field to html mode, set the font
	var styles:TextField.StyleSheet = new TextField.StyleSheet();
	styles.setStyle("html", {fontFamily:"Verdana,Arial,Helvetica,sans-serif", fontSize:"11px"});
	
	root.readMessages.txtMessage.html = true;
	root.readMessages.txtMessage.styleSheet = styles;
}

//Clear the post interface fields
function resetPost()
{
	root.postMessages.txtName.text = "";
	root.postMessages.txtEmail.text = "";
	root.postMessages.txtUrl.text = "http://";
	root.postMessages.txtMessage.text = "";
}

//Hide the post interface
function hidePost()
{
	root.postMessages._visible = false;
}
}

Ok, that is it. When testing the swf make sure to export it to the app/webroot/swfs directory. Once again, big ups to Jesse and Patrick for pulling out their creative machetes and clearing a path for us to follow. If Bob Ross were a programmer, I think at this point he'd say something like: Happy Collaborating.
-Seth

 
2 queries took 2 ms
NrQueryErrorAffectedNum. rowsTook (ms)
1DESC `bulletin_boards`771
2DESC `users`221