Working with Doctrine 1.x, Zend Framework, and Flex

Later Update: If you are looking for an article rather on Doctrine 2 than Doctrine 1, then check this one.

This year I finally had the time to play with Doctrine (version 1.x) and Flex. Actually, it was more than playing; I’m using it for a real project that hopefully will enter production pretty soon. To summarize the experience in just a few words: it’s mind-blowing.

OK, I admit I may be exaggerating a little bit. Still, it is something that can change the way you build projects. Doctrine is an ORM (Object Relational Mapper) framework for PHP and it can really speed up the server side development when you have a lot of tables in your database.
In this article, I explore how to work with Doctrine on the server side, Flex on the client side, and remoting to communicate between Flex and PHP (using the Zend Framework for remoting on the PHP side). I also want to share with you some tools and workflows that can save you some time. While most things are fairly straightforward there are a number of tips and tricks that you may find useful if you decide to go down this road. (I will show you how to use plain vanilla value objects and how to handle dates just to give you two examples. Why reinvent the wheel?)

The application I’m going to build in this article is simple but the workflow is the same one I used with a much more complex application. Having said that let’s start by understanding the big picture.

  1. The Big Picture
  2. Database Model
  3. Installing Doctrine
  4. Configure Doctrine and generate the model files
  5. IDEs: Eclipse PDT and Flash Builder 4
  6. Creating the PHP services and PHP value objects
    1. Getting the countries and courses
    2. Creating the StudentsService service
    3. Handling Create, Update, and Delete with Doctrine
  7. Installing the Zend Framework
  8. Setting up the gateway file
  9. Creating the Flex front-end
    1. Creating the ActionScript value objects
    2. Creating the UI
  10. Getting the source code
  11. Conclusions

The Big Picture

Let’s see how these different pieces come together and play nice with each other like one big happy family. Have a look at Figure 1. Yeah, this is what you’ll get if you follow this article.

Figure 1. Architectural view

Breaking down Figure 1:

  • On the right is a MySQL database to persist the data. You’ll see the data model in the next chapter.
  • On the PHP server I use Doctrine to manage the tables in the database. I use Doctrine features to generate the model classes out of the database structure.
  • On the PHP server I have a bunch of PHP services, which will be consumed from Flex using remoting. Typically for each table from the database there is a PHP class that provides, at a minimum, basic CRUD operations (Create/Read/Update/Delete).  These PHP services use the Doctrine model classes to read and persist the data. They are completely unaware of the database structure.
  • Finally, we have plain vanilla PHP value objects. These may follow the structure of the database. The Doctrine models return instances of these value objects. The PHP services, when called from the Flex side, receive instances of these classes as arguments.
  • The last piece on the PHP server is the Zend Framework. I use this framework to enable remoting for PHP (you could use AMFPHP, WebORB for PHP, or Sabre AMF as well). Basically it provides a gateway that represents the entry point to the PHP services for the Flex application.
  • On the Flex side there are RemoteObjects that map to the PHP services.
  • Then I have ActionScript value objects. For each PHP value object there is a corresponding ActionScript object. When using remoting the data is automatically changed to the right type based on the mappings you define.
  • I can bind these ActionScript value objects to the different user interface elements in my application.

[Top]

Database Model

It is time to create the database. I’m going to use MySQL for the database. Every time I start a project that someday will be in production and uses a database I use data modeling software. Eight years ago the only viable solutions were Erwin or Power Designer (at least to my knowledge). I found that MySQL Workbench does a decent job and I use it fairly regularly nowadays (you can download MySQL Workbench from here).

Why use  database modeling software? It helps you design the database and all the dependencies, share the design with others, and quickly remember the database structure when you go back to the project after couple of months off. And it helps you a lot when you start to change the project as you are in the middle of it. Remember that I’m talking about projects that go into production, and I think there are very few of those that don’t undergo modifications, including changes to the database structure.

You can use MySQL Workbench to create a data model from scratch or reverse engineer an existing database. You can push all the changes you make to the model (new tables/fields/indexes) directly to the database. And, of course, you can print these models.

To start a new model, choose File > New and then click Add Diagram if you want to start from scratch. Then you can use the toolbox on the left to add tables and draw the relationships between them. When you double-click a table you can edit the name or add/edit fields and indexes. On the right side there is a Catalog viewer. By default the model will try to create the tables into a database named mydb. If you want to change the name of the database you can double-click on the current name from the Catalog view.

Once you’ve finished the design of the data model you can add a connection to your database server. The server can be on the localhost or a remote host, in the latter case you need to be careful if you have firewalls. Next use the Database > Synchronize Model wizard to push the new database model to the database server. You can use the same wizard whether you make changes to the data model and need them reflected to the database or you change the database and you want to push them to the data model.

You can download the model I created (see Figure 2) from here.

Figure 2. Creating the database

My application has four tables: students, countries, marks, and courses. A student can be registered to many courses and the mark (or grade) for each student for each course is stored in the marks table (a many-to-many table). Each student originates from a country, and there is a one-to-many relationship between the countries table and students table.

It is always a good practice to ensure that the data are consistent by using all the means a relational database offers (foreign keys, unique keys, and so on). I have unique keys for the name field in the countries and courses tables, on the first_name and last_name fields in the students table.

Now that I have the database in place and I can easy change it using the model, it is time to create the PHP classes that will handle these tables. Which means it is time for Doctrine to take the stage.

[Top]

Installing Doctrine

Doctrine is one of the few PHP ORMs (Object Relational Mapper) available. The current version (at the time of writing this article) is 1.2.2. You can download it from here. You can also get the library using SVN (here is the link for the repository; you need to look under the tags node).

Why should you consider using Doctrine (or any other ORM for that matter)? Usually, any change you make at the database level will require a change to the PHP code that handles that database. Doctrine can isolate you from these changes. How? It has a feature that lets you generate PHP classes that handle each table from the database, and the input for this feature can be the database itself.

Suppose you add a new table and two new fields in existing tables. You do this in MySQL Workbench, then you use the MySQL Workbench synchronize feature to push all these changes to the database. Next you use Doctrine to generate again the PHP classes that handle this database. In just a couple of minutes you have all the code you need to perform CRUD operations on your tables.

Doctrine also takes care of escaping the values, thus protecting you from SQL injection attacks. Another cool feature of Doctrine is DQL (Doctrine Query Language). This feature makes it easy to write complete queries.

You may be thinking that using the layers on top of PHP (Doctrine and the Zend Framework) can degrade the performance of your application, including how fast it responds to a request, how many clients can connect, and so on. While this is true theoretically speaking, in practice I’d say that the vast majority of projects don’t have performance issues. They have problems keeping up with design changes and new features.

