PHP has always been my favourite scripting language to develop applications for the web. I have always envied Flash applications! PHP applications, to me, are a fine mixture of PHP + SQL + HTML + CSS + JavaScript. You can always add the fancy TLAs in your applications such as XML, if you want to.

With FLEX, Adobe has opened doors to developers like me, who have been kept apart for years, who are unwilling to commit time to learn the complexities of Flash development.

So, with the popularity of web applications and XML-like languages popping everyday, Adobe introduced (in 2004):

FLEX = Flash + MXML + CSS + ActionScript.

The first unknown in our formula: MXML is XML-based language which resembles HTML + CSS + JavaScript. In a traditional web application, the engine is the browser. Here it is the Flash player.

Our PHP + HTML + CSS + JavaScript codes/files of our application are stored on the web server. But MXML code has no use until it is compiled into SWF files Flash can run. For example, SWF file can be embedded inside a HTML page and sent to the visitor’s browser where Flash player runs it. Java is required by the MXML compiler!?

Straightaway, I remember OpenLaszlo! From 2000 onwards, they had this idea of compiling codes of web developers into Flash applications. Poor guys: because of fundamental marketing flaw and approach of presentation, Adobe is now collecting the fruits instead of them. Sometimes, even the choice of the name of a product has major effects on its lifespan.

Now let’s get hands dirty! I wanted to convert the user interface of my PHP Stock profiler into Flex.

phpShareFlex

For simplicity, I will keep all the files inside a folder on my web server; here is the folder structure of
/phpShareFlex/
|–/flex/
|—-services-config.xml
|—-phpShareFlex.mxml
|—-build.bat
|–/service/
|—-/class.MyShareService.php
|—-/index.php
|–/zf/library/Zend/
|–/tmp/
|–AC_OETags.js
|–phpShareFlex.swf
|–index.php

Since I am a PHP coder, let’s write the code for our service first:
/phpShareFlex/service/class.MyShareService.php

<?php
/**
 * Class to provide share services
 */
class MyShareService{
 
	/**
	 * Get options for index combobox
	 * @return array
	 */
	public function getIndexes(){
		$indexes = array();
		$indexes[] = array('label' => 'FTSE 100', 'data' => '^FTSE');
		$indexes[] = array('label' => 'FTSE 250', 'data' => '^FTMC');
		return $indexes;
	}
 
	/**
	 * Get options for top combobox
	 * @return array
	 */
	public function getTop(){
		$top = array();
		for($i=1; $i<=10; $i++) $top[] = array('label' => 'Top ' .($i*10), 'data' => ($i*10));
		return $top;
	}
 
	/**
	 * Get array of share data
	 * @param string $index
	 * @param array $methods_arr 
	 * @param int $top Number of shares
	 * @return array
	 */
	public function getShares($index, $methods_arr, $top=10){
		$shares = array();
		$shares[] = array('code' => 'AAA', 'price' => rand(1,100), 'change' => rand(-10,10), 'volume' => rand(100,1000));
		$shares[] = array('code' => 'BBB', 'price' => rand(1,100), 'change' => rand(-10,10), 'volume' => rand(100,1000));
		$shares[] = array('code' => 'CCC', 'price' => rand(1,100), 'change' => rand(-10,10), 'volume' => rand(100,1000));
		return $shares;
	}
}//end class
?>

Note: phpDoc blocks are required by Zend AMF, and the system does not like the output arrays with string keys, so use integer indexes.

Now, parent page to publish it:
/phpShareFlex/service/index.php

<?php
error_reporting(E_ALL);
date_default_timezone_set('Europe/London');
$myLog='';
function myErrorHandler($errno, $errstr, $errfile, $errline){
	global $myLog;
	$myLog .= "Error $errno on line $errline in file $errfile\n"
			. "----- $errstr\n";
	return TRUE;
}
$old_error_handler = set_error_handler("myErrorHandler");
 
$paths = array(
    realpath(dirname(__FILE__) . '/../zf/library'),
    '.'
);
set_include_path(implode(PATH_SEPARATOR, $paths));
 
require_once "../zf/library/Zend/Loader.php";
Zend_Loader::loadClass('Zend_Amf_Server');
 
require "class.MyShareService.php";//our class to do the job
 
$server = new Zend_Amf_Server();
$server->setProduction(FALSE);
$server->setClass( "MyShareService" );
 
