Categories
Articles

Parsley MVC, RemoteObject, Zend AMF and MySQL Basic Flex Example

After conquering a basic Parsley Framework Flex example I wanted to create a minimal example of using the Flex Parsley framework, Zend AMF and MySQL. The Flex implementation goal is to mold a model view controller framework using Parsley and to communicate via Flex RemoteObject to Zend AMF. Eventually I applied what I learned to revamp DefineIt.com with Parsley and Zend AMF.

There are many approached to using Parsley for MVC. The Parsley documentation is very light on the subject. These also entail including using other frameworks like Caringorm. In this example I kept it simply by just using the Parsley messaging features and creating my own controller class.

I found other examples of Parsley that go further into its features. You might look at Christophe Coenraets’ example. This is referenced from the Parsley documentation . Christophe includes an update and add feature to the application. It also contains a dynamically loaded image class. It is a bit over featured and thus over involved for a pure beginner. It is void of substantive explanation leaving you to explore the Parsley documentation to try to understand why one approach was chosen over another in code. Worse code has no comments. This should be a minimum requirement for any referenced sample code. However there are some good Flex and Parsley techniques in there that can be helpful and one to bookmark for your learning process.

Learn More About Flex 4 From Adobe

Parsley 2.3 is used and the SWCs are included in the example Flex 4 project download I have here. I did appreciate these SWCs also being included in the downloads from the Parsley download page. Lets you get to learning faster.

You can build this with the free Flex SDK by using the code in the src folder and be sure to include a path to the Parsley and Spicelib library.

When you run this application, the trace log will show you the interactions between the model, controller, view and service.

Application Class – Parsley_Configuration.mxml
This is the main MXML file. Line 15 links the Parsley_Configuration.mxml file that configures Parsley framework.

Line 16 tells Parsley to include this mxml file and thus allows us to inject the ApplicationModel data.

I put all locale language into the model to simplify the example. In the effort the ApplicationModel class for application level data only contains the locale language title for the application you can see referenced in the Label on lines 26 and 39.

Line 40 contains the Panel to display the data grid.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
			   xmlns:s="library://ns.adobe.com/flex/spark" 
			   xmlns:hellozend="com.alh.parsley.example.hellozend.*"
			   xmlns:parsley="http://www.spicefactory.org/parsley"
			   xmlns:mx="library://ns.adobe.com/flex/mx"
			   xmlns:sf="http://www.spicefactory.org/parsley"
			   minWidth="955" minHeight="600" 
			   xmlns:views="com.alh.parsley.example.hellozend.views.*"
			   >
	<fx:Declarations>
		<!-- 
		Parsley configuration MXML file
		-->
		<parsley:ContextBuilder config="Parsley_Configuration" />
		<sf:Configure/>
	</fx:Declarations>
	<fx:Script>
		<!&#91;CDATA&#91;
			import com.alh.parsley.example.hellozend.model.ApplicationModel;
			/**
			 * Parsley injects the ApplicationModel here.
			 * */
			&#91;Inject&#93;
			&#91;Bindable&#93;
			public var model:ApplicationModel;
			
		&#93;&#93;>
	</fx:Script>
	<!-- 
		UI
	-->
	<s:layout>
		<s:VerticalLayout gap="10" 
						  horizontalAlign="center"
						  paddingLeft="12" paddingRight="12" paddingBottom="12" paddingTop="12"
						  />
	</s:layout>
	<s:Label text="{model.lang_AppTitle}" fontSize="20"/>
	<views:AllMembersGrid/>
</s:Application>

Parsley Configuration – Parsley_Configuration.mxml
This is the file Parsley reads for configuration.

I included the RemoteObject definitions here as well. I am following Christophe Coenraets’ example on this. It makes sense to me to have this as part of the configuration file. Remember to change the YOUR_GATEWAY_URL on line 13 to reflect your link to the Zend AMF or other RemoteObject service.

You may note there is no services xml or configuration file. The channel information is in the MXML instead. You can learn more on this by reading
ZEND AMF (Action Message Format) Minimalist Example Using RemoteObject and MySQL

On lines 27 – 30 I have four Actionscript classes handling the model, view and controller. They are listed here so we can use the [Inject] metatag for Parsley.

<?xml version="1.0" encoding="utf-8"?>
<Objects 
	xmlns:fx="http://ns.adobe.com/mxml/2009"
	xmlns="http://www.spicefactory.org/parsley"
	xmlns:s="library://ns.adobe.com/flex/spark" 
	xmlns:hellozend="com.alh.parsley.example.hellozend.*" 
	xmlns:model="com.alh.parsley.example.hellozend.model.*" 
	xmlns:services="com.alh.parsley.example.hellozend.services.*" 
	xmlns:controller="com.alh.parsley.example.hellozend.controller.*">
	<fx:Declarations>
		<s:ChannelSet id = "zend_amf_channel_set">
			<s:channels>
				<s:AMFChannel uri="YOUR_GATEWAY_URL"/>
			</s:channels>
		</s:ChannelSet>
		<!-- MembershipService RemoteObject -->
		<s:RemoteObject	id="membershipService_ro" 
						destination="zend-amf" 
						source="MembershipService" 
						channelSet="{zend_amf_channel_set}"
						showBusyCursor="true" 
						>
		</s:RemoteObject>
		<!--
			Parsley defined objects slated for injection where &#91;Inject&#93; metatag appears.
		-->
		<model:ApplicationModel/>
		<model:MembershipModel/>
		<services:MembershipServiceRemote/>
		<controller:MembershipController/>