Once you have the library files, create a new folder called doctrine_students under your web root folder. Inside of this folder create these folders: lib, lib/vendor, and lib/vendor/doctrine/. Next copy the Doctrine files to the doctrine_students/ lib/vendor/doctrine/  folder. If you downloaded the archive file, then you have to copy the folders and files from Doctrine-1.2.2/Doctrine-1.2.2/lib/ (there should be a Doctrine.php file and a Doctrine folder).

You can read more about installing Doctrine here.

[Top]

Configure Doctrine and generate the model files

The configuration of Doctrine is pretty simple. Basically you have to create a bootstrap file and include the Doctrine.php file, and then set the credentials for the database.

I created a file called bootstrap.php inside of the doctrine_students folder. This bootstrap file will be required in any PHP file that uses Doctrine. Inside of this file I added the following code:

<?php
//require the main file from Doctrine
require_once(dirname(__FILE__) . '/lib/vendor/doctrine/Doctrine.php');
//register the class autoloader function
spl_autoload_register(array('Doctrine', 'autoload'));
//create the singleton Doctrine_Manager instance
$manager = Doctrine_Manager::getInstance();
// enable automatic queries resource freeing
$manager->setAttribute(Doctrine_Core::ATTR_AUTO_FREE_QUERY_OBJECTS, true);

Next, create a new PHP file called generate_model.php inside of the doctrine_students folder and add this code as a simple test:

<?php
require_once('bootstrap.php');
echo Doctrine::getPath();

If you navigate to the doctrine_students folder in a terminal/console window and run generate_model.php you should see the absolute path to the doctrine folder:

mcorlan-mac:doctrine_students mcorlan$ php generate_model.php
/Applications/MAMP/htdocs/doctrine_students/lib/vendor/doctrinemcorlan-mac:doctrine_students mcorlan$

Create a new folder called models inside of doctrine_students. This is where Doctrine will generate the model files. Then go back to bootstrap.php file and add a new line at the end to create a database connection:

//create a connection
$conn = Doctrine_Manager::connection('mysql://mihai:mihai@127.0.0.1/students', 'doctrine');

Basically you set the connection type to mysql, then you set the user and password (in my case “mihai” for both) then you set the database IP (don’t use localhost even if you have MySQL installed on your local machine because it will not work), and finally you specify the name of the database to be used.

Back in the generate_model.php file, comment out the echo line and add these lines:

//generate the models from the DB schema in the models folder
Doctrine_Core::generateModelsFromDb('models', array('doctrine'), array('generateTableClasses' => true));

In your console, run php generate_model.php and you should find in the doctrine_students/models/ folder a bunch of files and another folder named generated. Basically you have a file with the same name for each table from your database. You will see in the next chapter how to use these model files to persist and retrieve data.

Having generated the model files, I want to load them by adding these lines to the bootstrap file (at the end of the file):

//include the models
Doctrine_Core::loadModels(dirname(__FILE__) . '/models/generated');
Doctrine_Core::loadModels(dirname(__FILE__) . '/models');

Doctrine offers another way for creating these model files: YAML schema files. You can write a schema file from scratch and then use this file for generating the models. Alternative, you can use the models you generated using the database structure to create a schema file and then edit this file before using it to regenerate the models.

If you want to create a schema file, just edit the generate_model.php file as follows (I commented the line that generates the model from the database and I added a new line that creates a YAML file from the models):

<?php
require_once('bootstrap.php');
//echo Doctrine::getPath();

//generate the models from the DB schema in the models folder
//Doctrine_Core::generateModelsFromDb('models', array('doctrine'), array('generateTableClasses' => true));

//generate the Doctrine Schema from models
Doctrine_Core::generateYamlFromModels('schema.yml', 'models');

Return to the console and execute the following command again: php generated_model.php. After this you’ll find a schema.yml file inside of doctrine_students folder.

With all the models in place, it is time to see how you can use these files to manage the database.

[Top]

IDEs: Eclipse PDT and Flash Builder 4

Up until now you could have used just about any text editor to complete the steps I’ve outlined. As the code you write gets more complex (and soon you will start to write Flex code as well), it is time to introduce you to my favorite setup for working on Flex and PHP projects: Eclipse PDT + Flash Builder 4 plug-in + XDebug (for enabling PHP debugging).

The easiest way to install Eclipse PDT and Flash Builder together is to first grab the Eclipse PDT and then install the plug-in version of Flash Builder on top of  Eclipse PDT. You can read more about how to install these products together here.

Now that you have support for both PHP and Flex projects, create a PHP project using the PHP new project wizard from Eclipse PDT (select the PHP perspective and choose File > New > PHP Project). I named the project students.

Then  add the doctrine_students folder to the project. Right-click the project name in Eclipse and choose New > Folder. When the wizard opens, click the Advanced button and then select the Link To Folder In The File System option. Next, click Browse, select the doctrine_students folder, and click Finish (see Figure 3).

Figure 3. Adding the doctrine_students folder to the project.

Then create another two folders inside of doctrine_students folder: services and vo (see Figure 4).

Figure 4. The students project in Eclipse

[Top]

Creating the PHP Services and PHP value objects

It is time to create the PHP services. I’ll use the model files that Doctrine generated earlier.

First I will create a PHP class with two methods: getCourses() and getCountries(). In many applications you have some tables that stores data that almost never changes, for example lists of countries or cities. For such tables, you don’t need to implement CRUD operations you just need the read method. This is the case with the courses and countries tables.

[Top]

Getting the countries and courses

Create a file named CatalogService.php inside the doctrine_students/services/ folder and add this code:

<?php
require_once(dirname(__FILE__) . '/../bootstrap.php');

class CatalogService {

    public function getCourses() {
        $q = Doctrine_Query::create()
                ->select('*')
                ->from('Courses')
                ->orderBy('name');
        $ret = $q->execute(null, Doctrine_Core::HYDRATE_ARRAY);
        return $ret;
    }

    public function getCountries() {
        $q = Doctrine_Query::create()
                ->select('*')
                ->from('Countries')
                ->orderBy('name');
        $ret = $q->execute(null, Doctrine_Core::HYDRATE_ARRAY);
        return $ret;
    }

}

If this is your first time using Doctrine you’ll probably find this code both strange and familiar at the same time.

As I said earlier, Doctrine offers a query language (DQL). You can learn more about DQL here. The code above uses DQL to retrieve all the records from the courses and countries tables. Some things to remember:

  • When you set the from clause (for example, ->from(‘Countries’)) you don’t specify the actual table name but the name of the model PHP class generated by Doctrine to manage that table. In this case it happens to have the same name and the first letter is capitalized.
  • DQL works with objects and generates objects or graphs of objects.
  • You can control the result format for a query by setting the second parameter of the execute() method on Doctrine_Query. I want to have the result sets as arrays or associative arrays. You could have your own hydration method or you could use one of the built-in hydrators.
  • If you want to see the actual SQL that is executed you can call this method: $q->getSqlQuery().

Add some records to the courses and countries tables or use this script to create the database.