if($myLog!=''){
	$filename = dirname(__FILE__).'/tmp/service.debug.'.date('YmdHis').'.txt';
	file_put_contents($filename, $myLog);
}
$handle = $server->handle();
echo ($handle);
?>

Note: The path of Zend Framework library is required to be able to use the classes. One of the silly tricky points was to get set_include_path right! Sometimes such things take hours to find out! You must have noticed my error logging function because the communication happens in the background and developing and debugging this side might be painful; just check the output files inside /tmp/ folder.

Next is the MXML application to use our PHP Zend AMF service. Let’s define how it can communicate with our service.
/phpShareFlex/flex/services-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<services-config>
    <services>
        <service id="zend-service"
            class="flex.messaging.services.RemotingService"
            messageTypes="flex.messaging.messages.RemotingMessage">
            <destination id="zend">
                <channels>
                    <channel ref="myZendChannel"/>
                </channels>
                <properties>
                    <source>*</source>
                </properties>
            </destination>
        </service>
    </services>
    <channels>
        <channel-definition id="myZendChannel"
            class="mx.messaging.channels.AMFChannel">
            <endpoint uri="http://www.muratyaman.co.uk/phpShareFlex/service/index.php"
                class="flex.messaging.endpoints.AMFEndpoint"/>
        </channel-definition>
    </channels>
</services-config>

Note: Destination id will be required by the RemoteObject in our MXML application. The type of channel we used is AMFChannel.
/phpShareFlex/flex/phpShareFlex.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
 pageTitle="phpShareFlex" layout="absolute"
 creationComplete="Application_creationComplete()"
>
<mx:Script>
<![CDATA[
 
	import mx.collections.ArrayCollection;
	import mx.collections.SortField;
	import mx.collections.Sort;
 
	import mx.rpc.events.ResultEvent;
	import mx.rpc.events.FaultEvent;
 
	import mx.utils.ObjectUtil;
 
	private function logMsg(msg:String):void{
		txtLog.text += msg;
	}
	private function svcShareService_getShares_fault( event:FaultEvent ) : void {
		logMsg('svcShareService_getShares_fault() ' + faultInfo(event) + '\n');
	}
	private function Application_creationComplete() : void {
		try{
			logMsg('BEGIN Application_creationComplete()' + '\n');
			logMsg('Calling svcShareService.getIndexes()' + '\n');
			svcShareService.getIndexes();
			logMsg('Calling svcShareService.getTop()' + '\n');
			svcShareService.getTop();
			logMsg('END Application_creationComplete()' + '\n');
		}catch(e:Error){
			logMsg('ERROR Application_creationComplete():\n    ' + e.message + '\n');
		}
	}
	private function svcShareService_fault(event:FaultEvent):void{
		logMsg('svcShareService_fault() ' + faultInfo(event) + '\n');
		trace('svcShareService_fault');
	}
	private function call_svcShareService_getShares() : void {
		logMsg('BEGIN call_svcShareService_getShares()' + '\n');
 
		var myIndex:Object = cboIndexes.selectedItem;
		var myIndexCode:String = myIndex.data;
 
		var myTop:Object = cboTop.selectedItem;
		var myTopNum:Number = myTop.data;
 
		var myMethodsArr:Array;
		myMethodsArr = new Array();
		if(chkLP.selected) myMethodsArr.push("LP");
		if(chkLL.selected) myMethodsArr.push("LL");
		if(chkLV.selected) myMethodsArr.push("LV");	
 
		logMsg('Calling svcShareService.getShares()' + '\n');
		svcShareService.getShares(myIndexCode, myMethodsArr, myTopNum);
		logMsg('END call_svcShareService_getShares()' + '\n');
	}
	private function svcShareService_getIndexes_result( event:ResultEvent ) : void {
		logMsg('svcShareService_getIndexes_result()' + '\n');
		cboIndexes.dataProvider = event.result;
	}
	private function svcShareService_getIndexes_fault(event:FaultEvent):void {
		logMsg('svcShareService_getIndexes_fault() ' + faultInfo(event) + '\n');
	}
	private function svcShareService_getTop_result( event:ResultEvent ) : void {
		logMsg('svcShareService_getTop_result()' + '\n');
		cboTop.dataProvider = event.result;
	}
	private function svcShareService_getTop_fault(event:FaultEvent):void {
		logMsg('svcShareService_getTop_fault() ' + faultInfo(event) + '\n');
	}
	private function btnGetShares_click(event:Event):void{
		logMsg('btnGetShares_click()' + '\n');
		call_svcShareService_getShares();
	}
	private function svcShareService_getShares_result( event:ResultEvent ) : void {
		logMsg('svcShareService_getShares_result()' + '\n');
		dgShares.dataProvider = event.result;
	}
	private function faultInfo(event:FaultEvent):String{
		var s:String = event.fault.faultString + ' Msg: ' + event.fault.message + ' Status: ' + event.statusCode;
		return s;
	}
	private function sortNumber(d1:Number, d2:Number):int{
		if(d1 < d2) {
			return -1;
		} else if(d1 == d2) {
			return 0;
		}
		return 1;
	}
	private function myParseFloat(strNumber:String):Number{
		var s:String = strNumber;
		s = s.split(",").join("");
		s = s.split("%").join("");
		var f:Number = parseFloat(s);
		return f;
	}
	private function sortDate(obj1:Object, obj2:Object):int{
		return sortNumber( (new Date(Date.parse(obj1.date))).getTime(), (new Date(Date.parse(obj2.date))).getTime());
	}
	private function sortNumber_price(obj1:Object, obj2:Object):int{
		return sortNumber( myParseFloat(obj1.price), myParseFloat(obj2.price));
	}
	private function sortNumber_change(obj1:Object, obj2:Object):int{
		return sortNumber( myParseFloat(obj1.change), myParseFloat(obj2.change));
	}
	private function sortNumber_volume(obj1:Object, obj2:Object):int{
		return sortNumber( myParseFloat(obj1.volume), myParseFloat(obj2.volume));
	}
	private function shareChart(sign:String):void{
		var url:String = 'http://uk.ichart.yahoo.com/z?s=' + sign + '&t=3m&q=b&l=on&z=m&p=e5,e20&a=ss,vm';
		imgShareChart.source = url;
	}
	private function dgShares_itemClick(event:Event):void{
		logMsg('dgShares_itemClick()' + '\n');
		var code:String = dgShares.selectedItem.code;
		lblShareCode.text = code;
		shareChart(code);
	}
	private function lnkCompanyInfo_click(event:Event):void{
		logMsg('lnkCompanyInfo_click()' + '\n');
		var url:String = 'http://uk.finance.yahoo.com/q/pr?s=' + lblShareCode.text;//company profile
		//ExternalInterface.call( "myNewWindow("+url+")" );
	}
]]>
</mx:Script>
 