</fx:Declarations>
</Objects>

Application Model Class – ApplicationModel.as
This class defines application level data and we only have a language variable to simplify the example for localization.

package com.alh.parsley.example.hellozend.model
{
	/**
	 * Model for application level data
	 * */
	public class ApplicationModel
	{
		/**
		 * UI language in model to simplify example
		 * 
		 * Language for Application
		 * */
		[Bindable]
		public var lang_AppTitle:String = "Minimalist Parsley Zend AMF Example Using RemoteObject";;
	}
}

Membership Model Class – MembershipModel.as
This class defines membership data. Again you can see the variables to simplify the example for localization.

The key data here is the ArrayCollection on line 13 for the member data.

package com.alh.parsley.example.hellozend.model
{
	import mx.collections.ArrayCollection;
	/**
	 * Model for membership
	 * */
	public class MembershipModel
	{
		/**
		 * List of members;
		 * */
		[Bindable]
		public var members:ArrayCollection;
		/**
		 * UI language in model to simplify example
		 * 
		 * Language for AllMembersGrid view
		 * */
		[Bindable]
		public var lang_AllMembersGridTitle:String = "Membership Listing";
		[Bindable]
		public var lang_MemberKey:String = "Member Key";
		[Bindable]
		public var lang_FirstName:String = "First Name";
		[Bindable]
		public var lang_LastName:String = "Last Name";
		[Bindable]
		public var lang_EmailAddress:String = "Email Address";
		[Bindable]
		public var lang_GetMembers:String = "Get Members";
		[Bindable]
		public var lang_ClearMembers:String = "Clear";
	}
}

IMembershipService Class – IMembershipService.as
Interface to define methods for membership service classes. We only have one in the example: MembershipServiceRemote.

package com.alh.parsley.example.hellozend.services
{
	import mx.rpc.AsyncToken;
	/**
	 * Interface to define MembershipService classes
	 * */
	public interface IMembershipService
	{
		function getAllMembers():AsyncToken;
	}
}

[ad name=”Google Adsense”]
MembershipServiceRemote Class – MembershipServiceRemote.as
This class deals with the server service. Only the controller communicates with this class by calling the get getAllMembers() method.

Line 46 is the interface back to the MVC or to the application however you want to look at it. Here a Parsley message is created for the MembershipGetAllMembersEvent.GET_ALL_MEMBERS event that you will see the MembershipController class handles.

You could have the controller pass its own result and fault handlers to the getAllMembers() method and eliminate the MembershipGetAllMembersEvent.GET_ALL_MEMBERS message.

As a further simplification, the faults are all routed to the catchAllServiceErrorHandler(…) method where a view component is inserted breaking a clean MVC. You could create a message for the fault handler to dispatch and have the controller to handle this.

package com.alh.parsley.example.hellozend.services
{
	import com.alh.parsley.example.hellozend.events.MembershipGetAllMembersEvent;
	import com.alh.parsley.example.hellozend.services.IMembershipService;
	import mx.controls.Alert;
	import mx.rpc.AsyncResponder;
	import mx.rpc.AsyncToken;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.remoting.RemoteObject;
	/**
	 * IMembershipService class to handle RemoteObject data communications with server.
	 * */
	public class MembershipServiceRemote implements IMembershipService
	{
		/**
		 * Specify the remote service for Parsley to inject.
		 * */
		[Inject(id="membershipService_ro")]
		public var service:RemoteObject;		
		/**
		 * Parsley creates an event dispatcher function
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;

		/**
		 * Get all members from RemoteObject service.
		 * */
		public function getAllMembers():AsyncToken
		{
			trace ("SERVICE: MembershipServiceRemote.getAllMembers()");
			var token:AsyncToken = service.getAllMembers();
			token.addResponder(
				new AsyncResponder(getAllMembersResultsHandler, catchAllServiceErrorHandler));
			return token;
		}
		/**
		 * ResultEvent handler for service.getAllMembers()
		 * */
		protected function getAllMembersResultsHandler(event:ResultEvent, result:Object):void
		{
			trace ("SERVICE: MembershipServiceRemote.getAllMembersResultsHandler()");
			//event.result == Array.
			var members:Array = event.result as Array;
			dispatcher( new MembershipGetAllMembersEvent(MembershipGetAllMembersEvent.GET_ALL_MEMBERS, members));
		}
		/**
		 * Default handler for MembershipServiceRemote calls
		 * */
		protected function catchAllServiceErrorHandler(e:FaultEvent):void
		{
			Alert.show(e.toString());
		}
	}
}

MembershipController Class – MembershipController.as
This controller ties in the MembershipModel class and the MembershipServiceRemote class on line 18 and 24 respectively.

The controller listens for the MembershipEvent events on line 29. The Parsley [MessageHandler] tag makes this happen. These events are dispatched in the view.

On line 46 the controller listens for the MembershipGetAllMembersEvent dispatched from the MembershipServiceRemote class.