Now, you’re ready to test this code. Usually I do this before trying to connect to the PHP code from a Flex application. Just add some simple code at the top of this file in order to initialize the CatalogService object and call the two methods:

$service = new CatalogService();
print_r($service->getCourses());
print_r($service->getCountries());

If you run CatalogService.php in the browser you should see  the courses and countries used in the two tables (see Figure 5).

Figure 5. Testing the CatalogService class

I have the first service in place, but I’m not entirely satisfied with the way my service works. I want to use some simple value objects to wrap each record instead of having an array of associative arrays like I have now.

I create two value objects called Country and Course in the doctrine_students/vo/ folder:

<?php
//Country.php
class Country {

    public $id;
    public $name;
}

<?php
//Course.php
class Course {

    public $id;
    public $name;
}

Now I just have to create a function that transforms the arrays I get from the Doctrine_Query object into arrays of value objects. You could achieve this by creating a custom hydration method or processing the result you get from the array hydration method.

Here’s how I did it. I created a function called  prepareForAMF that accepts two arguments. The first is the array you get using the Doctrine_Query object and the second is an array of objects to be used to wrap the associative array. From this array the function retrieves the types it has to use to transform the associative array into objects.

Here is the code for this function:

function prepareForAMF($data, $arrTypes) {
    if (count($data) == 0)
        return $data;
    $ret = array();
    $substract = false;
    if (!array_key_exists('0', $data)) {
        $data = array($data);
        $substract = true;
    }
    for ($i=0; $i<count($data); $i++) {
        $o = new $arrTypes[0]();
        foreach ($data[$i] as $property => $value) {
            $pproperty = strtolower($property);
            if (!property_exists($o, $pproperty)) {
                continue;
            }
            if (array_key_exists($property, $arrTypes)) {
                if ($value == NULL) {
                    $o->$property = array();
                    continue;
                }
                $newArr = $arrTypes;
                $newArr[0] = $arrTypes[$property];
                $o->$pproperty = prepareForAMF($value, $newArr);
            } else {
                $o->$pproperty = $value;
            }
        }
        $ret[] = $o;
    }
    if ($substract)
        $ret = $ret[0];
    return $ret;
}

I chose to declare this function in the bootstrap.php file because this file is already included in all the PHP services.

Having this function defined I rewrote the methods from CatalogService like this:

public function getCourses() {
    $q = Doctrine_Query::create()
            ->select('*')
            ->from('Courses')
            ->orderBy('name');
    $ret = $q->execute(null, Doctrine_Core::HYDRATE_ARRAY);
    $ret = prepareForAMF($ret, array(0 => 'Course'));
    return $ret;
}

public function getCountries() {
    $q = Doctrine_Query::create()
            ->select('*')
            ->from('Countries')
            ->orderBy('name');
    $ret = $q->execute(null, Doctrine_Core::HYDRATE_ARRAY);
    $ret = prepareForAMF($ret, array(0 => 'Country'));
    return $ret;
}

Next you need to include the two value objects in the CatalogService.php class by adding these lines at the top of it:

require_once(dirname(__FILE__) . '/../vo/Country.php');
require_once(dirname(__FILE__) . '/../vo/Course.php');

If you run CatalogService.php in the browser you’ll see the courses and countries are now listed as objects (see Figure 6).

Figure 6. Using value objects in the CatalogService class.

Note: In Doctrine 2.0 (the upcoming version of Doctrine) support for plain vanilla value objects and for AMF remoting is planned and this trick/hack will not be needed. This is actually the reason why I didn’t bother to write my own hydration method. I’m planning to do a test run with the nightly builds soon.

[Top]

Creating the StudentsService service

It is time to move on and create another service for the students and marks tables. Create a file inside of doctrine_students/services/ folder named StudentsService.php.

Here is a blue print of the StudentsService class:

<?php
require_once(dirname(__FILE__) . '/../bootstrap.php');
require_once(dirname(__FILE__) . '/../vo/Country.php');
require_once(dirname(__FILE__) . '/../vo/Course.php');

$service = new StudentsService();
print_r($service->getAll());

class StudentsService {

    public function getAll() {

    }

    public function save($record) {

    }

    public function delete($record) {

    }
}

Let’s start by filling in the getAll() method. It will use the DQL feature to retrieve not only all the students, but also the country set for each student and all the marks he has (at this point you need to have some data in the students and marks tables):

public function getAll() {
    $q = Doctrine_Query::create()
                ->select('s.*, m.*, c.*')
                ->from('Students s')
                ->leftJoin('s.Marks m')
                ->leftJoin('s.Countries c')
                ->orderBy('s.last_name, s.first_name');
        $ret = $q->execute(null, Doctrine_Core::HYDRATE_ARRAY);
        return $ret;
}

Save the file and run it in the browser. If you choose view source you should see an array of arrays (see Figure 7).

Figure 7. StudentsService getAll() method result

Notice that for each record from the students table I retrieve all the records from the marks table (student_id = id) and the country record  from the countries table (country_id = id). This works because Doctrine takes into account all the relations between the tables when it creates the models.

Because I defined a one-to-many relation between the countries and students tables (a country can be assigned to any number of students) and a many-to-many table (marks) between the students and courses tables (each student can have a mark for each course), Doctrine generated the model (schema.yml) and model classes to support this database schema. Indeed if you open the doctrine_students/schema.yml file and look at the Students entry you’ll see that it has defined two relations: Countries and Marks.

These two relations have the same name as the two fields you saw when you ran the StudentsService->getAll() method.

Next you’ll need to create two value objects (Student and Mark) to use with StudentsService. Add these two classes to the doctrine_students/vo/ folder:

<?php
//Stundent.php file
class Student {

    public $id;
    public $first_name;
    public $last_name;
public $registration;
    public $marks;
    public $countries;
}

<?php
//Mark.php file
class Mark {

    public $student_id;
    public $course_id;
    public $mark;
}

Don’t forget to include these two new classes in the StudentsService.php file.

Now I’ll change the getAll() method to transform the array I get from the Doctrine query into an array of value objects:

//StundentsService class definition
...

private $types = array (0 => 'Student', 'Countries' => 'Country', 'Marks' => 'Mark');

public function getAll() {
    $q = Doctrine_Query::create()
            ->select('s.*, m.*, c.*')
            ->from('Students s')
            ->leftJoin('s.Marks m')
            ->leftJoin('s.Countries c')
            ->orderBy('s.last_name, s.first_name');
        $ret = $q->execute(null, Doctrine_Core::HYDRATE_ARRAY);
        $ret = prepareForAMF($ret, $this->types);
        return $ret;
}
...

Note: The prepareForAMF function works recursively as long as you provide the right mapping between the keys of the arrays and the value objects it needs to create. This time I defined these mappings as a class variable ($types).

If you run the code again you should now see arrays of value objects: Student, Mark, and Country.

There’s something else I want to fix. The registration field from the students table has the Date type. But in the array returned by the getAll() method it is a String. I want to make sure that this field is actually an instance of the DateTime class. This will pay dividends later on when I retrieve these records in the Flex client, because it will receive value objects that have the Date type for the registration field instead of String.

The best place to do this transformation is probably inside the prepareForAMF() function. I added an optional third argument that is an array of the fields that have Date or DateTime type. So here is the modified function:

function prepareForAMF($data, $arrTypes, $arrDates=NULL) {
    if (count($data) == 0)
        return $data;
    $ret = array();
    $substract = false;
    if (!array_key_exists('0', $data)) {
        $data = array($data);
        $substract = true;
    }
    for ($i=0; $i<count($data); $i++) {
        $o = new $arrTypes[0]();
        foreach ($data[$i] as $property => $value) {
            $pproperty = strtolower($property);
            if (!property_exists($o, $pproperty)) {
                continue;
            }
            if (array_key_exists($property, $arrTypes)) {
                if ($value == NULL) {
                    $o->$pproperty = array();
                    continue;
                }
                $newArr = $arrTypes;
                $newArr[0] = $arrTypes[$property];
                $o->$pproperty = prepareForAMF($value, $newArr, $arrDates);
            } else {
                if ($arrDates && array_key_exists($pproperty, $arrDates)) {
                    $o->$pproperty = new DateTime($value);
                } else {
                    $o->$pproperty = $value;
                }
            }
        }
        $ret[] = $o;
    }
    if ($substract)
        $ret = $ret[0];
    return $ret;
}

Then I add a third argument when calling the prepareForAMF() function inside of getAll():

//StundentsService class definition
...
private $dates = array('registration' => 1);

public function getAll() {
    $q = Doctrine_Query::create()
            ->select('s.*, m.*, c.*')
            ->from('Students s')
            ->leftJoin('s.Marks m')
            ->leftJoin('s.Countries c')
            ->orderBy('s.last_name, s.first_name');
        $ret = $q->execute(null, Doctrine_Core::HYDRATE_ARRAY);
        $ret = prepareForAMF($ret, $this->types, $this->dates);
        return $ret;
}
...

[Top]

Handling Create, Update, and Delete with Doctrine

It is time to write the StudentsService methods for creating, updating, and deleting records. For this, you need to fill in the blanks for the other two methods  defined earlier: save() and delete(). Please notice:

  • I want to use the save() method for both creating new records and updating existing ones.
  • The save() method receives one argument: an instance of a Student value object. If you want to add marks for the student you have to have Mark objects stored in the mark property.
  • The save() method will add or change the related records from the marks table for the current student .
  • The delete() method deletes all the related records from the marks table first and then the student (it is transaction safe).

Here is the code for save():

public function save($record) {
        $record = makeArrayFromObject($record, $this->dates);
        //update an existent student
        if ($record['id'] > 0) {
            //retrieve the current student model
            $o = Doctrine_Query::create()
                ->select('*')
                ->from('Students')
                ->where('id = ?', $record['id'])
                ->fetchOne();
            if (!$o)
                return false; //exit if there is no student
            //loading relations
            $o->Marks;
            //clean up the array
            $record['Marks'] = $record['marks'];
            unset($record['marks']);
            if (array_key_exists('countries', $record)) {
                //set the country id
                $o->country_id = $record['countries'][0]['id'];
                unset($record['countries']);
            }
            //modify the existent student data 
            //using the data we have in $record 
            $o->synchronizeWithArray($record);
        //insert a new student
        } else {
            $o = new Students();
            //load the main properties from the $record
            $o->fromArray(cleanRefences($record, $this->types));
            //add marks records for the student if any
            if ($record['marks']) {
                foreach ($record['marks'] as $val) {
                    $t = new Marks();
                    $t->fromArray($val);
                    $o->Marks[] = $t;
                }
            }
            //setting student's country id
            if (array_key_exists('countries', $record) && count($record['countries']) > 0) {
                $o->country_id = $record['countries'][0]['id'];
            }
        }

        //persist the data model to the database
        $o->save();
        //loading Countries relations
            $o->Countries;
        $ret = prepareForAMF($o->toArray(), $this->types, $this->dates);
            return $ret;
    }

And here is the code for two functions I use in the save() method (I declared these functions in the bootstrap.php file):

function makeArrayFromObject($data, $arrDates=NULL)
{
    $data = (array)$data;
    foreach ($data as $k => $v) {
        if (is_array($v)) {
            $data[$k] = makeArrayFromObject($v, $arrDates);
        } else {
            if ($arrDates && array_key_exists($k, $arrDates)) {
                if ($v instanceof DateTime) {
                    $data[$k] = $v->format('Y-m-d');
                } else {
                    $data[$k] = $v->toString('Y-M-d');
                }
            } else if (is_object($v)) {
                $data[$k] = (array)$v;
            }
        }
    }
    return $data;
}

function cleanRefences($data, $references)
{
    foreach ($references as $key => $val) {
        if (array_key_exists($key, $data)) {
            $data[$key] = NULL;
        }
    }
    return $data;
}

There are comments in the code so it should be easy to read and understand. I’ll explain just the parts I think are important:

  • First of all, as I said earlier, Doctrine 1.x doesn’t have support for plain vanilla value objects. This is why the first thing I do is to transform the Student value object into an associative array using the makeArrayFromObject() function.
  • When I update an existing record I use DQL to create an instance of Students. Once I have the Students object for the given ID, I want to load all the related marks records (by calling $o->Marks). Next I use the synchronizeWithArray() method to apply the changes from the $record array to the existing data (basically synchronizeWithArray() receives an associative array similar in structure to the one you get when using the Array hydration method on the same object). Finally I set the country id, and once I call the save() method on the Students object ($o->save()) the data are persisted.
  • When I add a new record, I instantiate a Students object and then I use the fromArray() method to load the data from the $record array. However I found a glitch: for some reason it doesn’t work with the relations (in my case it didn’t save the related records to the marks table). So the workaround I found is to use the fromArray() method to load only the properties stored in the same table (students). This is why I clean the array from all the relations (Marks and Countries) using a call to the cleanReferences() function. I add the Marks objects manually using a for() loop.
  • When adding marks records, it is interesting to note that Doctrine takes care of all the plumbing work for me. Suppose you add a new student and at the same time two marks: (course_id:1, mark:10) and (course_id:3, mark:9). The marks table has a third field student_id. When you call the save() method on the Students object, Doctrine will insert a new record in the students table, it will retrieve the id and then fill in the missing student_id for the two Mark records and then insert these two records.

The code for the delete() operation is much simpler. I use DQL to first delete all the related records from the marks table and then I delete the student record using an instance of Students. I make use of Doctrine support for transactions to keep the data consistent.

//StudentsService class definition
...
public function delete($record) {
        $record = makeArrayFromObject($record, $this->dates);
        //delete all related records from marks table
        $q = Doctrine_Query::create()
                ->delete('Marks')
                ->where('student_id = ?', $record['id']);
            //start transaction        
            $q->getConnection()->beginTransaction();
            $q->execute();

        $o = new Students();
        $o->assignIdentifier($record['id']);
        $o->delete();

        $q->getConnection()->commit(); //commit transaction

        $ret = prepareForAMF($o->toArray(), $this->types, $this->dates);
            return $ret;
    }
...

You now have all the code in place. If you want to verify it you can add testing code (the lines below) in the StudentsService.php file. Then run the file in the browser and then check the database:

$service = new StudentsService();
$c = new Country();
$c->id = 1;

$m1 = new Mark();
$m1->course_id = 3;
$m1->mark = 7;
$m2 = new Mark();
$m2->course_id = 4;
$m2->mark = 8;

$s = new Student();
$s->first_name = 'Michael';
$s->last_name = 'Doherty';
$s->registration = new DateTime('2010-03-14');
$s->countries = $c;
$s->marks = array($m1, $m2);

print_r($service->save($s));

Before moving on, don’t forget to comment (or delete) all the code you added in the service classes for testing.

[Top]

Installing the Zend Framework

The next task I have to complete on the PHP side is to enable support for remoting in PHP. I chose to do so by using the Zend Framework. You can use AMFPHP, WebORB for PHP, or SabreAMF as well.

Download and unzip the framework somewhere on your disk. Then use the php.ini file to include the framework:

include_path = ".:/Applications/MAMP/bin/php5/lib/php:/Applications/MAMP/zend_framework/library "

Save the file and restart the server. If you don’t have access to the php.ini you probably have to copy the whole framework under your web root, though this is not recommended for production.

[Top]

Setting up the gateway file

The last step I have to complete on the PHP side before moving to Flex is to create the gateway file. This file will represent the end point for all the remoting calls from the Flex application. It will configure and start the Zend AMF server.

Create a new file called gateway.php inside of doctrine_students folder and then add this code:

<?php
require_once('Zend/Amf/Server.php');

//create an instance of Zend AMF server
$server = new Zend_Amf_Server();

//adding the services folder in order to expose
//CatalogService and StudentsService
$server->addDirectory(dirname(__FILE__) . '/services/');

//Mapping the ActionScript VO to the PHP VO
//you don't have to add the package name
$server->setClassMap("Country", "Country");
$server->setClassMap("Course", "Course");
$server->setClassMap("Mark", "Mark");
$server->setClassMap("Student", "Student");

$server->setProduction(false);

echo($server -> handle());

Save the file and run it in the browser. You should either see the message “Zend Amf Endpoint” or a save dialog.

[Top]

Creating the Flex front-end

With all the PHP code in place, it is time to create the Flex front-end. From a workflow perspective you have two options:

  • You can create a separate Flex project and have two projects in place: one for Flex and one for PHP.
  • You can add Flex nature to the PHP project and use the same project for both Flex and PHP.

I usually prefer the second approach because I work on both Flex and PHP code and it is simple to debug them together.

To add Flex nature to the students project, right-click on the project name in Eclipse, and select Add/Change Project Type > Add Flex Project Type. When the wizard opens, select PHP as the application server type and click Next. Fill in the path to the web server root and URL to reach the root, then click Validate Configuration and then Finish. You might see this error in the Problems view: “Cannot create HTML wrapper. Right-click here to recreate folder html-template.” Just right-click on it and choose Recreate Folder HTML-template.

This wizard will create an MXML file named students.mxml as well. Open this file if you haven’t already and add the following code inside the <fx:Declarations> tag:

<s:RemoteObject id="catalogService" source="CatalogService" destination="zend"
                endpoint="../doctrine_students/gateway.php"
                result="onResult(event)" fault="onFault(event)">
    <s:method name="getCourses" />
    <s:method name="getCountries" />
</s:RemoteObject>

<s:RemoteObject id="studentsService" source="StudentsService" destination="zend"
                endpoint="../doctrine_students/gateway.php"
                result="onResult(event)" fault="onFault(event)">
    <s:method name="getAll" />
    <s:method name="save" />
    <s:method name="delete" />
</s:RemoteObject>

The code defines two remoteObjects, one for each service on the PHP side: CatalogService and StudentsService. I added the methods for clarity, although this is not mandatory. Note that the endpoint attribute points to the gateway.php file  created earlier.

The next step is to define the onResult and onFault event listeners. Add this code above or below the <fx:Declaration> tag:

<fx:Script>
    <![CDATA[
        import mx.rpc.events.FaultEvent;
        import mx.rpc.events.ResultEvent;

        private function onResult(e:ResultEvent):void {

        }

        private function onFault(e:FaultEvent):void {

        }
    ]]>

To test the code you’ll need to add a button and call one of the service’s methods on click. Next, add two breakpoints on the first line where each event listener is defined and then run the project in debug mode.

"catalogService.getCourses()"/>

After the page loads, click the button and in Eclipse you should see the Flex debugger stopped (see Figure 8).

Figure 8. Testing the services in the Flex client using the Flex debugger.

[Top]

Creating the ActionScript value objects

When using remoting it is usually a good idea to create value objects on the Flex side that correspond to the value objects you have on the server side. In my case I need to create four value objects: Student, Mark, Course, and Country. I place all these classes in the org.corlan.vo package.

The code is here:

package org.corlan.vo {

    [Bindable]
    [RemoteClass(alias="Country")]
    public class Country {
        public var id:int;
        public var name:String;
    }
}

package org.corlan.vo {

    [Bindable]
    [RemoteClass(alias="Course")]
    public class Course {
        public var id:int;
        public var name:String;
    }
}

package org.corlan.vo {

    [Bindable]
    [RemoteClass(alias="Mark")]
    public class Mark {
        public var student_id:int;
        public var course_id:int;
        public var mark:Number;
    }
}

package org.corlan.vo {

    [Bindable]
    [RemoteClass(alias="Student")]
    public class Student {

        public var id:int;
        public var first_name:String;
        public var last_name:String;
        public var registration:Date;
        public var countries:Country;
        private var _marks:Array;

        public function get marks():Array {
            return _marks;
        }

        public function set marks(value:Array):void {
            for (var i:int=0; i
                value[i] = value[i] as Mark;
            }
            _marks = value;
        }
    }
}

There is nothing special to these value object classes. I’m using the Bindable metadata to make each property bindable and I use the RemoteClass metadata to tell AMF which is the corresponding class on the server side. On the PHP side I have the mapping defined inside of the gateway.php file using the setClassMap() method from AMF server – $server->setClassMap(“Country”, “Country”);.

For the Student value object I use the getter/setter to cast the array of objects received from the server to an array of Mark value objects.

Before testing the code you need to make sure that these four value object classes are included in the compiled SWF. You can simply add these lines in the students.mxml file:

import org.corlan.vo.*;
static public var clz:Array = [Country, Course, Mark, Student];

If you run the project and call the studentsService.getAll() method when clicking on the button, you should see the value objects in the Flex debugger (see Figure 9).

Figure 9. Using value objects on the Flex side.

[Top]

Creating the UI

Now you can focus on the UI of this application. It will be a simple UI with a data grid for displaying all the students (see Figure 10). When a student is selected from the data grid, the form on the right is populated and you can use it to change details for the current student. If you want to delete a student, just select a row and click Delete. If you want to add a new record, click New and then fill in the form.

I have a function that is used for creating the text in the courses column, and a bunch of arrays I’m using to store the data models (courses, countries, and students).

Figure 10. The Flex client.

Here is the code for the application:

"1.0" encoding="utf-8"?>
"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" creationComplete="init()" xmlns:vo="org.corlan.vo.*">


        


            [Bindable]
            private var arrStudents:ArrayCollection;
            [Bindable]
            private var arrCourses:ArrayCollection;
            [Bindable]
            private var arrCountries:ArrayCollection;

            private var coursesHash:Object;
            private var marks:Array;

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

            private function getStudents():void {
                studentsService.getAll();
            }

            private function onStudents(e:ResultEvent):void {
                arrStudents = new ArrayCollection(e.result as Array);
            }

            private function onCourses(e:ResultEvent):void {
                arrCourses = new ArrayCollection(e.result as Array);
                //save the courses into a hashmap
                var l:int = arrCourses.length;
                coursesHash = new Object();
                marks = new Array();
                var c:Course;
                for (var i:int = 0; i
                    c = (arrCourses[i] as Course);
                    coursesHash

[/c]

 = c.name;

                    marks.push({course_id : c.id, mark : 0, name : c.name});
                }
                dgMarks.dataProvider = marks;
            }

            private function onCountries(e:ResultEvent):void {
                arrCountries = new ArrayCollection(e.result as Array);
            }

            private function onFault(e:FaultEvent):void {
                Alert.show(e.fault.faultDetail, "Error!");
            }

            private function onSelection(e:ListEvent):void {
                student = e.itemRenderer.data as Student;
                if (student.countries) {
                    var l:int = arrCountries.length;
                    for (var i:int = 0; i<l; i++) {
                        if (arrCountries[i].id == student.countries.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].course_id == o["course_id"]) {
                                o["mark"] = student.marks[jj].mark;
                                break;
                            }
                        }
                    }
                    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];
                    arr.push(coursesHash[mark.course_id]);
                }
                return arr.join(", ");
            }

            private function saveStudent():void {
                //set the marks
                var l:int = marks.length;
                var m:Mark;
                student.marks = new Array();
                for (var i:int = 0; i<l; i++) {
                    if (marks[i]["mark"] > 0) {
                        m = new Mark();
                        m.course_id = marks[i]["course_id"];
                        m.mark = marks[i]["mark"];
                        if (student.id > 0)
                            m.student_id = student.id;
                        student.marks.push(m);
                    }
                }
                //set the country
                student.countries = country.selectedItem as Country;
                //set the names and registration date
                student.first_name = first.text;
                student.last_name = last.text;
                student.registration = registration.selectedDate;
                studentsService.save(student);
            }

            private function deleteStudent():void {
                if (!student || !student.id)
                    return;
                var o:AbstractOperation = studentsService.getOperation("delete");
                o.send(student);
            }
        ]]>
    </fx:Script>
    <fx:Declarations>
        <s:RemoteObject id="catalogService" source="CatalogService" destination="zend"
                        endpoint="../doctrine_students/gateway.php" fault="onFault(event)">
            <s:method name="getCourses" result="onCourses(event)"/>
            <s:method name="getCountries" result="onCountries(event)"/>
        </s:RemoteObject>

        <s:RemoteObject id="studentsService" source="StudentsService" destination="zend"
                        endpoint="../doctrine_students/gateway.php" fault="onFault(event)">
            <s:method name="getAll" result="onStudents(event)"/>
            <s:method name="save" result="getStudents()"/>
            <s:method name="delete" result="getStudents()" />
        </s:RemoteObject>
        <vo:Student id="student"/>
    </fx:Declarations>

    <mx:DataGrid id="dgStudents" dataProvider="{arrStudents}" x="13" y="14" itemClick="onSelection(event)">
        <mx:columns>
            <mx:DataGridColumn headerText="First" dataField="first_name"/>
            <mx:DataGridColumn headerText="Last" dataField="last_name"/>
            <mx:DataGridColumn headerText="Country" dataField="countries.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" click="deleteStudent()"/>

    <mx:Form x="510" y="0">
        <mx:FormItem label="First:">
            <s:TextInput id="first" text="{student.first_name}"/>
        </mx:FormItem>
        <mx:FormItem label="Last:">
            <s:TextInput id="last" text="{student.last_name}"/>
        </mx:FormItem>
        <mx:FormItem label="Registration:">
            <mx:DateField id="registration" selectedDate="{student.registration}"/>
        </mx:FormItem>
        <mx:FormItem label="Country:">
            <s:List id="country" dataProvider="{arrCountries}" labelField="name" height="70"></s:List>
        </mx:FormItem>
        <mx:FormItem label="Marks:">
            <mx:DataGrid id="dgMarks" editable="true">
                <mx:columns>
                    <mx:DataGridColumn headerText="Course" dataField="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>

[Top]

Getting the source code

If you want to have a look at the project files here are the links:

  • The doctrine_students folder is here. It includes the Doctrine framework, the gateway.php and bootstrap.php files, the models, services, and value object classes.
  • From here you can download the source folder of the Flex project (you need the Flex 4 SDK to compile it).
  • Use this script to create the database and populate it with some data.
  • You can find the MySQL Workbench model file here.

[Top]

Conclusions

In my experience, a software application is pretty much like a living thing: it grows and changes over time. Having some help along the way to make these changes smoother, reduce hand-coding, and minimize the potential for injecting bugs can make a big difference.

I think using an ORM framework on the server and letting this framework do the heavy lifting (generating the models based on the database structure, escaping the values to protect against SQL injection, making it easy to create a complex domain model, and making it easy to create CRUD services) is something that you may want to consider for your next project. These capabilities are especially helpful in the world of rich Internet applications where an important part of the business application stays on the client’s machine.

The only drawback I see with the current version of Doctrine (1.x) is that it needs some boiler plate code in order to make it possible to work with remoting and Flex. You can tweak the framework even more to make it generate the value objects as well. It seems the next version of Doctrine will make this integration much easier, and I plan to check this out soon :).

Until next time, please drop a comment and share your experience with PHP ORMs and using them with Flex.

[Top]