<mx:RemoteObject id="svcShareService" source="MyShareService" destination="zend" showBusyCursor="true" fault="svcShareService_fault(event)">
	<mx:method name="getIndexes" result="svcShareService_getIndexes_result(event)" fault="svcShareService_getIndexes_fault(event)" />
	<mx:method name="getTop" result="svcShareService_getTop_result(event)" fault="svcShareService_getTop_fault(event)" />
	<mx:method name="getShares" result="svcShareService_getShares_result(event)" fault="svcShareService_getShares_fault(event)" />
</mx:RemoteObject>
 
<mx:Panel width="95%" height="95%" layout="vertical" title="phpShareFlex"
	horizontalAlign="center" horizontalCenter="0" verticalCenter="0"
	paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5">
	<mx:VDividedBox width="100%" height="100%">
		<mx:HDividedBox width="100%" height="90%">
			<mx:Canvas label="Criteria" width="15%" height="100%">
				<mx:VBox width="100%" height="90%">
					<mx:ComboBox id="cboTop" />
					<mx:Label text="shares from" />
					<mx:ComboBox id="cboIndexes" />
					<mx:Label text="Using" />
					<mx:CheckBox id="chkLV" label="Last Volume" selected="true" />
					<mx:CheckBox id="chkLP" label="Last Profit %" selected="true" />
					<mx:CheckBox id="chkLL" label="Last Loss %" selected="false" />
					<mx:Button id="btnGetShares" label="Get Shares" click="btnGetShares_click(event)" />
				</mx:VBox>
			</mx:Canvas>
			<mx:Canvas label="Shares" width="55%" height="100%">
				<mx:VBox width="100%" height="100%">
					<mx:DataGrid id="dgShares" width="100%" height="100%" itemClick="dgShares_itemClick(event)" >
					<mx:columns>
						<mx:DataGridColumn headerText="Code" dataField="code" />
						<mx:DataGridColumn headerText="Price" dataField="price" sortCompareFunction="sortNumber_price" />
						<mx:DataGridColumn headerText="Change %" dataField="change" sortCompareFunction="sortNumber_change" />
						<mx:DataGridColumn headerText="Volume" dataField="volume" sortCompareFunction="sortNumber_volume" />
					</mx:columns>
					</mx:DataGrid>
				</mx:VBox>
			</mx:Canvas>
			<mx:Canvas label="Information from Yahoo!" width="30%" height="100%">
				<mx:VBox width="100%">
					<mx:Label text="Chart of Last 3 Months" />
					<mx:HBox width="100%">
						<mx:Label text="Selected share:" />
						<mx:Label id="lblShareCode" text="" />
						<mx:LinkButton id="lnkCompanyInfo" label="More info" color="#0000FF" fontWeight="bold" click="lnkCompanyInfo_click(event)"/>
					</mx:HBox>
					<mx:Image id="imgShareChart" width="100%" />
				</mx:VBox>
			</mx:Canvas>
		</mx:HDividedBox>
		<mx:VBox width="100%" height="10%">
			<mx:TextArea id="txtLog" width="100%" height="100%" text="" />
		</mx:VBox>
	</mx:VDividedBox>
