Lubię CakePHP. Może z sentymentu, bo od niego zaczęłam moja przygodę z web developmentem. Jak mam coś napisać w PHP to zawsze najpierw sięgam po ten sprawdzony framework. Społeczność rozwijająca Cake’a zadbała o to, by można było połączyć aplikacje Flash-owe z frameworkiem za pomocą protokołu amfphp. Pluginów powstało kilka, ale w tym wpisie opisze w jaki sposób skorzystać z CpAmf.

Zacznijmy od tego, co nam będzie potrzebne. Przed wszystkim potrzebujemy samego cakePHP. Można go ściągnąć ze strony projektu: cakephp.org (w tym tutorialu korzystam z wersji 1.2). Umieszczamy go w katalogu cakephp na serwerze. Konfigurację Cake’a pozostawiam już Tobie. Jeżeli nie wiesz jak to zrobić, zajrzyj do oficjalnej dokumentacji.

Następnie ściągamy sam plugin: CpAmf, rozpakowujemy i wrzucamy katalog cpamf do /app/plugins frameworka.Możemy teraz sprawdzić, czy wszystko działa. Wpisujemy w przeglądarce adres: http://localhost/cakephp/cpamf/gateway. powinnieneś zobaczyć ekran z informacją: “amfphp and this gateway are installed correctly. You may now connect to this gateway from Flash”

Mój tutorial powstał na postawie oryginalnego tutoriala do pluginu. Też stworzymy prosta aplikację do zarządzania użytkownikami. Przy czym ja będę już pisać w Flex 4.

Ok, zajmijmy się najpierw przygotowaniem serwisu po stronie serwera. Bazę danych możesz ściągnąć tutaj. Jak widać baza zawiera tylko 1 tabelę users. Przechowuje ona takie informacje o użytkowniku jak jego imię, hasło, e-mail oraz datę dodatnia i aktualizacji danych.

Skoro mamy już tabelę, możemy utworzyć model. W katalogu model tworzymy plik user.php, a w nim podajemy następującą definicję klasy User:

class User extends AppModel {

	var $name = 'User';

        //walidacja
	var $validate = array(
	  'username' => array('notempty'),
	  'pass' => array('notempty')
	 );

        //tutaj jest zaszyta cała magia
	function afterFind( $results )
	{
		//mapowanie danych na obiekt lub tablice obiektów
		$resultObjects = Set::map( $results );
		foreach ( $resultObjects as &$item )
		{
			// Ustawienie właściwości _explicitType dla wszystkich obiektów
			// aby dobrze się zmapowały na obiekty we Flex
			$item->_explicitType = $this->name;

			// właściwość _name_ jest ustawiana automatycznie przez
                        //Set::map(). Nie potrzebujemy jej w klasie Flex'owej
			unset ( $item->_name_ );

		}

		return $resultObjects;
	}
}

Teraz utworzymy kontroler. W katalogu controllers tworzymy plik users_controller.php, a w nim umieszczamy definicję klasy:

class UsersController extends AppController {

	var $name = 'Users';

	function index() {

	}

        //zwraca listę wszystkich użytkowników
	function getAllUsers()
	{
		$user = $this->User->find('all');

		// Tworzy ArrayCollection z array
		// (/amfphp/services/vo/ArrayCollection.php)
		$userCollection = new ArrayCollection($user);
		return $userCollection;
	}

        //zwraca użytkownika o podanym id
	function getUser( $id = 0 )
	{
		$user = $this->User->read( null, $id );	

		return $user;
	}

        //dodaje nowego użytkownika
	function insertUser( $user )
	{
		return $this->User->save($user);

	}

}

Jak widać nasz kontroler posiada trzy metody, które operują na użytkownikach.

Po stronie serwera to już wszystko co musimy napisać. Przejdźmy zatem do Flex’a. Utwórzmy nowy projekt, powiedzmy CakeTest. Ponieważ po stronie serwera mamy model User, po stronie Flex’a musimy stworzyć klasę, która mu odpowiada. Stwórzmy ją zatem: tworzymy katalog models, a w nim następującą klasę User.as:

package models
{
	[RemoteClass(alias="User")]
	[Bindable]
	public class User
	{

		public var id:int;
		public var username:String;
		public var pass:String;
		public var email:String;
		public var created:String;
		public var modified:String;

		public function User()
		{
		}
	}
}

[RemoteClass(alias="User")] – za pomocą tych metadanych osiągniemy mapowanie obiektów.

Kolejną rzeczą o jaką musimy zadbać to konfiguracja naszego serwisu. Flex nie komunikuje się bezpośrednio z naszym serwisem po stronie serwera. Zamiast tego Flex “puka” do bramy (plik gateway.php), czyli pliku, który zapewnia komunikację miedzy aplikacją Flex-ową a PHP-ową. Konfiguracja serwisu po stronie Flex’a polega właśnie na powiedzeniu Flex-owi, gdzie ma szukać tego pliku. W tym w głównym katalogu aplikacji Flex tworzymy plik services-config.xml, w którym umieszczamy następujący kod:

