This section is maintained by the qooxdoo community. Here is how you can contribute.

Creating services: The application, controllers and data models

This part is outdated! In particular, the trunk version of qcl no longer uses xml files to define models. Please come back later

See also

Introduction

Like many other php web application frameworks such as cakephp, zend framework, or symphony, the qcl php backend use established patterns such as controllers, models and the "active record" object-relational-mappping technique. However, being a backend for a qooxdoo application, these patterns have their own spin to them in qcl.

First of all, the backend relies on the json-rpc transport and returns raw data to the client instead of HTML pages, as in a traditional web application framework. That means that the "view" part of the MVC (model-view-controller) architecture used in normal web application frameworks is not needed, since the "view" of the data is simply its tranformation into json data.

On the other hand, the qcl backend tries to make working with model data as easy as possible. Even though ruby-on-rails-inspired web application frameworks abstract away a lot of the complexities of working with relational databases, a common problem with them is that the creation and maintenance of the tables themselves is often still quite tedious. To create or update them requires working with shell scripts or even manual writing of sql. In qcl, the database schema is defined in the model itself and can be modified any time. The database tables will be automatically created and updated by the qcl backend itself, without any user interaction. This significantly eases deployment and maintenance of software based on qcl.

A further feature of the class-based definition of model is that it is possible to use different kind of datastores, such as xml databases or full-text databases such as Couch DB with the same model definition and the same model manipulation API.

General points

  • qcl class names mirror the way the source files are stored in the filesystem. my_custom_Class is located in "my/custom/Class.php" in the "class/" subfolder of the folder in which the top-including script "server.php" is stored.
  • The autoload feature of PHP5 is not used. Instead, each class must be explicitly included with the qcl_import() function. This function will look for 'init.php' files in each of the folders on the path to my/custom/Class.php. This allows to created package-wide settings such as constant definition, the creation of debug filters, etc.

Service controllers

The aim of the MVC pattern is to seperate data, logic and presentation. In qcl, the service classes containing the service methods – which are exposed by the json-rpc server as a web services – act as controllers (See here for more information on the PHP json-rpc server implementation)

Each controller inherits from qcl_data_controller_Controller, which provides the necessary utility methods for processing a request. However, this will not be enforced – you can write your own controller classes as you wish. A service class can have any type of result (scalar, array, object) which will then be transformed into the corresponding json data structure.

However, it is recommended to use a class that inherits from qcl_data_Result as the result of your service methods. The idea is that you can "enforce" the structure of the data that is being returned to the server – which is important if you return data to a json-rpc store marshalling the data into a qooxdoo object on the server. Also, such response objects has automatic getters and setters for the response's properties. For example, you can define your response class like so:

require_once "qcl/data/Result.php";
 
class my_UserDataResult extends qcl_data_Result
{
  var $lastname;
  var $firstname;
  var $email;
}

and use it in a controller's service method:

require_once "qcl/data/controller/Controller.php";
require_once "my/UserDataResult.php";
class class_my_Controller extends qcl_data_controller_Controller
{
  function method_getUserData( $params )
  {
    list($userId) = $params;
    $userModel = new my_userModel;
    $userModel->load($userId);
    $result = new my_UserDataResult;
    $result->set( $userModel->getRecord() );
    return $result;
  }
}

Note that you have to prefix the controller class name by "class_" and the service method name by "method_". This is a security constraint imposed by the RpcPhp server.

From your qooxdoo application, you would call this service method as follows (assuming that the variable userId has been set beforehand):

var store = new qcl.data.store.JsonRpc("path/to/server.php","my.Controller" ); // if you use setServerUrl() in your application instance, you can also provide a null as first argument
store.load("getUserData",[userId],function(userModel){
  this.alert("First Name: "+userModel .getFirstname() + "Last Name. "+userModel.getLastname() );
},this);

Data models

Creating Models

In qcl, just like in similar frameworks, models serve as a blueprint of a piece of data. When accessing a particular record or a number of records from the database, the result will be stored in the model and can programmatically retrieved and processed.

Before we use a model, we have to define it. In qcl, the model has two parts: the xml definition and the php class with which you can work in your controller.

The xml schema definition

The xml definition defines the properties of the model and sets up a corresponding database table. If you change the properties in this definition, qcl will automatically update the database. Please note that while this offers a highly convenient way of managing the schema in which your data is stored in the database, it also presents a considerable risk of data loss if you, for example, change the type of a database column without carefully considering the consequences of the type conversion. Therefore make sure that you always have a backup of your data before you change the definition of the properties in the xml model schema document.