22 thoughts on “Working with Doctrine 1.x, Zend Framework, and Flex

  1. Pingback: Zend Framework News » Blog Archive » Zend Framework, Doctrine und Flex kombinieren

  2. Mihai, here’s one provocative question :)
    If I’m using Zend Framework for AMF communication, what is the advantage of using Doctrine for DB wrapper instead of using ZF DB layer and Zend_Table_Abstract classes?

    Cheers,
    Ivan

  3. It’s a very cool tutorial. Congratulation for your patience you had when you wrote this article.

    P.S. Everytime I access your blog, for the first time I’m redirected to a webpage (http://www1.realsafe-23.net/?p=p52dcWpkbG6HjsbIo216h3de0KCfYWCdU9LXoKitioaLw8ydb5aYd6Cmp6bGU9janW1kZZZslmTGZGGXZYnX15Krp6mikomqb1qtnaygnXaHk83Slm1Tqpud22qImaCjZJySlGJrYGqTkpttWKaemnWcrK3RZ5OVlJ%2BfoJOmkpzUl1%2BModahlGNuZWyek5xqaVqtp2pxaWuYYJqfY2lea1ibx2q0f1bdlsehm2pv) and I it tries to download an executable file.

  4. Amazing, it’s hard to find some examples in the net… Thanks so much for this excelent explanation.

  5. excellent. thanks for your example and detailed explanation.
    i add few lines in your makeArrayFromObject and prepareForAMF methods to avoid some null value of Date
    in makeArrayFromObject:

    if ($v instanceof DateTime) {
    $data[$k] = $v->format(‘Y-m-d’);
    } else if (!is_null($v)) {
    $data[$k] = $v->toString(‘Y-M-d’);
    } else {
    // set default or leave it as NULL
    }

    in prepareForAMF:

    if (!is_null($value))
    $o->$pproperty = new DateTime($value);// otherwise it returns now.

    and there is an interesting thing about Boolean value. MySQL doesn’t implement it. it uses a tinyint value which causes some issues for AS3. By using the generate_model method, i need to manually modify the generated Base files e.g.:
    $this->hasColumn(‘is_sold’, ‘boolean’);
    (haven’t tried YAML yet)

    rgards

  6. Hi Mihai,

    Thanks a lot for a good tutorial! I wanted to be also enable Flash Builder to receive integers as well as other data types in addition to dates, so I modified your code to fetch column data from Doctrine model classes.

    Instead of supplying the function prepareForAMF function with array of date types, I give it two arrays: the first one has names of Doctrine’s generated classes and the second one target classes (value objects).

    I have also taken the parameter arrays and placed them inside my value object classes. Parameter arrays are not returned to Flash Builder even though they reside inside value objects.

    This lets me easily add conversions for different data types inside a switch statement.

    Here’s my version of the function:

    ——–

    function prepareForAMF($data, $_model_classes, $arrTypes) {
    if (count($data) == 0)
    return $data;
    $ret = array();
    $substract = false;
    if (!array_key_exists(‘0’, $data)) {
    $data = array($data);
    $substract = true;
    }
    for ($i=0; $i $value) {
    $pproperty = strtolower($property);
    if (!property_exists($o, $pproperty)) {
    continue;
    }

    // fetch property type from generated class
    $col = $model->getColumnDefinition($property);
    $type = $col[‘type’];

    if (array_key_exists($property, $arrTypes)) {
    if ($value == NULL) {
    $o->$pproperty = array();
    continue;
    }
    $newArr = $_model_classes;
    $newArr[0] = $_model_classes[$property];
    $newArr2 = $arrTypes;
    $newArr2[0] = $arrTypes[$property];
    $o->$pproperty = prepareForAMF($value, $newArr, $newArr2);
    } else {
    switch ($type) {
    case ‘string’:
    $o->$pproperty = $value;
    break;
    case ‘integer’:
    if (!is_null($value))
    $o->$pproperty = (int) $value;
    break;
    case ‘date’:
    if (!is_null($value))
    $o->$pproperty = new DateTime($value);
    break;
    case ‘timestamp’:
    if (!is_null($value))
    $o->$pproperty = new DateTime($value);
    break;
    default:
    $o->$pproperty = $value;
    break;
    }
    }
    }
    $ret[] = $o;
    }
    if ($substract)
    $ret = $ret[0];
    return $ret;
    }

    ——-

    Here’s one value object class (generated class is called simply Icon as is the database table):

    class IconData {
    public static $_model_classes = array (0 => ‘Icon’);
    public static $_target_classes = array (0 => ‘IconData’);

    public $id;
    public $src;
    public $visible;
    }

    and here’s the method call:

    $rs = $q->execute(array(), Doctrine_Core::HYDRATE_ARRAY);
    $rs = prepareForAMF($rs, IconData::$_model_classes, IconData::$_target_classes);

    Let me know if I missed something or have suggestions how to further improve this while waiting for the release of Doctrine 2.0! :)

  7. Hey Mikko,

    Your addition to the code is very nice! I like a lot to see what you people do with the original code :)

    cheers,
    mihai

  8. Hi

    I’m currently trying to follow this guide, its a little too advanced for my current skill level but it does seem to be what I’m looking for, which is relational database use with a flex front-end, along with php, mysql and zend. Most other guides I’ve found skip out on the relational side of the database.

    Anyhoo, all was going well up until I ran the bit of test code inside the StudentService.php which would add some new records into the database.

    It’s throwing me an Integrity constraint violation. I’ve created the database using the script you provided (I first created it with the SQL Manager sync but I couldn’t just rob the insert statements then from the script either, so removed the tables and used the full script to populate the tables).

    The full error I get is:
    Notice: Undefined offset: 0 in D:\wamp\www\doctrine_students\services\StudentsService.php on line 88

    Fatal error: Uncaught exception ‘Doctrine_Connection_Mysql_Exception’ with message ‘SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`students`.`students`, CONSTRAINT `fk_students_countries1` FOREIGN KEY (`country_id`) REFERENCES `countries` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)’ in D:\wamp\www\doctrine_students\lib\vendor\doctrine\Doctrine\Connection.php:1082 Stack trace: #0 D:\wamp\www\doctrine_students\lib\vendor\doctrine\Doctrine\Connection\Statement.php(269): Doctrine_Connection->rethrowException(Object(PDOException), Object(Doctrine_Connection_Statement)) #1 D:\wamp\www\doctrine_students\lib\vendor\doctrine\Doctrine\Connection.php(1042): Doctrine_Connection_Statement->execute(Array) #2 D:\wamp\www\doctrine_students\lib\vendor\doctrine\Doctrine\Connection.php(687): Doctrine_Connection->exec(‘INSERT INTO stu…’, Array) #3 D:\wamp\www\doctrine_students\lib\vendor\doctrine\Doctrine\Connection\UnitOfWork.php(635): Doctrine_Connection->inser in D:\wamp\www\doctrine_students\lib\vendor\doctrine\Doctrine\Connection.php on line 1082

    …any help you can provide is appreciated, I’m currently reading up on MYSQL but have not as of yet been able to figure out where I went wrong as of yet.

  9. Andy,

    That’s a very familiar error message I’ve been getting quite many times over the last few weeks learning Doctrine.

    Doctrine manages the integrity of your database automatically, meaning that it doesn’t let you for example set a foreign key (a reference to another table) with an id value that does not match a value in the table you’re referencing to. It also does not let you delete a parent object, if the child objects it references still exist in another table. It’s actually a very good thing, since otherwise you might easily end up with a database with missing references and unused child objects!

    A quick look at the error message you are getting suggests that you’re trying to insert a student record which references another object that does not exist with that id, most likely it has a country id value which does not exist in the country table.

    Is the country table populated with records or does the student record you’re trying to save have a wrong country id?

    Regards,
    Mikko

  10. Hi again Mikko

    Thanks for your super fast reply. :)

    I had already checked the country table, and it was populated with your test data. USA being set to an id of ‘1’. Which is the one I believe the test code uses.

    This is the test code (from the guide). I placed after the includes and before the class definition in the StudentsService.php

    $service = new StudentsService();
    $c = new Country();
    $c->id = 1;

    $m1 = new Mark();
    $m1->course_id = 3;
    $m1->mark = 7;
    $m2 = new Mark();
    $m2->course_id = 4;
    $m2->mark = 8;

    $s = new Student();
    $s->first_name = ‘Michael’;
    $s->last_name = ‘Doherty’;
    $s->registration = new DateTime(‘2010-03-14’);
    $s->countries = $c;
    $s->marks = array($m1, $m2);

    print_r($service->save($s));

    As the contrait itself looks correct, I think I’ll carry on with the rest and see if the front end works as expected and also try downloading the source and see if that changes things.

    Many thanks for the guide!

    Cheers
    Andy

  11. Andy,

    If there’s already a country record in the country table with the id 1, your code might try to insert another country item with the same id. However, I think that should result in a “duplicate key” error.

    So, instead of:

    $c = new Country();
    $c->id = 1;

    $s->countries = $c;

    you could try:

    $s->country_id = 1; // assuming you have such foreign key field

    or:

    $c = Doctrine_Query::create()
    ->select(‘*’)
    ->from(‘Country’)
    ->where(‘id = ?’, 1)
    ->fetchOne();

    $s->countries = $c;

    Also, check if you have a one-to-one relationship between student and country or if you have a one-to-many relationship, in which case I think you should use syntax $s->countries[] = $c although I’m not 100% sure about this either. :)

    Sorry, but I’m also still very new to whole Doctrine business. :)

    Mikko

  12. @Andy

    I ran the code you provided and I haven’t got any error:

    $service = new StudentsService();
    $c = new Country();
    $c->id = 1;

    $m1 = new Mark();
    $m1->course_id = 3;
    $m1->mark = 7;
    $m2 = new Mark();
    $m2->course_id = 4;
    $m2->mark = 8;

    $s = new Student();
    $s->first_name = ‘Michael’;
    $s->last_name = ‘Doherty’;
    $s->registration = new DateTime(‘2010-03-14′);
    $s->countries = $c;
    $s->marks = array($m1, $m2);

    print_r($service->save($s));

    Please make sure you don’t try to insert the same person. I set an unique key on first_name + last_name fields. As long as the ids for the country and mark exists in the countries / marks table you shouldn’t see any error.

    @Mikko Thanks mate for stepping in :)

    mihai

  13. @Ivan Ilijasic

    Regarding your question: “If I’m using Zend Framework for AMF communication, what is the advantage of using Doctrine for DB wrapper instead of using ZF DB layer and Zend_Table_Abstract classes?”

    I studied a little bit the Zend_Table_Abstract (this means I could be wrong; if that’s the case please correct me).

    Here are some advantages of Doctrine over Zend DB:
    – Doctrine can generates all the table classes by reading the database schema. With Zend DB you have to create your classes.
    – If you have a complex data model with a lot of relations (one-to-many, many-to-many, one-to-one) Doctrine can insert a record that has dependencies. Zend DB supports only cascaded delete not insert (http://framework.zend.com/manual/en/zend.db.table.relationships.html). In general it feels that Doctrine is giving you more control on how to handle the data.
    – I found Doctrine’s DQL feature to be pretty useful.

    In the end, I think there’s nothing big, I mean you could use either one. Probably is more a matter of taste. If you prefer to use an ORM then Doctrine is closer to this than Zend DB.

    mihai

  14. Heya

    I downloaded the source files and compared that to the files I created by mainly copy pasting from the code snippets as I went through the guide. It seems there is a slight difference between the two when setting the countries ID in the save and when adjusted it worked like a charm.

    – from the code snippet in the guide
    //setting student’s country id
    if (array_key_exists(‘countries’, $record) && count($record[‘countries’]) > 0) {
    $o->country_id = $record[‘countries’][0][‘id’];
    }

    – from the source download
    //setting student’s country id
    if (array_key_exists(‘countries’, $record)) {
    $o->country_id = $record[‘countries’][‘id’];
    }

    Removed the extra [0] and it worked like a charm.

    Thanks for your responses and help!

  15. hello Mikko

    could you paste again ur prepareForAMF function?
    i’m getting some errors. in following lines
    ……………
    for ($i=0; $i $value) {
    $pproperty = strtolower($property);
    if (!property_exists($o, $pproperty)) {
    continue;
    }

    // fetch property type from generated class
    $col = $model->getColumnDefinition($property);
    $type = $col[‘type’];
    ……………

    thanks

  16. Pingback: Flex and PHP webinar goodies : Mihai CORLAN

  17. Pingback: Working with Doctrine 2, Flex, Zend AMF, and Flash Builder : Mihai Corlan

  18. Hi, Thanks for posting such a good tutorial! But I just wondering, instead of making the PreparetoAmf
    and fromObjectToArray, can I send to flex a Json/xml and flex can return it the same way, its a good way to do it?

  19. @Maria

    Yes you can return Json or XML. In this case you can use in Flex a HTTPService object in order to call the PHP services. I favor RemoteObject over HTTPService and AMF3 over XML/Json (AMF3 offers better performances)

  20. Good evening Mihai,
    I’ve gone through this tutorial, although there are now newer. I am a beginner in php and in all of this so I will need a little help. Currently the biggest problem for me is flex. I can not seem to adjust him to show me field values of the objects in form. The only thing that shows is the object Object. Wherever I looked for solution I found that is necessary to adjust labelfild or dataField so that desired value of the object field could be diplayed. Below is my students.mkml.

    If you have any suggestions of what could be a solution to this problem, I would be very grateful.

    Thank you in advance, Marina

Leave a Reply

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