</mx:Panel>
</mx:Application>

Note: Like in other programming languages, there are various ways of doing the same thing: for example, I tried to stick to writing event handling functions first and then attaching them in object attributes like the click event of a button, rather than doing the job inside the MXML tags. I believe it is easier to understand: separation of code section and user interface section, if you see what I mean ;)

To compile this MXML application code, I have a copy of it and the services-config file on my PC and my Flex SDK is under:
c:\MyApps\FlexSDK\

So, the build.bat file is simply:

C:\MyApps\FlexSDK\bin\mxmlc.exe -services services-config.xml phpShareFlex.mxml

Note: The services switch tells the compiler custom service definitions. Once the phpShareFlex.swf file is created, you can upload it to your web server; mine is under:
www.muratyaman.co.uk/phpShareFlex/phpShareFlex.swf

Now, let’s present this to the world wide web either in a HTML page or PHP page; mine is:
www.muratyaman.co.uk/phpShareFlex/index.php

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>phpShareFlex</title>
<script src="AC_OETags.js" language="javascript"></script>
<script language="JavaScript" type="text/javascript">
<!--
	//to be called by flex objects like link buttons
	function myNewWindow(url){
		window.open(url);
		return true;
	}
// -->
</script>
<style>
body { margin: 0px; overflow:hidden }
</style>
</head>
 
<body scroll='no'>
<script language="JavaScript" type="text/javascript">
<!--
	AC_FL_RunContent(
					"src", "phpShareFlex",
					"width", "100%",
					"height", "100%",
					"align", "middle",
					"id", "phpShareFlex",
					"quality", "high",
					"bgcolor", "#869ca7",
					"name", "phpShareFlex",
					"allowScriptAccess","sameDomain",
					"type", "application/x-shockwave-flash",
					"pluginspage", "http://www.adobe.com/go/getflashplayer"
	);
// -->
</script>
<noscript>
	<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
			id="phpShareFlex" width="100%" height="100%"
			codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
			<param name="movie" value="phpShareFlex.swf" />
			<param name="quality" value="high" />
			<param name="bgcolor" value="#869ca7" />
			<param name="allowScriptAccess" value="sameDomain" />
			<embed src="phpShareFlex.swf" quality="high" bgcolor="#869ca7"
				width="100%" height="100%" name="phpShareFlex" align="middle"
				play="true"
				loop="false"
				quality="high"
				allowScriptAccess="sameDomain"
				type="application/x-shockwave-flash"
				pluginspage="http://www.adobe.com/go/getflashplayer">
			</embed>
	</object>
</noscript>
</body>
</html>

Note: Main issue could be the cross site security issue (errors like “Send failed”) when RemoteObject is calling our Zend AMF service, so stick both on to the same domain/website. The second issue during development is that you have to clear the browser’s cache to see the latest version of your FLEX application.

In order not to increase the complexity of this sample tutorial about Flex and PHP Zend/AMF, I have not included the actual code that gathers the share data from Yahoo Finance, which lets you download a CSV file, given sign of a quote or share with date intervals. I have already done it in phpStockProfiler including more functions to save it locally, converting into arrays and working on the data.

Ideally, I want to improve this application to achieve the following diagram:

flex-app-structure

Main improvement is the processing of external data source (public CSV data from Yahoo Finance) and optimizing data flow by using a database. Next one could be a kind of queuing mechanism for concurrent requests and not to do the same calculations twice, at least for the current day, maybe. Any suggestions are welcome.

Happy coding!

Tagged with: