Working with Doctrine 2, Flex, Zend AMF, and Flash Builder

I finally got some time to play with Doctrine 2 and Flex. Back in May I wrote an article about working with Doctrine 1.x and Flex (you can read the article here) and my feelings were mixed. I chatted with Jonathan Wage of Doctrine about some of the shortcomings I found in Doctrine 1.x and his response was to check Doctrine 2 (still in development at the time of writing this article). Doctrine 2 is a big step forward.

In this article I describe how I rewrote the original application I created for my first article, this time using Doctrine 2, Flex 4, Zend Framework, and the Flash Builder data-centric development wizards. I’ll highlight the relevant differences between Doctrine 1 and Doctrine 2 along the way. Thus, you should find this article valuable in any one of these two cases:

  • You are already working with Doctrine 1 and you’ve wondered what it would take to move to version 2
  • You want to learn how to use Doctrine 2 with Flex; you know PHP and you know enough Flex not to be scared away if you see some snippets of code

Before going into the details let me say this: if you aren’t already using an ORM framework for PHP then you should. For most projects it can help you by freeing you of the tedious tasks of writing CRUD code and SQL queries. It allows you to focus on the business logic of your application. And all these advantages are multiplied when working on Rich Internet Applications because on this kind of project much of the work is done on the client and not on the server.

There are some aspects of using this ORM with RIA that could be better, but hey we don’t live in a perfect world. Most of these things, I think, are related to the fact that every time you use a server side ORM with a rich client, you leave behind the main story used for creating that framework – you use the ORM in order to feed a rich client with data and enable the client to persist the changes. Thus you need additional boiler plate code to make the whole thing work.

If you don’t know much about ORMs in general, you may want to read my first article first and before continuing.

The Big Picture

Here is a screenshot of the application I built running in the browser:

figure-1

below is the database structure. The only difference between this database and the one used in the previous article is the addition of a simple primary key to the marks table.

figure-2

While this is by no means a complex application, I think it’s a decent one in terms of relations between tables: I have a number of courses and countries (courses and countries tables), and each student belongs to a country (many-to-one between students and countries) and receives marks for a number of courses (marks many-to-many table for students and courses tables).

The Flex application reads all the data stored in the MySQL database and lets the user fully edit students by performing the following operations:

  • Add, edit, delete students
  • Change the country for a student
  • Change the courses taken by a student and assign marks for a course

And here is an overview of the different parties involved in this application:

figure-6

Differences between Doctrine 2 and 1.x

Before explaining how I built the app, let’s talk a little bit about the main differences between Doctrine 2 and Doctrine 1.x (as I’m writing this article Doctrine 2 is still in Beta, so things could change).

The biggest change by far is the one related to the main pattern used by the Doctrine 2 ORM. In 1.x versions it used the Active Record pattern (Ruby on Rails uses Active Record too), now you can say goodbye to Active Record and welcome Data Mapper (Hibernate uses the same pattern). What are the differences between these two patterns?

With Active Record the entities know how to persist themselves to the database; basically each entity extends some sort of a class from the ORM framework and implements methods like read(), find(), delete(), and update(). Although it is not mandatory, the entities look very much like the database structure.

With Data Mapper entities know nothing about the persistency layer and nothing about the database structure. The ORM provides the means to persist the entities and read the data into entities (from the database).

From a Rich Client perspective this translates in less work on the PHP side when preparing data for sending across the wire while using Doctrine 2. With Doctrine 1, the data model was heavy due to the Active Record pattern. Thus I had to create a plain vanilla data model used to send the data to the Flex client. In order to efficiently transform the heavy entities used by Doctrine 1 into the plain vanilla ones used for sending the data to Flex, I had to write custom functions. When data came in from the Flex client, the reverse process was needed: use the plain vanilla objects to build the Doctrine entity objects. An alternative could be to send all the data to Flex as arrays. Unfortunately, this approach doesn’t work out of the box; you have to write functions to transform the graph of objects into a graph of arrays/associative arrays.

With Doctrine 2, I don’t need this extra layer of simple value objects and I can return the data as a graph of objects or arrays with the built-in capabilities.

The second big difference is that Doctrine 2 requires PHP 5.3 or newer. Thus, if your setup requires older versions of PHP, then you have to stick with Doctrine 1.x.

Of course the ripples stirred by these two changes are quite big and I think it is safe to say that when moving from Doctrine 1.x to 2 you won’t reuse much of your previous experience with Doctrine 1.x or the code you wrote.

Having said this, I have to say that I, for one, am happy with the evolution of Doctrine, because I favor Data Mapper over Active Record.

Installing Doctrine 2 and creating the PHP project

First of all you need to get the Doctrine framework. There are four different ways (PEAR , Package Download, GitHub, or SVN). Depending on what method you use the configuration of Doctrine will differ a little bit. I used GitHub for my project, and I pulled out the code outside of Apache’s web root.

Speaking of the project, I used Eclipse PDT with Flash Builder 4 (I installed the Flash Builder plug-in on top of Eclipse PDT). The first step after getting the Doctrine framework was creating a PHP project named students-doctrine2. Next I added Flex nature to the project by right-clicking on the project name and choosing Add/Change Project Type… > Add Flex project type from the contextual menu. Make sure you select PHP for the Application Server type and you fill in the path and URL for your web root.

The next step was to create a folder inside the web root where I’ll put the PHP services, entities, and Doctrine’s configuration files: doctrine2_students. Inside of it, I created three folders named entities, proxies, and services. And finally, I created a linked resource between doctrine2_students folder and my Eclipse project (right-click on the project, choose New > Folder and then click on Advance button and navigate to the folder location). With that I was ready to write PHP and Flex code.

The next step is to create a bootstrap file (I call it bootstrap.php and place it inside of the doctrine2_students folder) that configures Doctrine 2 to be usable with my project. This means to load the framework classes, set up database access information, specify entities location and annotation method, and configure the different caches that will be used by the app. In the same bootstrap file I created an instance of the EntityManager class. This is the entry point to the Doctrine 2. If you download the project source code (see the links from the second to last section) you’ll find the bootstrap.php file inside of the doctrine2_students folder. The file looks like this:

<?php
use Doctrine\ORM\EntityManager,
    Doctrine\ORM\Configuration;

$applicationMode = 'development';

//Doctrine Git bootstrap
$lib = '/Users/mcorlan/Documents/work/_git/doctrine/doctrine2/lib/';
require $lib . 'vendor/doctrine-common/lib/Doctrine/Common/ClassLoader.php';

$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\Common', $lib . 'vendor/doctrine-common/lib');
$classLoader->register();

$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\DBAL', $lib . 'vendor/doctrine-dbal/lib');
$classLoader->register();

$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\ORM', $lib);
$classLoader->register();

//additional Symphony components for Doctrine-CLI Tool, YAML Mapping driver
$classloader = new \Doctrine\Common\ClassLoader('Symfony', $lib . 'vendor/');
$classloader->register();

//load entities
$classloader = new \Doctrine\Common\ClassLoader('entities', __DIR__);
$classloader->register();
$classLoader = new \Doctrine\Common\ClassLoader('proxies', __DIR__);
$classLoader->register();

//load services
$classLoader = new \Doctrine\Common\ClassLoader(null, __DIR__ . '/services');
$classLoader->register();

if ($applicationMode == 'development') {
    $cache = new \Doctrine\Common\Cache\ArrayCache;
} else {
    $cache = new \Doctrine\Common\Cache\XcacheCache();
}

$config = new Configuration;
$config->setMetadataCacheImpl($cache);
$driverImpl = $config->newDefaultAnnotationDriver(__DIR__ . '/entities');
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCacheImpl($cache);
$config->setProxyDir(dirname(__FILE__) .'/proxies');
$config->setProxyNamespace('doctrine2_students\proxies');

if ($applicationMode == "development") {
    $config->setAutoGenerateProxyClasses(true);
} else {
    $config->setAutoGenerateProxyClasses(false);
}
//database connection config
$connectionOptions = array(
    'driver' => 'pdo_mysql',
    'dbname' => 'students',
    'user' => 'mihai',
    'password' => 'mihai'
);

$GLOBALS['em'] = EntityManager::create($connectionOptions, $config);

Creating the PHP entities

With Doctrine 2 (and any ORM that uses the Data Mapper pattern) you have to specify how an entity is persisted by the framework and what relationships it has with other entities (if any). in Doctrine 2 you can choose from four different methods: annotations, YAML, XML, and plain PHP. I initially favored the first one because all the information is stored in the entities classes as PHPDoc comments. Thus if you want to modify an entity you have only one place to look for it. However, after using this approach I think the XML approach is best because you get code-completion hints. Here is the listing for the Course entity (remember I have four tables in my database and I need four entities for my application):

<?php
namespace entities;

/** @Entity 
 * @Table(name="courses") 
 */
class Course {

    /**
     * @Id @Column(type="integer")
     * @GeneratedValue(strategy="AUTO")
     */
    private $id;

    /** 
     * @Column(type="string", length=255) 
     */
    private $name;

    public function getId() {
        return $this->id;
    }

    public function getName() {
        return $this->name;
    }

    public function setName($val) {
        $this->name = $val;
    }
}

Using annotations you specify what table is used for the entity (remember one row from that table will be wrapped in one instance of the entity). You can set different names for the properties if you want (in SQL you don’t use camelCase notation, but in PHP or ActionScript typically you use this convention).

The entities I created closely follow the structure of the database. The only difference is in how the foreign keys are represented. For example, the Student entity, which has a many-to-one relation with the Country entity, doesn’t have a property country_id of type int. Instead, I added a property called country that is of Country type. Similarly, I created a property called marks that holds an array of Mark entities – if a student attends three courses, then its marks property will hold an array of Mark objects with three instances.

Here are some notes on creating entities with Doctrine 2:

  • With Doctrine 1 I used the built-in tools to create the domain model from the database structure
  • Doctrine 2 has support to create the YAML out of the database schema, and then generate the entities; however I’m not sure it is good idea to do this. If you have complex schemas the generated code might not worked as you expect, and you’ll need to tweak it manually anyway
  • You have to remember to set the properties as private/protected and not public, and add getters/setters. If you fail to do this, you might get nasty bugs (Doctrine will have problems injecting the code to handle relations).

Creating the PHP services

With the four entities in place, it is time to create the PHP services. These will be the services used by the Flex client to get and persist data. Basically, using the Zend Framework I’ll be able to invoke remote procedure calls on these objects from the Flex side.

Inside the doctrine2_students/services/ folder I created four PHP files: CountriesService.php, CoursesService.php, MarksService.php, and StudentsService.php.  I edited the bootstrap.php file to load the services folder along with the rest of the files. Here is the listing for the CountriesService class:

<?php
require_once(__DIR__.'/../bootstrap.php');

class CountriesService {

    public function __construct() {
        $this->entityManager = $GLOBALS['em'];
    }

    public function getCountries() {
        $q = $this->entityManager->createQuery('SELECT c FROM entities\Country c ORDER BY c.name');
        return $q->getArrayResult();
    }

}

As you might expect, the complex code is inside of StudentsService class; here’s the public API:

class StudentsService {

    //returns all the students
    public function getStudents() {
        ...
    }
    //save changes for an existent student, or insert a new one
    public function saveStudent($student) {
        ...
    }
    //deletes a student
    public function deleteStudent($student) {
        ...
    }
}

As I said before, the entry point to Doctrine 2 is its EntityManager class. You can use this class to query for persistent objects using different methods. The most powerful method is Doctrine Query Language (DQL) which resembles SQL but works on the entities you’ve defined in your domain model rather than on the underlying tables.

If, for example, you want to retrieve the country with the id equal to 1, you could use this code:

$id = 1;
$dql = 'SELECT c FROM entities\Country c WHERE id = ?1';
$query = $entityManager->createQuery($dql);
$query->setParameter(1, $id);

$countryEntity = $query->getResult();

If you want to change the name for this country, you’d write this code:

$countryEntity->setName('new name for your country');
//persist the changes to database
$entityManager->flush();

If you want to create a new country, you’d write this code:

$countryEntity = new entities\Country();
$countryEntity->setName('Mihai\'s country');
//set the entity to be managed by Doctrine
$entityManager->persist($countryEntity);
//persist the changes to database
$entityManager->flush();

Whith DQL when you write a join, it can be a filtering join (similar to the concept of join in SQL used for limiting or aggregating results) or a fetch join (used to fetch related records and include them in the result of the main query). When you include fields from the joined entity in the SELECT clause you get a fetch join. Here is the code for the StudentsService->getStudents() method:

$dql = "SELECT s, c, m, e FROM entities\Student s 
            JOIN s.country c JOIN s.marks m
            JOIN m.course e ORDER BY s.lastName, s.firstName";

$q = $this->entityManager->createQuery($dql);
return $q->getArrayResult();

Each student is retrieved along with the country he belongs to and all of his courses from the many-to-many table – all with a single DQL query. And if you print_r() the result you’ll see a structure along these lines:

figure-3

In addition, when you create these kinds of queries with fetch joins and you use getArrayResult() method on the query object instead of getResult(), you get a nice array or associative array of other arrays. These data are ready to be sent to the Flex client without any transformation.

The only part I haven’t discussed is how I handle the changes for a Student. I will talk about this as part of the Flex client.

For the communication between the Flex and PHP side I’ll use remoting. On the PHP side I need Zend AMF in order to enable remoting. But for the purposes of this article I will let Flash Builder 4 handle this (installing Zend Framework and creating the gateway for exposing the four PHP services I created).

Creating the Flex client

With the server code in place, it is time to add the Flex code. In the previous article I wrote all client code manually. This time I’ll use the data-centric development features of Flash Builder to introspect PHP classes and create the service wrappers as well as the ActionScript value objects.

The easiest way to do this, is to first create the four services, and then to define the return types for the getStuff() operations. Follow these steps:

  1. From the Data/Services view click the Connect to Data/Service link for the first service or on the third icon (the one with a + sign) for the other three services
  2. When the wizard opens, select PHP, and click Next. If it is your first time using the wizard for PHP, then you’ll be presented with a dialogue to install the Zend Framework
  3. Click Browse and select the first PHP service
  4. Repeat these three steps until you have defined all the four services. Please note that you can change the packages where the services and the value objects will be created

figure-4

Now, it is time to define the value objects I’ll use on the Flex side. Again I’ll use the data-centric development features:

  1. Because the StudentsService returns a complex type that uses Student, Course, Country, and Mark it is important to start defining the return types first with CountriesService and CoursesService, then for MarksService, and finally for StudentsService
  2. To define the return type for an operation on as service, expand the tree for the service, and right-click the operation (for example, select getCountries() from CountriesService) and choose Configure Return Type from the context menu. When the wizard opens, make sure the first option is selected on the first screen (Auto-detect the return type from the sample data) and click Next
  3. On the second page you can enter a name for the value object class (for example Country)
  4. The most complex type is the return type for the StudentsService.getStudents() method. For this one, on the second page of the wizard you need to expand the nodes and choose for the type column, the types you defined earlier (Course, Country, or Mark)

Here is how the Student value object is presented in the Data/Services view after finishing the above steps:

figure-5

With the service wrappers and value objects in place, it is time to take care of the application UI and put these files to use. For this task I reuse most of the code I wrote in my previous application. I had to tweak some of the methods a little (for example, saveStudent() and onStudentSelection()).

When the application starts, the first thing I want it to do is load the courses, countries, and students. To do so, I created an init() function and registered it on the creationComplete event of the application. Then, I selected the getStudents() method from the Data/Services view, right-clicked it, and chose Generate Service Call. This command adds to the code an instance of StudentsService, an instance of CallResponder (you use this object to retrieve the result using the lastResult property or to register a result/fault listener for that operation), and a method that makes the call to the selected operation and assigns the token returned by the operation to the token property of the CallResponder object:

private function getStudents():void {
    getStudentsResult.token = studentsService.getStudents();
}
<s:CallResponder id="getStudentsResult"/>
<services:StudentsService id="studentsService"
fault="Alert.show(event.fault.faultString + '\n' + event.fault.faultDetail)" showBusyCursor="true"/>

Another change from the first article is the use of bidirectional binding for the firstName, lastName, and registration fields of the form.

Here is the complete code of the Main.mxml where the magic happens:

<?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:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" xmlns:services="org.corlan.services.*"
               creationComplete="init()" xmlns:entities="org.corlan.entities.*">
    <fx:Declarations>
        <s:CallResponder id="getStudentsResult"/>
        <services:StudentsService id="studentsService" fault="Alert.show(event.fault.faultString + '\n' + event.fault.faultDetail)" showBusyCursor="true"/>
        <s:CallResponder id="getCountriesResult"/>
        <services:CountriesService id="countriesService" fault="Alert.show(event.fault.faultString + '\n' + event.fault.faultDetail)" showBusyCursor="true"/>
        <s:CallResponder id="getCoursesResult" result="onCourses()"/>
        <services:CoursesService id="coursesService" fault="Alert.show(event.fault.faultString + '\n' + event.fault.faultDetail)" showBusyCursor="true"/>
        <entities:Student id="student"/>
        <s:CallResponder id="deleteStudentResult" result="getStudents()"/>
        <s:CallResponder id="saveStudentResult" result="getStudents()"/>
    </fx:Declarations>
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;
            import mx.controls.Alert;
            import mx.events.FlexEvent;
            import mx.events.ListEvent;

            import org.corlan.entities.Country;
            import org.corlan.entities.Course;
            import org.corlan.entities.Mark;
            import org.corlan.entities.Student;

            private var marks:Array;

            private function onCourses():void {
                marks = new Array();
                var c:Array = (getCoursesResult.lastResult as ArrayCollection).toArray();
                for (var i:int=0; i<c.length; i++) {
                    marks.push(
                            {id : 0, mark : 0, course : c[i]}
                        );
                }
                dgMarks.dataProvider = marks;
            }

            private function customLabel(data:Object, column:DataGridColumn):String {
                var arr:Array = new Array();
                var c:int = (data as Student).marks.length;
                var mark:Mark;
                for (var i:int = 0; i<c; i++) {
                    mark = (data as Student).marks[i] as Mark;
                    arr.push(mark.course.name);
                }
                if (arr.length > 0)
                    return arr.join(", ");
                return "NONE";
            }

            private function init():void {
                getCountries();
                getCourses();
                getStudents();
            }

            private function getStudents():void {
                getStudentsResult.token = studentsService.getStudents();
            }

            private function getCountries():void     {
                getCountriesResult.token = countriesService.getCountries();
            }

            private function getCourses():void {
                getCoursesResult.token = coursesService.getCourses();
            }

            private function saveStudent():void {
                var m:Mark;
                student.marks = new ArrayCollection();
                for (var i:int = 0, l:int = marks.length; i<l; i++) {
                    if (marks[i]["mark"] > 0) {
                        m = new Mark();
                        m.course = marks[i]["course"];
                        m.mark = marks[i]["mark"];
                        student.marks.addItem(m);
                    }
                }
                student.country = country.selectedItem as Country;
                saveStudentResult.token = studentsService.saveStudent(student);
            }

            private function onStudentSelection(e:ListEvent):void {
                student = e.itemRenderer.data as Student;
                if (student.country) {
                    var l:int = getCountriesResult.lastResult.length;
                    for (var i:int = 0; i<l; i++) {
                        if (getCountriesResult.lastResult[i].id == student.country.id) {
                            country.selectedIndex = i;
                            break;
                        }
                    }
                }

                if (student.marks && student.marks.length > 0) {
                    var k:int = marks.length;
                    var o:Object;
                    for (var j:int = 0; j<k; j++) {
                        o = marks[j];
                        o["mark"] = 0;
                        for (var jj:int = 0; jj < student.marks.length; jj++) {
                            if ((student.marks[jj] as Mark).course.id == (o["course"] as Course).id) {
                                o["mark"] = student.marks[jj].mark;
                                break;
                            }
                        }
                    }
                    dgMarks.dataProvider = marks;
                }
            }

            private function deleteStudent():void {
                if (!student || !student.id)
                    return;
                deleteStudentResult.token = studentsService.deleteStudent(student);
            }
        ]]>
    </fx:Script>
    <mx:DataGrid id="dgStudents" x="13" y="14" dataProvider="{getStudentsResult.lastResult}"
                 itemClick="onStudentSelection(event)">
        <mx:columns>
            <mx:DataGridColumn headerText="First" dataField="firstName"/>
            <mx:DataGridColumn headerText="Last" dataField="lastName"/>
            <mx:DataGridColumn headerText="Country" dataField="country.name"/>
            <mx:DataGridColumn headerText="Courses" labelFunction="customLabel" width="200"/>
        </mx:columns>
    </mx:DataGrid>

    <s:Button click="getStudents()" label="Get Students" x="13" y="200"/>
    <s:Button x="112" y="200" label="New" click="student = new Student();"/>
    <s:Button x="190" y="200" label="Delete Selected Student" click="deleteStudent()"/>

    <mx:Form x="510" y="0">
        <mx:FormItem label="First:">
            <s:TextInput id="first" text="@{student.firstName}"/>
        </mx:FormItem>
        <mx:FormItem label="Last:">
            <s:TextInput id="last" text="@{student.lastName}"/>
        </mx:FormItem>
        <mx:FormItem label="Registration:">
            <mx:DateField id="registration" selectedDate="@{student.registration}"/>
        </mx:FormItem>
        <mx:FormItem label="Country:">
            <s:List id="country" dataProvider="{getCountriesResult.lastResult}" labelField="name" height="150"/>
        </mx:FormItem>
        <mx:FormItem label="Marks:">
            <mx:DataGrid id="dgMarks" editable="true">
                <mx:columns>
                    <mx:DataGridColumn headerText="Course" dataField="course.name" editable="false"/>
                    <mx:DataGridColumn headerText="Mark" dataField="mark" editable="true"/>
                </mx:columns>
            </mx:DataGrid>
        </mx:FormItem>
        <mx:FormItem>
            <s:Button label="Save" click="saveStudent()"/>
        </mx:FormItem>
    </mx:Form>
</s:Application>

Now, when I call the saveStudent() method I make a call to the remote operation (saveStudent() from the StudentsService.php) and pass along an instance of the Student ActionScript class. In the PHP method (StudentsService->saveStudent()) I get an anonymous Object, so I had to manually build an instance of the Student entity and populate it with the data. Here is the complete code for the server side saveStudent() method:

public function saveStudent($student) {
    if ($student->id) { //update
        $entity = $this->entityManager->find('entities\Student', $student->id);
        if (!$entity)
            throw new Exception('Error saving student!');

        $marks = $entity->getMarks();
        foreach ($marks as $mark) { //update mark value for existent records
            $found = false;
            foreach ($student->marks as $record) {
                if ($mark->getCourse()->getId() == $record->course->id) {
                    $mark->setMark($record->mark);
                    $found = true;
                    $key = array_search($record, $student->marks, true); //remove the $record from array
                    if ($key !== false)
                        unset($student->marks[$key]);
                    break;
                }
            }
            if (!$found) { //remove current mark
                $entity->removeMark($mark);
                $this->entityManager->remove($mark);//remove mark from database
            }
        }
    } else { //insert
        $entity = new entities\Student();
        $this->entityManager->persist($entity);
    }

    $this->addNewMarks($entity, $student); //add new marks if any

    $entity->setFirstName($student->firstName);
    $entity->setLastName($student->lastName);
    $d = new DateTime();
    $d->setTimestamp($student->registration->getTimestamp());
    $entity->setRegistration($d);
    $country = $this->entityManager->find('entities\Country', $student->country->id);
    if (!$country)
        throw new Exception('Error saving student; invalid country!');
    $entity->setCountry($country);

    $this->entityManager->flush(); //save the student
}

If you think it is way to much code for this “simple” operation, then I think you are partially right. In the Conclusions section I touch on this. However you have to remember that this code handles many things: creating a new student, and inserting its marks in the Marks2 table or updating a student and its marks from the Marks2 table if they were changed (new courses added, grade changes and so on).

Instead the delete method is quite clean (remember that behind the scene removes all the related records from the marks many-to-many table):

public function deleteStudent($student) {
    $entity = $this->entityManager->find('entities\Student', $student->id);
    if (!$entity)
        throw new Exception('Error deleting student!');
    $this->entityManager->remove($entity);
    $this->entityManager->flush();
}

Getting the source code

If you’re curious and you want to explore the code, you can download the project from here. It contains the PHP doctrine2_students folder, the database SQL dump for creating the tables, and the Flex code.

The easiest way to import this project is just to extract the folders (you import the project in Flash Builder). Then place the PHP code (doctrine2_students folder) inside of the web root folder. Get the Doctrine 2 files, and then reconfigure the doctrine2_students/bootstrap.php file to define the Doctrine 2 location and update the database credential. And finally, create a new Flex/PHP project, and repeat the first part of the article (creating the Flex wrapper services and value objects).

Conclusions

Overall, I think Doctrine 2 makes it easier to work on PHP and Flex projects. I especially love the new Data Mapper approach and its flexibility/power. The entities are very light and you can easily use DQL in conjunction with getArrayResult() to build a data structure ready for sending to Flex. Also there is no need for all the plumbing work I did for Doctrine 1 in order to send the objects on the PHP side. I encourage you to read the excellent documentation you’ll find on Doctrine site to better understand the inner workings, the different types of associations, and in general the features it offers.

With Doctrine 2 you get a big boost in terms of writing the PHP services and exposing the data to the Flex client. And if you think about it, the server side is not the place where the most the effort goes in. So it is a good thing to have a framework that standardize the PHP code and helps you big time when retrieving and persisting data. However, you can tell that it is not a framework architected with rich clients in mind (and there is nothing bad in this, I mean it’d be the same with the most of the frameworks out there). As easy it is retrieving data from the underlying persistence layer and sending them to a rich client, as hard persisting the client changes is. You have to write custom code to create the PHP entities out of the data received from the client before being able to persist the changes. What I feel it is missing is a way to use a data structure (for example an associative array) as the source for creating the PHP entities (more on this below).

Another interesting departure from the Doctrine 1 example is that I didn’t create an exact match between the ActionScript and PHP entities. When I designed the two sides of the equation, I had in mind the best domain model to serve the Flex client because all the information is edited on the client. Then I used the getArrayResult() method to send associative arrays to Flex (which are deserialized into Objects in Flex).

A feature I stayed away instinctively (both with Dotrine 1 and 2) was the ability to generate the database schema using the entities and the mapping between them. In other words, you could start your project first writing the PHP data model, and then use Doctrine to generate the database for you. I’m old school and all my experience taught me that relational databases treat you well if you treat them well. Thus, I much prefer to create the database with my “own” hands and make sure I set all the indexes/constraints I need. Having said this, I’m not saying that the Doctrine feature for generating database schema is buggy or worthless. I just wanted to explain why I haven’t tried. Your mileage could vary on this one.

The data-centric development features of Flash Builder simplified the creation of the data model a lot. I’m not sure if they work for any kind of project, but in this case I didn’t see any drawbacks.

You have very little work to do when handling the Delete and Read part of the CRUD operations on the server side. However, with Create/Update things change, especially when the object has associations (many-to-many, one-to-many, many-to-one with other entities). I thought it would be enough to retrieve the existing Student data from Doctrine and call the removeMark() method to remove a Mark. In fact, doing this doesn’t delete the entry from the many-to-many table. Instead, you have to explicitly remove the Mark instance from the Student and from the entityManager:

$student->removeMark($mark);
$this->entityManager->remove($mark);

Doctrine 2 offers a Cascade feature for persist/remove. For example, here I defined cascade delete/update for the Marks entities on the Student object:

class Student {

...
    /**
    * @OneToMany(targetEntity="Mark", mappedBy="student", cascade={"persist", "remove"})
    */
    private $marks;
...
}

However, I found that they actually work perfectly only on delete (see the previous paragraph for why I don’t believe the persist is fully functional). It is possible that I didn’t understand the usage, and I was expecting more than intended or I just messed up something…

Another small glitch was related to the composite primary keys. When I tried to follow Doctrine’s documentation and annotate the Mark entity to compose the primary key out of student_id and course_id, I got a runtime error. So I had to alter the table and add an auto increment primary key.

The only other thing I didn’t like was the date handling. When you send a Date object to the PHP side, on the PHP side you get a Zend_Date object (when using the Zend Framework). And because Doctrine 2 knows how to handle only the PHP DateTime object, you have to manually handle the transformation. It would be cool to either configure in some way Zend Framework to use PHP DateTime instead of Zend_Date or to have Doctrine 2 handle this for you :D

I had tons of fun while writing this article and playing with this new technology. I can say that Doctrine 2 rocks! If you give Doctrine 2 a try with Flex, please drop a comment with your impressions.

27 thoughts on “Working with Doctrine 2, Flex, Zend AMF, and Flash Builder

  1. Pingback: Working with Doctrine 1.x, Zend Framework, and Flex : Mihai Corlan

  2. Dear Mr. Corlan,

    Thank you very much for your tutorial. You have been doing a huge contribution to the php community!

    I have two questions:

    1) I could not save the registration date for today! I keep choosing today and click save. But when I go check another register in the datagrid and then come back to the register I have just inserted, I see the registration with yesterdays date. What would be wrong?

    2) I did the tutorial as you have taught. I had to make a little modification to get the app to work. (e.g. I had to get the .as files inside the services package one level up. I cut them and pasted directly in the services directory).
    Well, the code the data/services generated seems hard to understand and it seems to me that the first tutorial with doctrine1 is much better understandable!
    My question is, what do you think about the code generation? Should we use, or make our value objects by hand?

    Actually there are 3 questions! :)

    Thanks a lot dude!
    Kind Regards,
    Gustavo

  3. Hey Mihai,great tutorial.
    I don’tknow If you are aware of Aerial,it’s a project that combines Doctrine and AMFPHP.
    Personally I have stopped using the data services wizard of Flash Builder,I use Aerial instead:All I have to do is define the database Schema in yml file,and Aerial generates PHP and corresponding ActionScript Services and models plus a dozen or something operations for each service!NO SQL QUERIES coding whatsoever.Deleting a user from the database is as easy as writing userService.delete(user).
    I am building an AIR CMS for Flah Flex.Imagine articles,users,permissions,file uploads,all stored in database,CRUD operations for all of them.Aerial makes it so easy.
    Check it out:
    http://wiki.aerial-project.org/index.php/Main_Page

  4. @Gustavo

    1) This is very strange. I’ve just double check and it works on my computer :D I guess you can try this to see where the problem is:
    – choose the current date and press the save button;
    – check directly in the database to see what value is there
    – you can also use logMe() function (you’ll find it defined in the bootstrap.php file) to log the date value that is coming from Flex (in the StudetnsService->save() method)

    2) I think that was the place for them from the beginning :D

    3) Indeed the code generated by the data-centric development wizard is not as straightforward as the one written for my first Doctrine tutorial. The reason I used for this tutorial this wizard was to compare how efficient/useful is compared to the hand-writing of all the services/classes. And as I said in the article it really speed up the development because of:
    – code generation features for services/entities
    – taking care of the configuration

    However, I think it is best to understand first what a wizard does and then use it. If for some reason I decide to ditch this code and write it by myself, I don’t think it will take me more than 20 minutes to do so.

    I hope I answered to your questions. Thanks for the nice words :)

    Mihai Corlan

  5. Hello,

    Thanks for the great post!!! And all of the other cool stuff you have on the web. I have learned a lot from your material.

    I have a problem I am hoping you can help with. I have similar code to your example. But for some reason Zend_AMF only exposes public properties for my domain objects. For example.

    class Test{

    public name;

    private description;

    public function getDescription(){
    return $this->description;
    }
    }

    For the above class I always only get the name property on the flex side.

    Do you know what I am doing wrong?

    I am using Zend_AMF 1.10.7. I have traced the problem to the Zend_Amf_Parse_Amf3_Serializer class line 461. It seems the code here is consistent with my results. But in your example you have no public properties, so I am wondering how you got this to work.

    Thanks,
    Navid Mitchell

  6. Hello Mihai,

    About the date, I checked in the database. All date fields are correct, but for all registers the flex app shows one day before the dates stored in the database. Do you have any idea why this is happening?

    Thanks a lot!

  7. Hi,from Sofia – Bulgaria.I’m very interested with your two part tutorial about Doctrine.Because I’m a newbie,my walk through the tutorials is hard and I’m at first part yet.I have a question(sorry maybe putted not exactly at the right place, but ):

    How far is an AIR application using PHP as a middle tier vs LCDS (Live Cycle Data Services),regarding offline data sync – in general data sync!

    All The Best And Good Luck!

    Best wishes to Romania!

  8. Hello Mihai,

    I am facing a trouble while trying to find a way to control the updates of flex app, without upset the user. Is there any way I can get flex metadata to show the version used at a time?

    Thanks a lot!

  9. Very good article Mihai but i still consider using doctrine the same as shooting yourself in the foot.
    (the good foot – the other one got shot when MS access was released)

  10. I have tried Doctrine 1 and 2, and I have to say that version 2.0 kinda sucks. Version 1.2 is so intuitive and easy to get started and use. Version 2 is like WTF?

  11. Mihai, you have a habit of writing articles on the exact items that I am currently working on.

    I have just been examing ALL of the php ORM options..of which there are a lot. Doctrine and Propel are the two biggest and best well known…but I found them very heavy. Propel is horrible because you need to install other apps (Phing, Creole), so I couldnt even be bothered. Doctrine 1x is great, but after benchmarking it, I found it quite slow. And I agree with above poster…Doctrine 2 is a completely different application, and I found it very confusing too.

    I finally stumbled on PHP.ActiveRecord (http://www.phpactiverecord.org) – it doesnt have nearly anything near the community of Doctrine or Propel…but this thing rocks. Hooking it up to AMFPHP was really easy, and its super fast too.

    If anyone wants some code on how I got it working with AMFPHP, let me know…I’m happy to share.

  12. @Gustavo

    I was on my first vacation on this year dude :D
    Now, I’m back and reading tons of emails :(

    @Steve

    Performance could be a problem, however reading though Doctrine 2 docs it seems they implemented caches at different levels, so it should be fast for most usecases.

    Regarding PHP.ActiveRecord vs. Doctrine I don’t have anything to say because I never tried the former. However the name suggests to be a framework that uses ActiveRecord pattern and however these days I find myself in love with Data Mapper pattern. After having experienced both approaches and reading articles on these subject I don’t thing one is better than the other in general. So as long as you feel productive with your choice there is nothing wrong.

    @JonoB

    What can I say man, it is not that easy to write all the mapping but once you’ve got it right it is pretty simple I think.

  13. In the $connectionOptions array you have not set the host and port.

    How can I connect to the database If I don’t use localhost or the default port for mysql? Doctrine 2 documentation is not clear on this topic…

  14. Thanks Mihai. If you add the host and port values in the same array it works.

    Now I try to generate the database structure and the php classes form a xml mapping like: students.dcm.xml. Can you make also a tutorial, or add to this one, how to generate the sql for the database from the mapping files.

    I think a clear example, step by step, would be appreciated by a lot of people.

  15. @Mihai

    Have you found a way to serialize doctrine objects out through AMF ? Because the variables are private, they aren’t visible by the AMF server, thus they don’t get serialized out.

    I’m really impressed with Doctrine, after getting it up and running. But I can’t seem to think of an elegant way to get my data out of php, other than to create a bunch of “transition” models.

  16. @Justin

    Indeed this is how AMF serialization works. So what I’ve done was to use $q->getArrayResult() method on the queries. This returns either one associative array or an array of associative arrays. And the keys are just the private fields (those that map the database fields).

    This associative array is deserialized on the Flex side either to an untyped object or to a typed object (see the DCD section from my article).

    So I think this is a prety elegant way to use the data model you’ve built using Doctrine “philosophy” and to be able to reuse it for Flex without any other transformations.

  17. Thanks for the great post. Is it possible for you to give an example on how to access that data when you return only one entity? For example, I’m trying to find a zipcode entity but don’t want to use a list because the database contains nearly 5000 zipcodes and I don’t want to load them each time.

  18. Hi Mihai,

    Great tutorial! It got me interested in Doctrine 2 and I now been using it for the last few days.

    Yes, there’s a lot of concepts to grasp but the docs are great if your willing to spend some time going through them properly.

    A few tips for other newbies:
    1. Make sure to use the SchemaValidator, it gives you pretty detailed info on what you have done wrong with your entities. Just return the $error array straight back to FLEX and you’ll get all you need to debug your PHP. Place this in you PHP service.
    2. Let Doctrine handle the Zend_Date conversion automatically by creating a custom type, overriding the datetime type.
    3. You can also add your custom DQL functions. I’ve added one for MySQL sha1 encryption. It’s all in the docs and basically you copy and paste an existing function, make your changes, and register the function in your bootstrap.
    4. Make sure your db (MySQl) has all foreign keys set and is only using InnoDB tables.
    5. Take a couple of hours learning how to set up your entities using doc notations, how to use OneToMany, ManyToMany, @JoinTable, @JoinColumn and so on.

    When your on the “other side” you’ll start to feel the power of Doctrine 2. Once you’ve got it working it’s easy to use and it gets stuff done in no-time.

  19. Great tutorial!A lot of ideas!

    I have a problem with ZF, trying to conect to services.
    Warning: require(lib/doctrine-orm/\Doctrine\Common\Cache\ArrayCache.php) [function.require]: failed to open stream: No such file or directory in C:\wamp\www\doctrine2_students\lib\doctrine-orm\Doctrine\Common\ClassLoader.php on line 148

    Fatal error: require() [function.require]: Failed opening required ‘lib/doctrine-orm/\Doctrine\Common\Cache\ArrayCache.php’ (include_path=’C:/wamp/www/doctrine2_students/services\.;.;C:\php\includes;C:\wamp\www\ZendFramework\library;C:/wamp/www/ZendFramework/library’) in C:\wamp\www\doctrine2_students\lib\doctrine-orm\Doctrine\Common\ClassLoader.php on line 148

    ZF – php5.3 namespace conflict or my fault

  20. I fixed it, adding a path to Doctrine folders in include_path(php.ini).

    And finally all is working. yeahhh:)

Leave a Reply

Your email address will not be published. Required fields are marked *