package com.alh.parsley.example.hellozend.controller
{
	import com.alh.parsley.example.hellozend.events.MembershipEvent;
	import com.alh.parsley.example.hellozend.events.MembershipGetAllMembersEvent;
	import com.alh.parsley.example.hellozend.model.MembershipModel;
	import com.alh.parsley.example.hellozend.services.MembershipServiceRemote;
	import mx.collections.ArrayCollection;
	/**
	 * Controller for Membership model and views.
	 * */
	public class MembershipController
	{
		/**
		 * Parsley injects the MembershipModel here.
		 * */
		[Inject]
		[Bindable]
		public var model:MembershipModel;
		/**
		 * Parsley injects the MembershipService here.
		 * */
		[Inject]
		[Bindable]
		public var service:MembershipServiceRemote;
		/**
		 * Parsley identified handler for MembershipEvent
		 * */
		[MessageHandler]
		public function membershipEventHandler( message:MembershipEvent ):void
		{
			trace ("CONTROLLER: MembershipController.membershipEventHandler(...) - message.type:" + message.type);
			switch (message.type )
			{
				case MembershipEvent.GET_ALL_MEMBERS:
					service.getAllMembers();
					break
				case MembershipEvent.CLEAR_ALL_MEMBERS:
					model.members.removeAll();
					break
			}
		}
		/**
		 * Parsley identified handler for MembershipGetAllMembersEvent
		 * */
		[MessageHandler]
		public function membershipGetAllMembersEventHandler( message:MembershipGetAllMembersEvent ):void
		{
			trace ("CONTROLLER: MembershipController.membershipGetAllMembersEventHandler(...)");
			model.members = new ArrayCollection(message.members);
		}
	}
}

[ad name=”Google Adsense”]
Membership Grid View – AllMembersGrid.mxml
This is a Panel containing a DataGrid and Buttons for the user interaction.

Lines 27 and 28 sets up the class to dispatch Parsley messages. Line 35 dispatches the MembershipEvent.CLEAR_ALL_MEMBERS event and line 43 dispatches the MembershipEvent.GET_ALL_MEMBERS event. The MembershipController class handles these messages to update the model and communicate with the service as needed.

This view is tied to the MembershipModel class via Parsley on line 23. The DataGrid binds to the model members variable.

Other bindings for the language localizations appear as well.

<?xml version="1.0" encoding="utf-8"?>
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009" 
		 xmlns:s="library://ns.adobe.com/flex/spark" 
		 xmlns:mx="library://ns.adobe.com/flex/mx"
		 xmlns:sf="http://www.spicefactory.org/parsley"
		 title = "{model.lang_AllMembersGridTitle}"
		>
	<fx:Declarations>
		<!-- 
		Parsely will manage this component.
		-->
		<sf:Configure/>
	</fx:Declarations>
	<fx:Script>
		<!&#91;CDATA&#91;
			import com.alh.parsley.example.hellozend.events.MembershipEvent;
			import com.alh.parsley.example.hellozend.model.MembershipModel;
			/**
			 * Parsley injects the MembershipModel here.
			 * */
			&#91;Inject&#93;
			&#91;Bindable&#93;
			public var model:MembershipModel;
			/**
			 * Parsley creates an event dispatcher function
			 * */
			&#91;MessageDispatcher&#93;
			public var dispatcher:Function;
			/**
			 * Dispatch MembershipEvent.CLEAR_ALL_MEMBERS event
			 * */
			protected function clearDataGrid():void
			{
				trace("VIEW: " + className + ".clearDataGrid()");
				dispatcher( new MembershipEvent( MembershipEvent.CLEAR_ALL_MEMBERS ));
			}
			/**
			 * Dispatch MembershipEvent.GET_ALL_MEMBERS event
			 * */
			protected function getAllMembers():void
			{
				trace("VIEW: " + className + ".getAllMembers()");
				dispatcher( new MembershipEvent( MembershipEvent.GET_ALL_MEMBERS ));
			}
		&#93;&#93;>
	</fx:Script>
	<s:layout>
		<s:VerticalLayout gap="10" 
						  paddingLeft="12" paddingRight="12" paddingBottom="12" paddingTop="12"
						  />
	</s:layout>
	<mx:DataGrid  id="member_dg"  height="100" dataProvider="{model.members}">
		<mx:columns>
			<mx:DataGridColumn headerText="{model.lang_MemberKey}" dataField="memberKey"/>
			<mx:DataGridColumn headerText="{model.lang_FirstName}" dataField="firstName"/>
			<mx:DataGridColumn headerText="{model.lang_LastName}" dataField="lastName"/>
			<mx:DataGridColumn headerText="{model.lang_EmailAddress}" dataField="emailAddress" width="200"/>
		</mx:columns>
	</mx:DataGrid>
	<s:HGroup horizontalAlign="center" width="100%">
		<s:Button label="{model.lang_GetMembers}" click="{getAllMembers();}"/>
		<s:Button label="{model.lang_ClearMembers}" click="{clearDataGrid();}"/>
	</s:HGroup>

</s:Panel>

MembershipEvent.as
This is a standard Event class to inform the MVC framework all membership data is required or needs to be cleared. These free the view from being coupled with the service or updating the model.

package com.alh.parsley.example.hellozend.events
{
	import flash.events.Event;
	/**
	 * Events related to membership.
	 * */
	public class MembershipEvent extends Event
	{
		/**
		 * Request to retrieve all membership data
		 * */
		public static const GET_ALL_MEMBERS:String = "com.alh.parsley.example.hellozend.events.MembershipEvent.getAllMembers";
		/**
		 * Request to clear all membership data
		 * */
		public static const CLEAR_ALL_MEMBERS:String = "com.alh.parsley.example.hellozend.events.MembershipEvent.clearAllMembers";

		public function MembershipEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
		}
	}
}

MembershipGetAllMembersEvent.as
This event message signals success retrieval of members data from the service.

package com.alh.parsley.example.hellozend.events
{
	import flash.events.Event;
	/**
	 * Event defining service receipt of membership data.
	 * */
	public class MembershipGetAllMembersEvent extends Event
	{
		public static const GET_ALL_MEMBERS:String = "com.alh.parsley.example.hellozend.events.MembershipGetAllMembersEvent.getAllMembers";
		/**
		 * Membership data
		 * */
		public var members:Array;
		
		public function MembershipGetAllMembersEvent(type:String, members:Array, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.members = members;
		}
	}
}

Zend AMF Gateway PHP Script
This is the gateway program for the Zend Amf. This is the file that you reference on line 13 of the Parsley_Configuration.mxml file. In this example it was named index.php.

<?php
/**
*  Sample Zend AMF gateway
*  @return Endpoint &#91;Zend Amf Endpoint&#93;
* */

// Configurable values
// Debugging values
$debug = true;                             // Debugging status
if ($debug)
{
	// Report all errors, warnings, interoperability and compatibility
	error_reporting(E_ALL|E_STRICT);
	// Show errors with output
	ini_set("display_errors", "on");
}
else
{
	error_reporting(0);
	ini_set("display_errors", "off");
}
// Add the Zend AMF installation folder to the include path.
// In this example the frameworks folder is a sibling folder to
// this application folder. The frameworks folder contains the Zend
// folder that is extracted from http://framework.zend.com/download/amf
ini_set("include_path", ini_get("include_path") . PATH_SEPARATOR . "..\\frameworks" );

// Instantiate the Zend Amf server
require_once 'Zend/Amf/Server.php';
$server = new Zend_Amf_Server();

// Register your service classes
require_once 'MembershipService.php';
$server->setClass("MembershipService");

//Map ActionScript value objects to the PHP value objects.
$server->setClassMap("MemberData", "MemberData");

// Return the handle.
echo ($server->handle());

?>

[ad name=”Google Adsense”]
MembershipService Class
This is the service class that contains remote methods. Generally a the database and business logic is delegated to another API you write. In this case they are all together for simplicity.

<?php
/**
*	Service class exposing the methods to deal with membership.
*   This example includes business logic for simplicity of study.
*/
require_once 'MemberData.php';
class MembershipService
{
	public function MembershipService()
	{
		// Connect to MySql database.
		// Supply your own MySQL access values.
		// These are defaults when running on your own private computer.
		mysql_connect("localhost", "root", "");
		// Select the database.
		// Supply your own database name.
		mysql_select_db("test");
	}
	/**
	*	Get all members and all fields.
	*/
	public function getAllMembers()
	{
		// Array of MemberData objects.
		$members = array();
		// Selecting all fields and all records from table.
		// Supply your own table name and optionally your own SQL statement.
		$result = mysql_query("SELECT * FROM zend_amf_members");
		// Assuming mysql_query success. Slog through records.
		while ($row = mysql_fetch_assoc($result))
		{
			// Create a MemberData value object and populate.
			$member = new MemberData();
			$member->memberKey = $row["memberKey"];
			$member->firstName = $row["firstName"];
			$member->lastName = $row["lastName"];
			$member->emailAddress = $row["emailAddress"];
			array_push($members, $member);
		}
		// Return the members array to client.
		return $members;
	}
}
?>

MemberData Value Object Class for PHP
This is the value object on the server side to define the field names for a member object. This is mapped to the client side on line 37 of the Zend AMF gateway script.

<?php
/**
 * Value object defining the member data
 * */
class MemberData
{
  public $memberKey;	// uint
  public $firstName;	// String
  public $lastName;		// String
  public $emailAddress;	// String
}
?>

SQL To Create Testing Table
The PHP script uses zend_amf_members for the table and this is the SQL to create that table. In this example the database was called test.

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

--
-- Database: `test`
--

-- --------------------------------------------------------

--
-- Table structure for table `zend_amf_members`
--