<?xml version="1.0" encoding="UTF-8"?>
<services-config>
	<services>
		<service id="amfphp-flashremoting-service"
			class="flex.messaging.services.RemotingService"
			messageTypes="flex.messaging.messages.RemotingMessage">

			<destination id="amfphp">
				<channels>
					<channel ref="my-amfphp"/>
				</channels>
				<properties>
					<source>*</source>
				</properties>
			</destination>
		</service>
	</services>
	<channels>

    <channel-definition id="my-amfphp" class="mx.messaging.channels.AMFChannel">
        <endpoint uri="http://localhost/cakephp/cpamf/gateway" class="flex.messaging.endpoints.AMFEndpoint"/>
    </channel-definition>

	</channels>
</services-config>

Jak widać ścieżkę do naszego gateway podajemy w znaczniku endpoint (to jest ta sama ścieżka, dzięki której przetestowaliśmy czy plugin działa prawidłowo).

Następnie z menu górnego wybieramy Project->Properties->Flex Compiler i do argumentów kompilatora dodajemy właśnie stworzony plik: -services services-config.xml

Przejdźmy do pliku naszej aplikacji (CakeTest.mxml). Na początek stworzymy obiekt DataGrid, który będzie wyświetlał listę wszystkich naszych użytkowników:


	<fx:Script>

			import models.User;

			import mx.collections.ArrayCollection;
			import mx.controls.Alert;

			[Bindable]
			public var myUserCollection:ArrayCollection = new ArrayCollection();	

			[Bindable]
			public var user:User = new User();

			protected function allUsersBtn_clickHandler(event:MouseEvent):void
			{
				userRemoteObject.getAllUsers();
			}

			public function getAllUsersResult(result:*):void
			{
				myUserCollection = result;
			}

			public function getAllUsersFault(faultString:String) : void
			{
				Alert.show( faultString, "Błąd!" );
			}

	</fx:Script>

	<fx:Declarations>
		<s:RemoteObject id="userRemoteObject"
						destination="amfphp"
						source="UsersController">

			<s:method name="getAllUsers" result="getAllUsersResult(event.result)" fault="getAllUsersFault(event.fault.faultString)"/>

		</s:RemoteObject>
	</fx:Declarations>

		<s:Group width="100%" height="100%">
			<s:layout>
				<s:VerticalLayout/>
			</s:layout>
			<s:Button id="allUsersBtn"
					  label="Pobierz listę użytkowników"
					  click="allUsersBtn_clickHandler(event)"/>

			<mx:DataGrid id="dataGrid" dataProvider="{myUserCollection}">
				<mx:columns>
					<mx:DataGridColumn headerText="id" dataField="id"/>
					<mx:DataGridColumn headerText="Username" dataField="username"/>
					<mx:DataGridColumn headerText="Email" dataField="email"/>
				</mx:columns>
			</mx:DataGrid>
		</s:Group>

Przede wszystkim tworzymy obiekt RemoteObject, któremu nadajemy identyfikator userRemoteObject. Właściwość source ustawiamy na nasz kontroler (UsersController). Definiujemy również jedną metodę w RemoteObject. Nadajemy jej właściwość name na “getAllUsers”. W przypadku, gdy operacja naserwerze powiedzie się zostanie wywołana metoda getAllUsersResult, a gdy otrzymamy błąd metoda getAllUsersFault.

Pobranie listy użytkowników nastąpi po kliknięciu w przycisk o identyfikatorze allUsersBtn. Zostanie wtedy wywołana funkcja allUsersBtn_clickHandler, w której mamy wywołanie metody getAllUsers obiektu RemoteObject (jest to metoda, którą zdefiniowaliśmy w naszym kontrolerze po stronie serwera). Jeżeli operacja powiedzie się, do zmiennej myUserCollection przypisujemy, to co otrzymaliśmy z serwera (przypominam, że w php ta metoda zwraca nam ArrayCollection). Zmienna myUserCollection jest przypisana jako dataProvider do dataGrid, zatem nasz dataGrid powinna wypełnić się danymi z bazy:

datagrid w Flex

Pierwszą funkcjonalność naszej aplikacji mamy zatem za sobą. Dodajmy teraz pozostałe dwie. Poniżej podam cały kod naszej aplikacji Flex-owej, tak byś miał pełen obraz sytuacji.

<fx:Script>

    import models.User;

    import mx.collections.ArrayCollection;
    import mx.controls.Alert;

    [Bindable]
    public var myUserCollection:ArrayCollection = new ArrayCollection();	

    [Bindable]
    public var user:User = new User();

    protected function allUsersBtn_clickHandler(event:MouseEvent):void
    {
        userRemoteObject.getAllUsers();
    }

    public function getAllUsersResult(result:*):void
    {
        myUserCollection = result;
    }

    public function getAllUsersFault(faultString:String) : void
    {
        Alert.show( faultString, "Error!" );
    }

    protected function getUserBtn_clickHandler(event:MouseEvent):void
    {
        userRemoteObject.getUser(new int(userId.text));
    }

    public function getUserResult(result:*):void
    {
        user = result;
        myUserCollection = new ArrayCollection();
        myUserCollection.addItem(user);
    }

    public function getUserFault(faultString:String):void
    {
        Alert.show(faultString, "Error!");
    }

    protected function addNewUser_clickHandler(event:MouseEvent):void
    {
        var newUser:User = new User();
        newUser.username = userName.text;
        newUser.pass = userName.text;
        newUser.email = email.text;
        userRemoteObject.insertUser(newUser);
    }

    public function insertUserResult(result:*):void
    {
        (result == false) ? Alert.show("Błąd przy dodawaniu nowego użytkownika") : Alert.show("Użytkownik został dodany!");
    }

    public function insertUserFault(faultString:String):void
    {
        Alert.show("Błąd");
    }

</fx:Script>

<fx:Declarations>
<s:RemoteObject id="userRemoteObject"
                destination="amfphp"
                source="UsersController">

    <s:method name="getAllUsers" result="getAllUsersResult(event.result)" fault="getAllUsersFault(event.fault.faultString)"/>
    <s:method name="getUser" result="getUserResult(event.result)" fault="getUserFault(event.fault.faultString)"/>
    <s:method name="insertUser" result="insertUserResult(event.result)" fault="insertUserFault(event.fault.faultString)"/>

</s:RemoteObject>
</fx:Declarations>

<s:Group width="100%" height="100%">
    <s:layout>
        <s:VerticalLayout/>
    </s:layout>
    <s:Button id="allUsersBtn"
              label="Pobierz listę użytkowników"
              click="allUsersBtn_clickHandler(event)"/>

    <mx:DataGrid id="dataGrid" dataProvider="{myUserCollection}">
        <mx:columns>
            <mx:DataGridColumn headerText="id" dataField="id"/>
            <mx:DataGridColumn headerText="Username" dataField="username"/>
            <mx:DataGridColumn headerText="Email" dataField="email"/>
        </mx:columns>
    </mx:DataGrid>
    <s:HGroup>
        <s:TextInput id="userId"/>
        <s:Button id="getUserBtn"
                  label="Wyślij"
                  click="getUserBtn_clickHandler(event)"/>
    </s:HGroup>
    <mx:Form id="addUserForm">
        <mx:FormItem label="User name">
            <s:TextInput id="userName"/>
        </mx:FormItem>
        <mx:FormItem label="Password">
            <s:TextInput id="userPassword"/>
        </mx:FormItem>
        <mx:FormItem label="e-mail">
            <s:TextInput id="email"/>
        </mx:FormItem>
    </mx:Form>
    <s:Button id="addNewUser"
              label="Dodaj"
              click="addNewUser_clickHandler(event)"/>
</s:Group>

Pozostałe dwie metody, które chcemy obsłużyć po stronie Flax’a to metoda getUser (pobiera użytkownika po identyfikatorze) oraz insertUser (dodająca do bazy nowego użytkownika). Znajduje to odzwierciedlenie w 2 metodach dodanych do obiektu RemoteObject. Myślę, że komentarza wymaga metoda addNewUser_clickHandler, która powoduje wstawienie nowego rekordu do bazy. Otóż, po stronie serwera zostanie odpalona metoda insertUser, która wymaga 1 argumentu. Z Flex’a wysyłamy obiekt usera, ale po stronie PHP nie mapujemy go na obiekt User, ale używamy po prostu tablicy asocjacyjnej.

Możliwe problemy

Korzystając z cakePHP 1.2 oraz wersji PHP 5.3 możesz się zdarzyć, że przy wykonywaniu dodawania do bazy nowego rekordu będziesz dostawać błąd: “Assigning The Return Value of New By Reference is Deprecated” (ja w każdym razie taki błąd dostawałam). Generalnie jakoś taka konfiguracja tej wersji frameworka z tą wersją PHP ma tendencję do rzucania tym błędem. Nie mam pojęcia dlaczego, bo nie jestem specem od PHP. W każdym razie, jeżeli dostaniesz taki błąd z serwera, są dwa wyjścia:

1. zmienić wersję PHP na np. 5.2.x (ja tak zrobiłam i problem zniknął)
2. podnieść wersję cakePHP na 1.3.

Nasza aplikacja ma dopiero 3 nowe funkcjonalności, a więc jeszcze wiele pracy przed Tobą. Miłej zabawy i …. smacznego! Flex + CakePHP całkiem nieźle smakuje!

Aktualizacja wpisu: jeżeli chcesz korzystać z tego pluginu na wersji CakePHP 1.3 niezbędne jest utworzenie odpowiedniego wpisu w pilku config/routes.php:

Router::connect(’/cpamf/:action/*’, array(’plugin’=>’cpamf’,'controller’ => ‘cpamf’));

Bez ustawienia routingu Cake głupieje, gdy w ścieżce podajemy mu cpamf.