The xml schema document has a couple of interesting features in addition to storing the schema of the model. It is internally managed as a persistent simpleXML object which can be manipulated, and in which a lot of other information on the data model can be stored.

The PHP model class

The corresponding PHP model class is used to actually do something with the data. You can create, retrieve and manipulate data records using an instance of the model class. In some cases, it makes sense to design your model class as a singleton, in particular if you make heavy use of it at various places in your code during the processing of the request. The reason is that the construction of a qcl data model is somewhat expensive - the ease of use comes at the cost of a complex setup procedure. You can always make a class a singleton by adding the following getInstance() method to it:

function &getInstance()
{
  return parent::getInstance(__CLASS__);
}

and retrieving it by

$model =& my_DataModel::getInstance();

To save yourself some typing, you can also create a getInstance() method which loads a specific record into the model (see below):

function &getInstance( $id )
{
  $_this =& parent::getInstance(__CLASS__);
  if ( $id ) $_this->load($id);
  return $_this;
}

Example: Address book record

Let's take the example of an address book. We want to define a data model with several properties that you need for an address book, such as first name, last name, address, telephone number, and so on. Just like classes, XML model defninitions can extend other XML definitions. Each model definition is identified by its "class name", which its PHP class equivalent, but written in Java/JavaScript-like notation.

The base XML definition, from which all other definitions MUST inherit, is qcl_data_model_xmlSchema_DbModel (PHP class) / qcl/data/model/xmlSchema/DbModel.xml (XML model schema id: qcl.data.model.xmlSchema.DbModel). This base definition contains as properties the integer id that is needed to identify the a record in the database (the primary key), and "created" and "modified" timestamp columns. These columns are mandatory for any type of model used in qcl.

Our address book record model, thus, extends from this basic definition (stored in my/PersonModel.xml):

<?xml version="1.0" encoding="utf-8"?>
<schema 
  include="qcl/data/model/xmlSchema/DbModel.xml">
  <model 
    table="persons"
    name="my.PersonModel"
    extends="qcl.data.model.xmlSchema.DbModel" > 
     <!-- exended model properties -->
      <properties>
        <property name="firstname" type="string">
          <sql>varchar(50) NULL</sql>
        </property>
        <property name="lastname" type="string">
          <sql>varchar(50) NULL</sql>
        </property>
        <property name="telephone" type="string">
          <sql>varchar(30) NULL</sql>
        </property>
        <property name="email" type="string">
          <sql>varchar(100) NULL</sql>
        </property>
        <property name="address" type="string">
          <sql>varchar(500) NULL</sql>
        </property>
      </properties>
  </model>
</schema>

Looking at the 'properties' node, we can see that there is a generic definition of the data type, i.e., "string", and a specific sql command that is needed to initialize the column in the sql database. In a future version, the sql definition will be automatically determined, but for now, we need to manually provide it for each property.

We now need the corresponding php class:

class my_PersonModel extends qcl_data_model_xmlSchema_DbModel 
{
  var $xmlSchemaPath = "my/PersonModel.xml"
}

The first time this class will be instantiated, qcl will setup the table 'persons' and all required columns.

Using models

Programmatically, you can retrieve any record of this model by instantiating it and using the load() method to retrieve a record by its id:

require_once "my/PersonModel.php"
$personModel =& new my_PersonModel;
$personModel->load(23);

We will see further below how to retrieve records by searching for values of other properties.

After loading a record, you can get or set the record's property values by using getters and setter, just like in qooxdoo. If you change a property, however, you need to save it back to the database:

$this->info( $addressModel->getLastname() ) // the same as $addressModel->get("lastname");
$addressModel->setFirstname("John");
$addressModel->save();

Creating a new record or deleting an existing one is also easy:

$id = $addressModel->create(); // create() returns the id of the new empty record
$id2 = $addressModel->insert(array(
  'firstname' => "John",
  'lastname'  => "Doe",
)); // we create another record by inserting data
$addressModel->delete(); // the new record (with id $id2) is deleted.

Fine-tuning your model

In addition to the <properties> node, the schema definition can contain other tags that influence the way the data is stored and managed in the database.

Constraints

You can add constraints to your data model:

      <properties> 
        <property name="namedId" type="string" >
          <sql>varchar(32)</sql>
...
        </property>
      </properties>
...
      <constraints> 
         <!-- since we already have a primary key 'id', the 'namedId' key is added as additional primary key -->
        <constraint type="primary"> 
          <property name="namedId" />
        </constraint>      
        <!-- in addition to being part of the primary key, the namedId property has to be unique -->
        <constraint type="unique"> 
          <property name="namedId" />
        </constraint>      
      </constraints>

The property 'namedId' used in this example plays a special role in qcl (it is not defined by default in qcl.model.xmlSchema.DbModel). It serves as a id string that is human-readable (for example, the unique user name of a user or the name of a permission) and/or globally unique (such as an MD5 hash or UUID). The motivation of this is that integer primary keys are on one hand hard to work with, and on the other hand, they are unique only in respect of the database table.

Aliases

In some cases, it makes sense to use the same model (and its properties) with pre-existing tables which, however, differ from the model in that the column names are different from the property names. In this case, you can define aliases for the properties which will be used when the tables are accessed in the database. In some sense, the name "alias" is not fully adequate - it should probably be called "localName".

<aliases>
  <alias for="namedId">username</alias>
</aliases>

This alias definition would force qcl to use the column name 'givenName' when you access the model property 'firstname' through "$model→getFirstname()". In the example, we use a table in which the unique login name is stored in the "username" column. However, we want to work with this column as the namedId property, which allows for some more generalized data handling.

There are more features of the xml schema which I will discuss in a later tutorial.

Querying data

In addition to creating, manipulating and deleting records, a data model is used to search and retrieve records. For this, you can use the find…() methods. For example, you would like to retrieve all address book entries that start with a letter and return them to the client. As described above, we use a result class and a service method for this.

The response class contains the properties of the data model that will be available as a qx.core.Object on the client. Since we are returning not only one, but several data records, the properties are initialized as arrays and will be converted into qx.data.Array objects by the marshaller on the client.

require_once "qcl/data/Result.php";
 
class my_AddressbookRecordResult extends qcl_data_Result
{
  var $lastname = array();
  var $firstname = array();
  var $telephone = array();
  var $email = array();
  var $address = array();
}
function method_getStartsWith( $params )
{
  list($startsWith) = $params;
  require_once "my/AddressbookRecordResult.php"
  $result =& new my_AddressbookRecordResult;
  require_once "my/PersonModel.php"
  $personModel =& new my_PersonModel;
  $personModel->findLike("lastname","{$startsWith}%",/*order by*/"lastname");
  if ( $personModel->foundSomething() )  
  {
     $result->queryResultToModel( $personModel->getResult() );
  }
}

The idea of this is to save bandwidth and computing time: instead of sending a (potentiallly) huge array of maps to the client, containing a lot of redundancy (each map would repeat the property names as keys), where the marshaller would create a huge number of objects, one for each address book entry, we create only one object with properties containing an array of values, one for each result. The marshaller converts the data into one object, with the properties containing qx.data.Array objects.

The way you would call this from the client is as follows:

var store = new qcl.data.store.JsonRpc("path/to/server.php","my.Controller" );
store.load("getStartsWith",["a"],function(personModel){
   for(var i=0; i<personModel.getFirstname().getLength(); i++)
   {
      var firstname = personModel.getFirstname().getItem(i);
      var lastname = personModel.getLastname().getItem(i);
      // etc. 
   }
},this);

You are, of course, free to implement this differently. As noted above, you can return any kind of data structure that can be converted into a json object - just make sure that marshalling the data doesn't get too expensive. In particular, the data binding feature of qooxdoo may require other types of data for the different widget controllers. (@todo: research this).

Back to the query API: You can retrieve records by a variety of methods:

  • findBy( [property name], [value to search for], [property name to sort by], [the properties to retrieve] )
  • You can also use a shorthand method : if you search for property "foo", you can write $model - > findFoo([value to search for], [property name to sort by], [the properties to retrieve] )
  • I have already used findLike in the example above.
  • A more flexible method is findWhere( [where statement], [order by], [properties to retrieve]), a "swiss-army-knife"-like method which would need its own tutorial and which is the base of all other find…() methods.

Once you have executed the query, you can check if the query was successful by using the result of the foundSomething() or foundNothing() methods. If the result was successful, you can iterate over the result by using a "do {} while( $modes - > nextRecord() )" loop:

$model->findWhere("`created` BETWEEN  '2008-01-01' AND '2008-12-31'");
$ids = array();
if ( $model->foundSomething() ) do
{ 
   $ids[] = $model->getId();
}
while ( $model->nextRecord() );

The documentation of the class qcl_data_model_db_Abstract provides more information on the different query methods. In the next tutorial, I explain how models are "linked" with each other through automatic table associations.