CREATE TABLE IF NOT EXISTS `zend_amf_members` (
  `memberKey` int(10) unsigned NOT NULL auto_increment,
  `firstName` varchar(30) NOT NULL default '',
  `lastName` varchar(30) NOT NULL default '',
  `emailAddress` varchar(50) NOT NULL default '',
  PRIMARY KEY  (`memberKey`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Zend Amf Examples' AUTO_INCREMENT=7 ;

--
-- Dumping data for table `zend_amf_members`
--

INSERT INTO `zend_amf_members` (`memberKey`, `firstName`, `lastName`, `emailAddress`) VALUES
(1, 'Donald', 'Duck', 'quacker@pond.com'),
(2, 'Daffy', 'Duck', 'daft_2x@farm.org'),
(3, 'Elmer', 'Fudd', 'elmer.fudd@hunters.net'),
(4, 'Bugs', 'Bunny', 'whats_up_doc@underground.org'),
(5, 'Yosemite', 'Sam', 'varmint_chaser@forest.com'),
(6, 'Wile', 'Coyote', 'ceo@acme.com');

Categories
Articles

Amazon Web Service (AWS) Signed Request Using PHP For Flex HTTPService

By Lon (Alonzo) Hosford

I am in the process of updating my Flex Caringorm example that makes an ItemSearch operation to Amazon AWS to fetch data. AWS changed the security August 2009 to include a secret key value. Using this you need to create Signature parameter. You also need a Timestamp parameter.

The Signature parameter you need to generate and they have a utility helper to demonstrate should you need it.

[ad name=”Google Adsense”]

There is an Actionscript example of how to generate the signed request at Brendon Wilson’s blog. I did not try it. There is a discussion in the blog comments about placing the security key in the Actionscript code. The idea of a security key implies it is to be protected. Placing it in the Actionscript code is not wise because there are tools that can open the contents of a published swf.

Another plan is to store all the AWS information on a server and place the signature code there. So this is an example on how to do that with PHP. The AWS access ID and secret key are removed from the client side and are better protected. The secert key is in plain text and as such you may want to take more action to protect that if you think the PHP script is vulnerable.

Here is the PHP version of signing an AWS request. I had to cobble various examples I found and added the references at the end of this blog article. This example designed to work with Flex HTTPService and returns XML. The script allows placing test AWS parameters in the file for testing with a browser. You might want to try other AWS operations and parameters.

Configuration is easy. Lines 9 and 10 require your AWS codes. That is it.

To generate an error from AWS keep $useTestData on line 18 as false and run the script in a web browser. Your response will show AWS balking at a missing parameter because nothing is being sent for it to process. To see a positive result change $useTestData to true and the supplied test data will produce a nice pile of XML.

<?php
/**
*  Creats AWS request and signs the request
*  Wraps AWS response in user defined XML. 
*  To test add your AWS access key code ID and secrete access key and set $useTestData = true;
*/
header ("content-type: text/xml");
// Configurable values
$public_key = "{PLACE YOUR AWS ACCESS KEY HERE}";	// AWS access key code ID
$private_key = "{PLACE YOUR AWS SECRET KEY HERE}";	// AWS secret access key)
$amazonErrorRootNode = "<Errors";					// First node with < from amazon for error  response.


// Developer values
$version = "v.01.00.00";						// Version of this script

// Debugging values
$debug = false;								// Debugging status
$useTestData = false;							// Use embedded testing data

// Program controlled values
$success = "false";							// Default success value. 
$params = array();							// The parameters to pass to AWS 
$returnXML = "";								// XML returned

if ($useTestData)
{
	$params =  array(	"Operation"=>"ItemSearch",
                        "Keywords"=>"Beatles Abbey Road", 
                        "Service"=>"AWSECommerceService", 
                        "Sort"=>"salesrank", 
                        "SearchIndex"=>"Music", 
                        "Count"=>"25", 
						"ResponseGroup"=>"Medium,Tracks,Offers");
}
else
{
	$params = $_REQUEST;
}

$returnXML .= "<response>";

$returnXML .= "<version>";
$returnXML .= $version;
$returnXML .= "</version>";
if ($debug)
{
	$returnXML .= "<isTestData>";
	$returnXML .= $useTestData ? "true":"false";
	$returnXML .= "</isTestData>";
}

function aws_signed_request( $public_key, $private_key, $params)
{
	$method = "GET";
	$host = "ecs.amazonaws.com";  
	$uri = "/onca/xml";
	
	$timestamp = gmstrftime("%Y-%m-%dT%H:%M:%S.000Z");
	$timestamp = "&Timestamp=" . rawurlencode($timestamp);
	
	$params["AWSAccessKeyId"] = $public_key;
	
	$workurl="";
    foreach ($params as $param=>$value)
    {
		$workurl .= ((strlen($workurl) == 0)? "" : "&") . $param . "=" . rawurlencode($value);
    }
	//$workurl = str_replace(" ","%20",$workurl);
	$workurl = str_replace(",","%2C",$workurl);
	$workurl = str_replace(":","%3A",$workurl);
	$workurl .= $timestamp;
	$params = explode("&",$workurl);
	sort($params);
	
	$signstr = "GET\n" . $host . "\n/onca/xml\n" . implode("&",$params);
	$signstr = base64_encode(hash_hmac('sha256', $signstr, $private_key, true));
	$signstr = rawurlencode($signstr);
	$signedurl = "http://" .$host . $uri . "?" . $workurl  . "&Signature=" . $signstr;
	return $signedurl;
}

// Make the signed url for AWS
$signedurl = aws_signed_request( $public_key, $private_key, $params);

if ($debug)
{
	$returnXML .= "<signed_url>";
	$returnXML .= $signedurl;
	$returnXML .= "</signed_url>";
}

// Make request to AWS
$response = @file_get_contents($signedurl);

// The file_get_contents has failed. See PHP documentation for that.
if ($response === false) // Equal and same data type
{
	$success = "false";
}
// AWS returned a response
else
{
	$returnXML .= "<results>";
	// AWS did not return an error code
	if (strpos($response, $amazonErrorRootNode) == 0)
	{
		$success = "true";
	
		$returnXML .= substr($response, strpos($response, "?>")+2); // Strip Amazon XML header
	}
	// AWS returned an error code	
	else
	{
		$success = "false";
		$returnXML .= substr($response, strpos($response, "?>")+2); // Strip Amazon XML header
	}
	$returnXML .= "</results>";

}

$returnXML .= "<success>";
$returnXML .= $success;
$returnXML .= "</success>";

$returnXML .= "</response>";

echo $returnXML;

?>

References

Categories
Articles

Adobe AIR Native Drag and Drop File Upload With ProgressBar and PHP

This is an example of using Adobe AIR native drag and drop to upload a file to a web server using PHP5. The application only uploads one file at a time and assumes the drag operation is anywhere over the application.

This is the PHP code that will place the file into a folder named upload. The upload folder is a child folder under the folder for the PHP script. On line 13 you need to replace YOUR APPFOLDER with the folder you plan to install the PHP script. This also must be matched in the AIR MXML application.

You also need to set permissions on the upload folder to allow uploading.

The PHP script returns XML.The result.status node provide a true or false value for the success feedback.
file_upload.php

.
.
.
<?php
	$returnXML = "";
	$returnLog = "Receiving upload...\n";
    // Filedata is the default name used in uploading
	$returnLog .= "temporary file name = " . $_FILES['Filedata']['tmp_name']."\n";
	$returnLog .= "file name = " . $_FILES['Filedata']['name']."\n";
	$returnLog .= "file size = " . $_FILES['Filedata']['size']."\n";
	$file_temp = $_FILES['Filedata']['tmp_name'];
	$file_name = $_FILES['Filedata']['name'];
	$file_path = $_SERVER['DOCUMENT_ROOT']."/YOUR APPFOLDER/upload";
	$returnStatus = "false";
	$returnLog .= "attempting to move file...\n";
	if(  move_uploaded_file( $file_temp, $file_path . "/" . $file_name) )
	{
		$returnStatus = "true";
	}

	$returnLog .= "file move results = " . $returnStatus . "\n";
	$returnXML .= "<return>";
	$returnXML .= "<log>";
	$returnXML .= $returnLog;
	$returnXML .= "</log>";
	$returnXML .= "<status>";
	$returnXML .= $returnStatus;
	$returnXML .= "</status>";

	$returnXML .= "</return>";
	echo $returnXML ;

?>

This is the AIR main MXML code. You need to modify line 21 to include the URL to where you want to place the PHP script to handle the file upload.

.
.
.
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
					   xmlns:s="library://ns.adobe.com/flex/spark"
					   xmlns:mx="library://ns.adobe.com/flex/mx"
					   creationComplete="creationCompleteHandler(event)"
					   >
	<fx:Script>
		<![CDATA[
			import flash.desktop.ClipboardFormats;
			import flash.desktop.NativeDragManager;
			import flash.events.DataEvent;
			import flash.events.NativeDragEvent;
			import flash.filesystem.File;

			import mx.controls.Alert;
			import mx.events.FlexEvent;

			private const UPLOAD_SCRIPT_URL:String = "http://www.YOURDOMAIN.com/YOUR APPFOLDER/file_upload.php"

			protected function creationCompleteHandler(event:FlexEvent):void
			{
				console(className + ".creationCompleteHandler(...) - url:" + url );
				addEventListener(NativeDragEvent.NATIVE_DRAG_ENTER, onDragEnterHandler);
				addEventListener(NativeDragEvent.NATIVE_DRAG_DROP, onDragDropHandler);
			}
			/**
			 * Event handler for NativeDragEvent.NATIVE_DRAG_ENTER.
			 * May be called multiple times as file object is dragged over the application.
			 * <p>
			 * Check that there are file objects being dragged and only one file is being dragged.
			 * </p>
			 * */
			private function onDragEnterHandler(e:NativeDragEvent):void
			{
				console(className + ".onDragIn(...)");
				//Does the clipboard have an array of File objects (AIR only)
				if(e.clipboard.hasFormat(ClipboardFormats.FILE_LIST_FORMAT))
				{
					//Get the array of File objects
					var files:Array = e.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;

					//Allow only one file as only supporting one in this application
					if(files.length == 1)
					{
						//Triggers NativeDragEvent.NATIVE_DRAG_DROP event.
						NativeDragManager.acceptDragDrop(this);
					}
				}
			}
			/**
			 * Event handler for NativeDragEvent.NATIVE_DRAG_DROP
			 * Occurs when the file object is dropped over the application
			 * */
			private function onDragDropHandler(e:NativeDragEvent):void
			{
				var urlRequest:URLRequest = new URLRequest();
				urlRequest.url = UPLOAD_SCRIPT_URL + "?cache=" + new Date().getTime();
				console(className + ".onDragDropHandler(...) - urlRequest.url:" + urlRequest.url);
				//Get the array of File objects. The array should only have one entry per onDragEnterHandler(...)
				var files:Array = e.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;

				//Create a File object and register events.
				var file:File = File(files[0]);
				file.addEventListener(Event.COMPLETE, file_CompleteHandler);
				file.addEventListener(Event.OPEN, file_OpenHandler);
				file.addEventListener(DataEvent.UPLOAD_COMPLETE_DATA, file_UploadCompleteDataHandler);
				file.addEventListener(IOErrorEvent.IO_ERROR, file_IOErrorHandler);
				file.addEventListener(HTTPStatusEvent.HTTP_STATUS , file_HTTPStatusHandler);
				file.addEventListener(SecurityErrorEvent.SECURITY_ERROR, file_SecurityErrorHandler);
				file.addEventListener(ProgressEvent.PROGRESS, file_progressHandler);

				console(className + ".onDragDropHandler(...) - file.url:" + file.url);
				console(className + ".onDragDropHandler(...) - file.name:" + file.name);
				console(className + ".onDragDropHandler(...) - file.nativePath:" + file.nativePath);

				try {
					// Start the file upload
					console(className + ".onDragDropHandler(...) - uploading " + file.name + " ...\n" );
					file.upload(urlRequest, "Filedata", false);
				}
				catch (error:Error) {
					console(className + ".onDragDropHandler(...) - Unable to upload file." + file.name);
					console(className + ".onDragDropHandler(...) - error." + error.toString());
				}
			}
			private function file_progressHandler(event:ProgressEvent):void {
				var file:FileReference = FileReference(event.target);
				console(className + ".progressHandler(...) - name=" + file.name + " bytesLoaded=" + event.bytesLoaded + " bytesTotal=" + event.bytesTotal);
				progressBar.setProgress(event.bytesLoaded, event.bytesTotal);
				progressBar.label = "CurrentProgress: " + Math.round( (event.bytesLoaded /event.bytesTotal * 100)) + "%";
			}

			/**
			 * Dispatched when an upload or download operation starts.
			 */
			private function file_OpenHandler(event:Event):void
			{
				console(className + ".file_OpenHandler(...) - event:" + event );
			}
			/**
			 * Dispatched when an upload fails and an HTTP status code is available to describe the failure.
			 */
			private function file_HTTPStatusHandler(event:HTTPStatusEvent):void
			{
				console(className + ".file_HTTPStatusHandler(...) - event:" + event );
			}
			/**
			 * Dispatched when the upload fails for various I/O reasons.
			 */

			private function file_IOErrorHandler(event:IOErrorEvent):void
			{
				console(className + ".file_IOErrorHandler(...) - event:" + event );
			}
			/**
			 * Dispatched when an operation violates a security constraint.
			 */

			private function file_SecurityErrorHandler(event:SecurityErrorEvent):void
			{
				console(className + ".file_SecurityErrorHandler(...) - event:" + event );
			}

			/**
			 * Dispatched when download is complete or when upload generates an HTTP status code of 200.
			 */
			public function file_CompleteHandler(event:Event):void
			{
				console(className + ".file_CompleteHandler(...) file uploaded complete");
			}

			/**
			 * Dispatched after data is received from the server after a successful upload.
			 */
			public function file_UploadCompleteDataHandler(event:DataEvent):void
			{
				var result:XML = new XML(event.data);
				console(className + ".file_UploadCompleteDataHandler(...) - STATUS:" + result.status );
				console(className + ".file_UploadCompleteDataHandler(...) - LOG:\n" + result.log );

			}

			/**
			 * Update the UI trace log
			 */
			private function console(msg:String):void
			{
				trace(msg);
				console_ta.appendText(msg + "\n");
			}

		]]>
	</fx:Script>
	<s:layout>
		<s:VerticalLayout paddingLeft="10" paddingTop="10"/>
	</s:layout>
	<s:Label  text="Adobe AIR Drag and Drop File Upload" fontSize="20" fontWeight="bold"/>
	<s:Line yFrom="10" yTo = "100" xFrom="0" xTo="0" width="100%" height="0"  >
		<!-- Define the border color of the line. -->
		<s:stroke>
			<s:SolidColorStroke color="0x000000" weight="1" caps="square"/>
		</s:stroke>
	</s:Line>

	<mx:ProgressBar id="progressBar" labelPlacement="bottom" minimum="0" visible="true" maximum="100"
					color="0x323232" label="CurrentProgress 0%" direction="right" mode="manual" width="100%"/>

	<s:TextArea  width="100%" height="100%" id="console_ta" fontFamily="_typewriter" lineBreak="explicit"/>
	<s:HGroup horizontalAlign="center" width="100%">
		<s:Button   label="Clear" click="{console_ta.text = '';}"/>
	</s:HGroup>
</s:WindowedApplication>

The AIR application and the PHP script do not check for maximum upload file size allowed. A typical upload limit number imposed by web hosting is the default 2 megabytes. You may be able to increase that number in the php.ini file or the .htaccess file. The latter is the alternative is you have no control over the php.ini file and the code is included here to bump the size up. In any case you are at the mercy of your hosting provider.

.htaccess

php_value upload_max_filesize 10M
php_value post_max_size 10M
php_value memory_limit 128M

In the AIR application you can create a FILE object in the onDragEnterHandler in the same way as the onDragDropHandler and use the file.size parameter for a check. For the PHP script you can use the $_FILES[‘Filedata’][‘size’] value.

Categories
Articles

Php5 Simple Singleton Design Pattern

I wanted to reproduce a simple singleton design pattern in PHP 5. After reviewing many overly complex examples, I synthesized to this example.

It also makes use of the PHP5 magic function __get and __set also poorly addressed in web postings. Going for cleaner code I like to see $obj->prop rather than $obj->getProp() and $obj->setProp(value) in the code. But I am greedy and want to have read only and write only props. So I configured the various examples to achieve that goal. As well you can see in the __set and __get methods you may choose to work the props or call a private method that would handle the validation and proper handling of the set and return values. I demonstrated this here.

This is the singleton running with the idea of a single configuration object for an application. It only has one real property named isDebugging for the example.

<?php
class Config
	static private $_instance;
    private $isDebugging = false; // Read write
	private $version = "1.1"   ;  // Read only
	private $cannotReadProp = "x" ;
	public static function getInstance($args = array())
	{
		if(!self::$_instance)
		{
			self::$_instance = new Config();
		}
		self::$_instance->setIsDebugging(isset($args['isDebugging']) ? $args['isDebugging'] : self::$_instance->isDebugging);
		return self::$_instance;
	}
	private static function setIsDebugging($value)
	{
		self::$_instance->isDebugging = is_bool($value)? $value: self::$_instance->isDebugging;
	}
	public static function __set($prop, $value)

		if (isset(self::$_instance->$prop)
		{
			switch ($prop)
			{
				case 'isDebugging':
					self::$_instance->setIsDebugging($value);
					break
				default :
					throw new Exception('Cannot write property [' . $prop . ']',1);
			}
		}
		else
		{
			throw new Exception('Undefined property [' . $prop . ']',1);
		}
	}
	public static function __get($prop)
	{
		if (isset(self::$_instance->$prop))
		{
			switch ($prop)
			{
				case 'isDebugging':
					return self::$_instance->isDebugging
					break;
			       default :
					throw new Exception('Cannot read property [' . $prop . ']',1);
			}
		}
		else
		{
			throw new Exception('Undefined property [' . $prop . ']',1);
		}
	}
}
?>

This is a testing script for the above. Note there are some commented lines for testing the exception thowing.

<?php
	include 'config.inc.php';
	// First reference
	$myConfig1 = Config::getInstance();
	// Get property
	echo ('$myConfig1->isDebugging:' . ($myConfig1->isDebugging ? "true" : "false")) . '<br/>';
	// Set property
	echo 'Set $myConfig1->isDebugging = true <br/>';
	$myConfig1->isDebugging = true;
	echo ('$myConfig1->isDebugging:' . ($myConfig1->isDebugging ? "true" : "false")) . '<br/>';
	// Second reference
	echo '<br/>';
	echo 'Add second reference and not getInstance args<br/>';
	echo '$myConfig2 = Config::getInstance(); <br/>';
	$myConfig2 = Config::getInstance();
	echo ('$myConfig1->isDebugging:' . ($myConfig1->isDebugging ? "true" : "false")) . '<br/>';
	echo ('$myConfig2->isDebugging:' . ($myConfig2->isDebugging ? "true" : "false")) . '<br/>';
	// Third reference<br />
	echo '<br/>';
	echo 'Add third reference and change prop via getInstance arg <br/>';
	echo '$myConfig3 = Config::getInstance(array(\'isDebugging\'=>false)); <br/>';
	$myConfig3 = Config::getInstance(array('isDebugging'=>false));
	echo ('$myConfig1->isDebugging:' . ($myConfig1->isDebugging ? "true" : "false")) . '<br/>';
	echo ('$myConfig2->isDebugging:' . ($myConfig2->isDebugging ? "true" : "false")) . '<br/>';
	echo ('$myConfig3->isDebugging:' . ($myConfig3->isDebugging ? "true" : "false")) . '<br/>';
	// Set property via second instance
	echo '<br/>';
	echo 'Set property via second instance <br/>';
	echo 'Set $myConfig2->isDebugging = true <br/>';
	$myConfig2->isDebugging = true;
	echo ('$myConfig1->isDebugging:' . ($myConfig1->isDebugging ? "true" : "false")) . '<br/>';
	echo ('$myConfig2->isDebugging:' . ($myConfig2->isDebugging ? "true" : "false")) . '<br/>';
	echo ('$myConfig3->isDebugging:' . ($myConfig3->isDebugging ? "true" : "false")) . '<br/>';
	// Set property to incorrect data type
	echo '<br/>';
	echo 'Set $myConfig1->isDebugging = 123<br/>';
	echo 'No exception for invalid data type and not prop change.<br/>';
	$myConfig1->isDebugging = 123;
	echo ('$myConfig1->isDebugging:' . ($myConfig1->isDebugging ? "true" : "false")) . '<br/>';
	echo ('$myConfig2->isDebugging:' . ($myConfig2->isDebugging ? "true" : "false")) . '<br/>';
	echo ('$myConfig3->isDebugging:' . ($myConfig3->isDebugging ? "true" : "false")) . '<br/>';
	// Setter throws undefined property exception
	//$myConfig1->test = true;
	// Getter throws undefined property exception
	//echo '$myConfig1->test:' . $myConfig1->test . '<br/>';
	// Setter throws cannot write property exception
	//$myConfig1->version = '2.2';
	// Setter throws cannot read property exception
	//echo '$myConfig1->cannotReadProp:' . $myConfig1->cannotReadProp . '<br/>';
?>