Invenzzia » Resources / Articles / A photo gallery tutorial / Full article

A photo gallery with Doctrine and OPT 2

  • Published on 13 July 2009 10:02:00 GMT, updated over 5 years ago by Tomasz Jędrzejewski
  • pl

The tutorial teaches the reader, how to use Doctrine ORM and Open Power Template 2 by writing a sample web application, a photo gallery.

Table of Contents

The primary goal of this article is to teach the reader, how to work with Doctrine ORM and Open Power Template 2 template engine libraries by writing a sample web application, a photo gallery. The article is going to describe the basic configuration and the techniques of connecting the mentioned libraries one with another. Due to the size of the source code, the text contains only the most important pieces, whereas the full version is available to download separately. I assume that the reader is familiar with writing PHP scripts and the concepts of object-oriented programming.

1. Introduction

Doctrine

For many scripts and applications, ordinary communication with the database with the basic interface is not enough. The maintenance of the source code full of hand-written queries could be a painful experience, especially when the database structure is changing or we would like to add some new functionality to the existing version. A remedy for the increasing complexity are ORM systems (Object-Relational Mapping) that reflect the internal database structure in the application, using object-oriented interface and allowing simple and natural manipulations on the data. One of such libraries available for PHP5 is Doctrine, offering the programmer a wide variety of features. Here we are going to mention only a couple of them:

  1. The database structure defined in YAML files – they are legible and independent from the specific database engine and can be easily managed with a version control systems such as Subversion.

  2. The model generator – the classes corresponding to the database tables can be generated automatically from the descriptions in the YAML files.

  3. The database structure generator – moreover, Doctrine can export the entire structure to the database engine using the same files.

  4. A migration tool – simplifies the migration from the earlier version of the database without the risk of accidental data destruction.

  5. DQL: Doctrine Query Language – an extra layer over the SQL language integrated with the models.

  6. Built-in support for complex data structures, such as trees.

  7. Caching and transaction management tools.

Doctrine is built upon the standard database interface, PDO, available in PHP since 5.1 version. It has been expanded with extra functionality and supports a wide variety of database engines. In the MVC design pattern, Doctrine works in the model layer.

Open Power Template 2

In every advanced web application, the presentation layer plays an important role, displaying the data generated by the logic layer and the models. A common technique of building it are templates, simple files with HTML and pieces of extra code in any programming language that decide, where and how to show the particular data. Open Power Template 2 is a template engine providing a comprehensive support of the presentation layer. It consists of an objective programming interface and the parser of XML-based template language. Among various library features, we can mention:

  1. Validating the output document syntax on the server-side.

  2. Declarative template programming – you specify, what you want to have, not – how it is supposed to work and how to implement it.

  3. Lots of high-level language elements simplifying the most common tasks required from the presentation layer: multilingualism support, different types of lists or composing the output document from smaller templates.

  4. The presentation layer is independent from the PHP data types which allows to build portable templates and reuse the same code in different parts of the project or even different projects.

  5. True objective API based on the solutions that can be found in leading PHP frameworks.

To provide the optimal performance, the library compiles the templates to the simple PHP scripts, attempting to process as much as possible during the compilation. In the MVC design pattern, OPT works in the view layer.

How to read this article?

The goal of this article is to show the features offered by Doctrine and Open Power Template and the photo gallery is only a sample project used to achieve it. I am not going to provide detailed descriptions on generating thumbnails or validating the form data, although they are present in the source code. The less important issues are intentionally omitted in the article due to its size and the amount of other things that should be mentioned. The full source code can be found in the accompanying TAR.GZ archive. It has been documented and described, so I recommend to read it, too. The article mentions many different features of the libraries and in my opinion the best way is to split the reading into smaller parts, and the most of all – to experiment!

Although the source code of the photo gallery is available under the X11 license, you should not use it for production purposes.

2. Preparations

Let's begin with setting up the project directory structure. We will not use any external framework in order not to overload the article with more and more things. Instead, we will build a small set of the most basic functions that provide the minimum necessary for us to survive. The directory structure looks like this:

/
/actions
/doctrine
/doctrine/fixtures
/doctrine/migrations
/doctrine/schema
/doctrine/sql
/libs
/libs/Doctrine
/libs/Opl
/libs/Opt
/models
/photos
/photos/thumb
/templates
/templates_c
/tools

And here is the description:

  1. actions – the source codes of the actions that our photo gallery will be able to perform.
  2. doctrine – the information about the database for Doctrine.
  3. libs – the library sources.
  4. models – the models generated by Doctrine.
  5. photos – the directory for storing the uploaded images.
  6. templates – the source templates.
  7. templates_c – the precompiled templates.
  8. tools – additional tools (command-line interface for Doctrine).

The libraries can be downloaded from the following locations:

From the Doctrine archive, we take the lib directory and put it in our /libs. Doctrine.php must be located under /libs/Doctrine.php file. Then we take also the lib directory from the OPT archive and copy its contents to /libs. Finally, we should get something like this:

/libs/Doctrine/
/libs/Doctrine.php
/libs/Opl/
/libs/Opl/Base.php etc.
/libs/Opt/
/libs/Opt/Class.php etc.

3. Creating the database schema

Doctrine uses the YAML files to describe and manage the database contents. The schema consists of two parts.

  1. The database structure.
  2. The initial data (so-called fixtures) that are uploaded together with the structure.

Our photo gallery is too simple to have any fixtures, so we will stop on the structure which is going to have just two tables: for photos and comments. Let's create the /doctrine/schema/schema.yml file with the following content:

---
options:
    type: InnoDB
    collate: utf8_general_ci
    charset: utf8

These options concern the tables and depend on the database engine. As we write this article for MySQL, we have chosen the InnoDB table type and Unicode charsets for the text field values.

As we can see, the YAML syntax is very simple, and moreover – it is converted directly to PHP arrays, contrary to XML. The disadvantage is performance. The existing parsers are not too fast, especially if they are written in pure PHP, but fortunately Doctrine uses YAML only from time to time, usually during the development process, when we want to apply some changes to the database structure. Going back to YAML format itself, you must pay attention to the indentation. There must not be used the tabulation, but a constant number of spaces. In this article I decided to use four spaces for one indentation level.

Let's add the photo table to our model:

Photo:
    tableName: photos
    columns:
        id:
            type: integer(4)
            primary: true
            notnull: true
            autoincrement: true
        title:
            type: string(50)
            notnull: true
        filename:
            type: string(50)
            notnull: true
        date:
            type: integer(4)
            notnull: true
    listeners: [PhotoListener]
    relations:
        Comments:
            class: Comment
            local: id
            foreign: photo_id
            onUpdate: CASCADE
            onDelete: CASCADE
            type: many
            foreignType: one
            foreignAlias: Photo

The first identifier is the name of our model which we will use while accessing to it via Doctrine. In our case, this is Photo. We may also specify the name of the table in the database because it does not have to match the model name. The columns section defines the list of model columns. If we want to specify just the data type, Doctrine provides us a shortened form:

field1: type
field2: type
field3: type(length)

Doctrine uses its own type system and converts them to the appropriate database types depending on the chosen engine. We need only two base types: integer and string, in both cases we must specify their length in bytes. The library must also know about the relationships we want to establish between the tables and here we have the relations section. Each relationship must be identified with its own unique name to access it in DQL queries. Then, we provide the attributes:

  1. class – the name of the model that we create the relationship to.
  2. local – the field in the current table used in the relationship.
  3. foregin – the field in the foreign table used in the relationship.
  4. onUpdate, onDelete – actions that must be performed on the foreign table rows, if we update or delete the matching row in the current table. The CASCADE value is one of the standard options. It orders to update or remove the foreign rows, too.
  5. type – relationship type in the direction from Photo to Comment (many comments)
  6. foreignType – relationship type in the direction from Comment to Photo (one photo for a comment)
  7. foreignAlias – the alias used to access the Photo model from the comments' point of view.

There is also one more section, listeners - we will get back to it later.

The second model will be used to store comments:

Comment:
    tableName: comments
    columns:
        id:
            type: integer(4)
            primary: true
            notnull: true
            autoincrement: true
        author:
            type: string(50)
            notnull: true
        date:
            type: integer(4)
            notnull: true
        content:
            type: string(4000)
            notnull: true
        photo_id:
            type: integer(4)
            notnull: true
    listeners: [CommentListener]
    indexes:
        Photo_Index:
            fields: [photo_id]

The structure is similar to the previous one, but we can notice here another section: indexes which defines table indexes, as the name says. Similarly to relationships, each index must be given a unique name and a list of the columns. The list can be defined with square brackets, and the names are separated with commas.

4. Doctrine command line interface

Doctrine allows to manage the database schema through a simple command-line interface. However, we do not get a ready script, but rather a library that must be configured properly in order to create a complete script. We must create a new file as /tools/doctrine.php and fill it with the following code:

<?php
$paths = array(
    'data_fixtures_path' => '../doctrine/fixtures',
    'models_path' => '../models',
    'migrations_path' => '../doctrine/migrations',
    'sql_path' => '../doctrine/sql',
    'yaml_schema_path' => '../doctrine/schema'
);
 
// Set the autoloader
require_once('../libs/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
 
// Load the configuration
require('../config.php');
 
// Doctrine initialization
$conn = Doctrine_Manager::connection($config['database']['dsn']);
$conn->setCharset($config['database']['charset']);
foreach($config['database']['attributes'] as $attribute_name => $attribute_value)
{
    $conn->setAttribute($attribute_name, $attribute_value);
}
$conn->setAttribute(Doctrine::ATTR_QUOTE_IDENTIFIER, true);
$conn->setAttribute(Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE, true);
 
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_ALL);
$manager->setAttribute(Doctrine::ATTR_MODEL_LOADING, Doctrine::MODEL_LOADING_CONSERVATIVE);
 
// Run the command-line interface
$cli = new Doctrine_Cli($paths);
$cli->run($_SERVER['argv']);

In the first lines we specify the paths to the Doctrine schema directories etc. Then we initialize the autoloader and load the connection configuration. Using those data we have to connect to the database, set some settings and finally – execute the CLI library. The database connection is established with a static method Doctrine_Manager::connection() which takes one argument called DSN. It contains all the information on the connection arguments and looks like this:

dbtype://user:password@host/database

The example above shows its syntax for MySQL databases. Other engines may require different forms that respect system-specific settings. Below, you can find the used Doctrine options explained:

  1. Doctrine::ATTR_MODEL_LOADING – specifies the model loading policy. "Conservative" value means lazy-loading. The models are loaded when they are needed.

  2. Doctrine::ATTR_EXPORT – what to export to the database engine. EXPORT_ALL guarantees us that Doctrine exports also the foreign key and relationship information.

  3. Doctrine::ATTR_QUOTE_IDENTIFIER – allows to use identifiers that are reserved SQL keywords. They will be automatically escaped.

  4. Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE – enables automatic accessor overriding. We will discuss it later.

To make it work, we need to create a confiuguration file: /config.php:

<?php
$config = array(
    'database' => array(
        'dsn' => 'mysql://user:password@host/dbname',
        'charset' => 'utf8',
        'attributes' => array(
            'use_native_enum' => true
        )
    )
);

Now we can run the command line in our operating system. Ensure that your OS knows where PHP is located by entering:

php –version

If you get the information about PHP version, everything is all right. In any other case, you need to add the path to the PHP executable to your PATH environment variable. Once we know that we have PHP, we can switch to the directory with our photo gallery:

cd /path/to/photogallery/tools
php doctrine.php build-all-reload

The build-all-reload command removes and uploads once again the schema to the database engine using the settings from the YAML files. Furthermore, it generates the PHP models. As you can see, there should appear some new PHP files in /models directory. They are the PHP models that we are going to manipulate the data in the database. Our database structure is ready and now we can go to the next step – extending our models with some extra custom features.

5. Extending models

By default, Doctrine generates two files for each model: ModelName and BaseModelName in /models/generated. The second one is rebuilt every time we run build-all-reload so we should not modify it manually. On the other hand, we have the first class that extends the base model. Here we can add some extra code that cannot be directly specified in YAML format.

The models should prepare the data to be displayed by the presentation layer (in our case – Open Power Template) so that it does not have to process them on its own. To achieve this, we need to extend the default models to provide some new information generated from the raw data from the database. In our case the critical field is the field with the photo/comment creation date. We know the following facts about it:

  1. In the database, we store the data as simple Unix timestamp (the number of seconds since 1 January, 1970). Although Doctrine supports the timestamp type, we do not use its features and moreover it would force us to perform even more complex conversions on the script side.

  2. The date field must be initialized with the current time while adding a new row.

  3. The script should display the date in the human-readable format, for example May 14, 2009, 11:15 AM.

The first goal is done, there are two to go and we will use two new Doctrine features for it:

  • Event listeners.
  • Automatic accessor overriding.

Doctrine allows the model to respond on various events and perform extra user-defined operations then. For example, before we insert a new row, we would like to initialize some fields automatically so that the model user does not have to remember about them. Some of the events can be handled simply by overriding the appropriate method in the model, and some other – with an event listener class. The listeners section in the YAML file specified the event listener classes used by the model and we just need to write them. Let's open the /models/Photo.php file and add a new method to the empty Photo class:

    public function preInsert($event)
    {
        $this->date = time();
    } // end preInsert();

This is it: before a new row is inserted to the database, Doctrine runs the very method and sets the date field to the current time. Besides preInsert(), the library offers several other methods, such as postInsert() (executed after the row insertion) and similar operations for updating and deleting. They are especially useful, if our rows represent some files or directories in the file system. With event listeners, the models can handle them transparently to the user. As an exercise, you might want to add postDelete() method that removes the photo from the disk together with the row.

The date formatting must be performed in two ways. The first one is the use of the automatic accessor overriding allowing us to create fictional fields in our models. From the user's point of view, there is no difference between the true and fictional fields – we may read and save to them. However, Doctrine notices the difference and in case of auto accessor overriding, it redirects the call to such "false field" to one of our methods where we can do whatever we want to. In our case, the original date field contains the date in the system format: the number of seconds since 1.1.1970. A corresponding false field, date_text will run the accessor method that executes the date() function on the original date and returns the formatted text. Below, we can see the accessor method:

    public function getDateText()
    {
        return date('F j, Y, g:i a', $this->date);
    } // end getDateText();

The formatted date is created on-the-fly on the user demand from the value of the date field. To process the value setting, we need to create setDateText($value) method, however in our case we do not need it. Please note that Doctrine automatically converts the naming style – the fields use the foo_bar convention, whereas their accessor methods – getFooBar and setFooBar.

The automatic accessor overriding should be used very carefully. They have a higher priority over the ordinary fields and the functionality is not too well-designed, because... it was not supposed to appear in Doctrine. It was forced by the library users, but the developers decided to make it disabled by default. The problem is that Doctrine models have some internal methods like getSomething that have nothing to do with the automatic accessors. If we create a field with one of these "reserved" names by accident, our script will surely crash. If you are going to use this feature, please take a look at the API documentation to see, what field names must not be used then.

However, the accessor method solves the problem only partially. It works only, when we access the model object, whereas in most cases we would like the SELECT statements to return arrays because of performance reasons. Here, we may find the event listener class very helpful where we plug into the hydration procedure (the conversion of the result set to the requested format) and add the required extra fields to the output array. This is a complete class that can be pasted to the same file, as the model:

class PhotoListener extends Doctrine_Record_Listener
{
    public function preHydrate(Doctrine_Event $event)
    {
        $data = $event->data;
 
        if(isset($data['date']))
        {
            $data['date_text'] = date('F j, Y, g:i a', $data['date']);
        }
 
        $event->data = $data;
    } // end preHydrate();
} // end PhotoListener;

The preHydrate() method is executed when Doctrine attempts to build an array from the query result. We check whether it contains the date field and optionally generate its formatted version. You must pay attention to two issues here:

  1. You cannot access the $event->data field directly. Its value must be rewritten to a temporary variable and at the end – back to the field, as in the example.

  2. The result set does not have to contain the date field – before we perform any operation on it, we must check if it actually exists.

The Comment model contains the date field to that should be processed in the same way. As an exercise, try to extend the comment model using the code we have already written for Photo as a sample.

6. Writing the script

We start with writing index.php file which will initialize both libraries and load the action requested in the URL.

<?php
// Initialize the include path and the timezone.
set_include_path(get_include_path().PATH_SEPARATOR.'./models'.
    PATH_SEPARATOR.'./models/generated'.PATH_SEPARATOR.'./libs');
date_default_timezone_set('Europe/London');
try
{
    // Initialize Doctrine autoloader
    require('./libs/Doctrine.php');
    spl_autoload_register(array('Doctrine', 'autoload'));
 
    // Initialize OPL autoloader
    require('./libs/Opl/Base.php');
    Opl_Loader::setDirectory('./libs/');
    Opl_Loader::register();

The first thing that needs to be configured is include_path, where we must add the paths to the model directories. Then we start the try...catch block to capture the exception from the libraries and initialize the autoloaders. Here we will stop for a while to explain some issues concerning them. Open Power Template is actually the first member of a bigger project, Open Power Libs. In order not to duplicate the same functionality, the OPL libraries share a common core located in /libs/Opl/ directory. It provides the autoloader, error handling, configuration and the plugin system. The autoloader itself does not use include_path, but rather provides its own methods to specify the library locations in more flexible way. We decided to use Opl_Loader::setDirectory() that specifies that all the supported libraries are located in the same directory. Then we register the autoloader in the PHP stack.

You should have probably noticed that OPT and Doctrine share the same class naming style. OPL autoloader is a general-purpose tool that is able to handle also other libraries. Theoretically, we could use it to handle Doctrine, too, however we would have to write an extra handler to deal with models. Actually, such handler is included in the second library, Open Power Classes, but because the article is not all about OPT, we can also simply initialize the original Doctrine autoloader before the OPL one.

Now, as the autoloader issue has been explained, we will do some configuration:

    // Enable the extended error messages for debugging purposes
    Opl_Registry::setState('opl_extended_errors', true);
 
    // Load the configuration
    require('./config.php');

While moving to a production server, you should disable opl_extended_errors option. Otherwise, every exception thrown by OPT will be accompanied with a lot of extra information that are useful while debugging, but may be potentially dangerous to the web application and the server. Doctrine can be initialized in the same way, as in the command-line interface script:

    // Initialize Doctrine
    $manager = Doctrine_Manager::getInstance();
    $manager->setAttribute(Doctrine::ATTR_MODEL_LOADING, Doctrine::MODEL_LOADING_CONSERVATIVE);
    Doctrine::loadModels('./models/');
 
    $conn = Doctrine_Manager::connection($config['database']['dsn']);
    $conn->setCharset($config['database']['charset']);
    foreach($config['database']['attributes'] as $attributeName => $attributeValue)
    {
        $conn->setAttribute($attributeName, $attributeValue);
    }
    $conn->setAttribute(Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE, true);
    $conn->setAttribute(Doctrine::ATTR_QUOTE_IDENTIFIER, true);

Next, we must initialize Open Power Template to make it work. Create the Opt_Class object and load the configuration from the array in config.php:

    // Initialize OPT
    $tpl = new Opt_Class;
    $tpl->loadConfig($config['opt']);   
    // Load some extra stuff
    require('./libs/stuff.php');
 
    $tpl->setup();

Moreover, the /libs/stuff.php file adds some extra functions and OPT components that will help us building nice HTML forms with templates. We will back to them later. Once the library is configured, we need to call Opt_Class::setup() method.

Open Power Template is designed similarly to the view layers in popular frameworks. Contrary to most of the available template engines, we do not have a class-for-everything here. OPT features smaller, specialized classes that better reflect the reality. The most fundamental concept are views, the objects of Opt_View class. You may look at them as at the data with accompanying template. One script may create many views in one request and compose the output document from them. You do not have to configure each of them separately (the configuration is kept globally in Opt_Class object) and furthermore, you do not have to worry about the naming collisions between the data in two views. To process a view and display its result, we need another concept, an output system. Output systems are objects of the classes that implement Opt_Output_Interface. OPT provides two standard output systems. One of them is Opt_Output_Http that sends the view processing result to the browser and allows to manage the HTTP headers. Our script will receive the view from the action and display it using the output system:

    $action = 'list';
    if(isset($_GET['action']) && ctype_alpha($_GET['action']))
    {
        $action = $_GET['action'];
    }
    require('./actions/'.$action.'.php');
    $view = action();
    $output = new Opt_Output_Http;
    $output->setContentType(Opt_Output_Http::XHTML, 'utf-8');
    $output->render($view);

As we can see, the library provides also a convenient wrapper to send the Content-type header. In our case, we want to send the information on the XHTML document encoded with UTF-8. Note that OPT actually sends this header only if the browser supports it. In other case, it gets an ordinary text/html header.

Finally, we must also process the exceptions that could be generated during the development process. OPL provides a nice, standard error handling interface that equips the exceptions with lots of useful information that help finding the reason of the problem. Unfortunately, Doctrine does not do so and we have to wrap its exception into a standard OPL exception to get a nice message, too (but without the extended help).

}
catch(Opt_Exception $e)
{
    $handler = new Opt_ErrorHandler;
    $handler->display($e);
}
catch(Doctrine_Exception $e)
{
    $wrapper = new Opl_Exception($e->getMessage());
    $handler = new Opl_ErrorHandler;
    $handler->display($wrapper);
}

At last, we have to add the OPT configuration to config.php file:

$config = array(
    'database' => array(
        'dsn' => 'mysql://user:password@host/dbname',
        'charset' => 'utf8',
        'attributes' => array(
            'use_native_enum' => true
        )
    ),
    'opt' => array(
        'sourceDir' => './templates/',
        'compileDir' => './templates_c/',
        'stripWhitespaces' => false,
        'charset' => 'utf-8'
    )
);

The configuration options for OPT:

  1. sourceDir – specifies the directory for the source templates.
  2. compileDir – specifies the directory for the precompiled templates. PHP must have a write access to it.
  3. stripWhitespaces – as we know, the templates are XML documents, so it is not a problem for OPT to remove the unnecessary white spaces during the compilation and obfuscate the output code. However, in the development process, it is good to disable it.
  4. charset – the encoding for Opt_Output_Http. Actually, we do not need it here, because we have specified it manually in the setContentType() method, but we can also use the configuration to provide such piece of information.

The next thing we have to do is to create the base template with the general structure of the HTML code. We will create the first template for OPT and save it under /templates/layout.tpl. Below, you can find its content:

<?xml version="1.0" ?>
<opt:root include="snippets.tpl">
<opt:prolog version="1.0" />
<opt:dtd template="xhtml10transitional" />
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <title>{$title} – photo gallery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" href="style.css" type="text/css" />
</head>
<body>
<div id="header">
    <h1>Photo gallery</h1>
    <h2>{$title}</h2>
</div>
<div id="content">
    <opt:insert snippet="content">
        <p>No content defined</p>
    </opt:insert>
</div>
<div id="footer">
    <p>A sample photo gallery for the article <em>A photo gallery with Doctrine and Open Power Template 2</em> by Tomasz Jędrzejewski.</p>
    <p>Sources available under the terms of X11 license</p>
</div>
</body>
</html>
</opt:root>

OPT is de facto an XML parser and in the default mode we have to keep maximum compatibility with XML standards. Each template must contain an XML prolog which is not displayed in the browser, but used for OPT internal purposes. We need also the main tag and this role is played here by opt:root which allows also to perform some extra operations. In our case, we load the external template, snippets.tpl which will be introduced soon.

Generally speaking, OPT template language consists of two elements:

  1. Instructions – the XML tags and attributes, usually in opt namespace. They control the template processing and perform various manipulations. A single instruction may consist of several tags and attributes.

  2. Expressions, for example $a+$b – they play the same role, as in PHP and the rules used to construct them are very similar. Some syntax elements are modified in order not to cause problems with XML syntax. The expressions can be used as instruction/other tag attribute values or placed directly in the static text, using curly brackets. Their result will be displayed in that place.

The two first instructions that display something are opt:prolog and opt:dtd. They are helper instructions that simplify the prolog and DTD generation for the output document. The second one allows to specify the document type more easily, so that you do not have to remember the whole quite long Doctype address. Later, we display the page title from the variable $title. In the place where we want to display the content of the action, we used opt:insert.

OPT, like any other template engine, supports building the output document from many smaller templates. Otherwise, we would have to rewrite the header and the footer to every single template. However, contrary to Smarty and similar solutions, OPT requires us to follow the XML rules. We are not allowed to begin the <HTML> or <BODY> tag in one template and close it in another one. Instead, we must look at the templates as containers that may include smaller templates within themselves. One of the modularization techniques supported by OPT is template inheritance that came to PHP template engines from Python libraries. The name suggests that it is quite similar to the inheritance in the object-oriented programming. This is a right suggestion and we are going to see why.

The role of the class is played by a single template, and methods are "represented" by snippets. They are pieces of XML code that have been given a unique name. When one template extends another, it may overwrite the snippets defined by the extended template and add some new ones. Similarly to the overloaded methods, overloaded snippets may still refer to their parent "implementations". The only significant difference between OOP and templates are the base templates. They do not contain snippets, but an ordinary content with the free placeholders that run particular snippets. Our layout.tpl is such a base template for the rest of the view. Through the opt:insert instruction we specify, where the action-specific piece of code must be shown, in our case within the <div id="content"> tag. If the requested snippet is not defined, we may also specify the default content.

7. The first action

It is the right time to create our first action that displays the list of uploaded image thumbnails. We will begin with the template to continue the inheritance issue started in the previous chapter. The following content must be saved in /templates/index.tpl file:

<?xml version="1.0"?>
<opt:extend file="layout.tpl">
    <opt:snippet name="content">
        <opt:show name="items" cols="5">
            <table class="photos">
            <opt:grid>
                <tr>
                    <opt:item><td><a parse:href="'index.php?action=preview&id='~$items.id"><img parse:src="'photos/thumb/'~$items.filename" parse:alt="$items.title" /></a></td></opt:item>
                    <opt:emptyItem><td></td></opt:emptyItem>
                </tr>
            </opt:grid>
            </table>
        <opt:showelse><p>No photos</p></opt:showelse>
        </opt:show>
 
        <p class="links"><a href="index.php?action=photo">Upload a photo</a></p>
    </opt:snippet>
</opt:extend>

This template extends layout.tpl, using opt:extend instruction instead of opt:root. Within this tag, we are allowed to use opt:snippet instructions only – the other content will be ignored by the compiler. Because the base template needs a snippet named content, we create it and specify the content to be shown. Our main page is going to display a list of image thumbnails in five columns. It is a quite annoying task that leads to very ugly code full of mathematical operations that check whether the end of the row is reached etc. But why should we waste our time to implement it, if OPT provides us a ready and tested solution?

OPT introduces the concept of sections to display different types of lists. You can think of them like of smart loops. There are several types of sections, depending on the list type we want to get. However, all of them are designed using the same rules and can cooperate one with another. Contrary to PHP, we do not have to worry about how such loop works and how is implemented. All we have to do is to specify the look of a single list element and give the section a name, so that OPT could find the data for it. Here, we use the opt:grid section that is designed especially to display the items in columns. The exact number of columns can be controlled with cols attribute. Within the tag, we have to use two more tags: opt:item that specifies a single list element and opt:emptyItem – the look of an empty item for the last row, if the number of list elements is not a multiplicity of cols. Moreover, we do not want to display the table, if there are no uploaded images, so we enclose the whole section within opt:show. Opt:showelse lets us define the alternative text that should be displayed, if there are no elements in the list.

Let's get back to the look of a single thumbnail, because it illustrates, how to create a dynamic value of HTML tag attribute. We must not use curly brackets then. Instead, we change the namespace of the attribute to parse to indicate that it contains a value that must be processed by OPT. Please note, how we refer to the section element variable: $sectionName.variableName. There are no iterators, array accessors, object accessors etc. - the whole template is independent from PHP data types. The expression 'index.php?action=preview&id='~$items.id is a simple string concatenation. OPT uses the ~ operator for concatenation (an idea taken from D programming language), because dot is reserved for other purposes.

To sum up, we see that with OPT, the exact behavior and even data types in the template are not a subject of our concern anymore. Does the template mention that the section data must be arrays? No, because it does not have to. The compiler selects the data type during the compilation using the information from the script. Similarly, we do not need to reinvent the wheel and write the whole algorithm to display the photos in the columns.

After this quite long introduction to the templates, we get back to the PHP code and Doctrine where we will be introduced into DQL. Save the source code of the main page to /actions/list.php:

<?php
function action()
{
    $view = new Opt_View('index.tpl');
    $view->title = 'Photo list';
    $view->items = Doctrine_Query::create()
        ->select('id, title, filename')
        ->from('Photo')
        ->orderBy('id DESC')
        ->execute(array(), Doctrine::HYDRATE_ARRAY);
    return $view;
} // end action();

The code required by the action controller is reduced to the absolute minimum. All we have here is a simple function called action(). The action must fill the OPT view with some data retrieved from the database and return it to be displayed by index.php. The most interesting code snippet is undoubtedly the Doctrine part which sends a query to the database. DQL is very similar to SQL and you should have no problems with switching to it. The main difference is the fact that we are operating on models rather than database tables. DQL can be represented in two levels: the plain text query and objective representation. For the sake of performance, the first one should not be used very often, as Doctrine needs to parse such query and translate it to SQL. The Doctrine_Query::create() method creates the query object and various additional methods such as select() or orderBy() let us add different clauses to it.

Someone may say that this is only a visual difference, but this is not true. Contrary to SQL, Doctrine does know the relationships between the models and automatically applies the joins coming out of the foreign keys. For example, a query using two models would look like this:

Doctrine_Query::create()
    ->select('a.*, b.*')
    ->from('Table1 a')
    ->innerJoin('a.Relation b');

If Doctrine knows that the two models are connected with a relationship, the expression a.Relation b is parsed correctly and does not result in producing a Cartesian product. The differences are even more visible, if we take a look at the result structure. SQL gives us the flat data, whereas Doctrine converts them into a hierarchical tree according to the relationships.

The last action that must be performed on a query object is the call of execute() method. The first argument represents the additional query parameters (usually we provide an empty array here) and the second one – the result hydration mode. By default, Doctrine hydrates the data into objects but due to the performance reasons we should use arrays unless we need the extra model functionality. The library returns the result similar to the one below:

array(0 =>
    array('id' => 1, 'title' => 'Photo 1', 'filename' => 'file.jpg'),
    array('id' => 2, 'title' => 'Photo 2', 'filename' => 'file.jpg'),
    array('id' => 3, 'title' => 'Photo 3', 'filename' => 'file.jpg'),
    // etc.
);

It is also a correct default data format for OPT sections.

8. Photo upload form

Dynamic websites are full of forms. Their usage seems to be simple from the script point of view, but nothing further from the truth! There have been developed dozens of libraries to build advanced dynamic forms with good error reporting and hundreds of articles have been written on it. If you work with a PHP framework, you should probably know their solutions based entirely on PHP language. Although powerful, they have one significant disadvantage: complicated look&feel configuration system. Complex layout are very hard to construct and require advanced knowledge of the library structure. All these problems come from the fact of using PHP as a templating language – some frameworks even introduced "template templates" due to significant design problems. This is why Open Power Template provides a dedicated tool to support the form processing system in the presentation layer: components. A component consists of two parts:

  1. PHP object defining the form field logic (i.e. the textarea behavior)
  2. Template port defining the layout of the field and its neighborhood (the place for errors, title, descriptions, etc.)

The component object can be created on the script side and deployed in a port from a template variable, or created entirely on the template side (the form system can control the entire process transparently). You may find it very useful in simple forms or very complex designs where the manual control over the field location is required. We do not have to define the layout of each port separately (imagine the potential amount of work for a website with 20 forms of 10 fields each). Unlike the pure PHP equivalents, components do not reinvent the wheel in this area, but simply use the already well-known snippets. We create a single snippet with the port layout and then load it into ports. The snippet is automatically matched to a particular context during the template compilation and gives much more freedom of creation. However, in order to use it, we must write a simple form processing system compatible with OPT first.

The full sources of the form processing system can be found in /libs/stuff.php. Here, we are going to introduce writing the components only. A component object must be an object of a class implementing Opt_Component_Interface. It requires 9 methods to be implemented defining the following behaviors:

  1. Component parameter management (saving, reading, testing)
  2. Loading the full information of the field from an external source (so-called data sources)
  3. Displaying the component itself
  4. Event handling
  5. Managing the attributes of certain HTML tags in the port (useful for automated CSS configuration).

Our system is going to use three types of form fields: text inputs, text areas and file choosers. This means that we have to create a component class for each of them. However, please note that most of the component logic can be shared and implemented in a base class. The role of the derived classes is to implement the field-specific display function.

abstract class baseComponent implements Opt_Component_Interface
{
    protected $_initialized = false;
    protected $_params = array();
    protected $_form;
    protected $_view;

This is the header of our base component class from /libs/stuff.php. It uses the following class fields:

  1. $_initialized – was the component object initialized by OPT?
  2. $_params – the component parameter list.
  3. $_form – the array with form information (what fields are correctly filled, what errors occurred and where).
  4. $_view – the view that deployed the component.

We start the implementation by writing a constructor:

    public function __construct($name = '')
    {
        $this->_params['name'] = $name;
    } // end __construct();

OPT allows to pass a single, optional argument to the constructor which will be recognized as a field name by us. The next step is to implement the component deployment in setView():

    public function setView(Opt_View $view)
    {
        $this->_view = $view;
 
        if(!$this->_view->defined('form'))
        {
            return;
        }
        $this->_form = $this->_view->form;
        $this->_initialized = true;
    } // end setView();

This method is automatically called during the template execution. The view that deploys the component is passed in the argument. Our components assume that the view contains the $form template variable with the form information that the field belongs to. If we notice that such variable does not exist, we simply finish the work and do not set the initialization marker in $this->_initialized (it will be used later to capture such errors).

The methods to manage the component parameters are very simple and you should have no problems with understanding them:

    public function set($name, $value)
    {
        $this->_params[$name] = $value;
    } // end set();
 
    public function get($name)
    {
        return $this->_params[$name];
    } // end get();
 
    public function defined($name)
    {
        return isset($this->_params[$name]);
    } // end defined();

There is one more way to pass the data to the component: the setDatasource() method. It aims to handle loading the complete component configuration from a single source, for example – an external array. Here, it will simply scan the provided array and register the encountered elements as component parameters. Your implementation may be completely different – generally speaking, it's your code and it's you who decide, how to actually use the offered features.

    public function setDatasource($data)
    {
        foreach($data as $name => $value)
        {
            $this->set($name, $value);
        }
    } // end setDatasource();

The next method is a bit more important. The component is able to manipulate attributes of certain HTML tags chosen by the template author within the component port with the com namespace (for example, <com:div>). There may be many possible uses of this feature. We want to change the default CSS class of the entire form field, if the user data are invalid. The method takes an associative array of the values and returns the modified array which is converted back to HTML by OPT.

    public function manageAttributes($tagName, Array $attributes)
    {
        if($tagName == 'div' && !is_null($this->_form[$this->_params['name']]))
        {
            $attributes['class'] = 'error';
        }
        return $attributes;
    } // end manageAttributes();

To simplify the tag identification, the method receives also the tag name, however, you have to be aware of one quite important issue here. The template is processed as an XML document during the compilation, whereas the components work during the next stage, template execution. The XML tree does not exist for a long time then, and all we have to identify the tags their names and attribute lists. For example, a double occurrence of com:div tag within a single component port causes this method to be launched twice, but with the same tag name each time. In order to distinguish between them, you must provide some extra mechanisms. Also note that you have no access to the tag content. The last of the common component methods handles the component events. In the component port, the template designer may place the opt:onEvent tags that display some content on the specified event occurrence. OPT asks the component method processEvent() to get to know whether the event actually occurred. The method may also use the view object passed with setView() to set extra variables in the template. In our case, there will be two recognized events:

  1. error – the field was filled incorrectly by the user. The component object registers the error message in the view then, so that we could display it.

  2. notInitialized – script error – we forgot to assign the form information to the view.

    public function processEvent($event)
    {
        if($event == 'error')
        {
            if(!is_null($this->_form[$this->_params['name']]))
            {
                $this->_view->error = $this->_form[$this->_params['name']];
                return true;
            }
        }
        elseif($event == 'notInitialized')
        {
            return $this->_initialized == false;
        }
        return false;
    } // end processEvent();
} // end baseComponent;

The code is not quite complex. To determine if the field is filled correctly, we test the $this->_form array to see if the form processor set the error message for it. Once we find it, we assign it to the template and return true (the event occurred). Otherwise, the method returns false.

The abstract component class is ready and now we must write the actual components that provide the implementation of the last remaining method – display(). The display code is different for each component, contrary to the rest of the logic and this is why it must be implemented in another class. For the formInput component the source code looks like this:

class formInput extends baseComponent
{
    public function display($attributes = array())
    {
        $attributes['type'] = 'text';
        $attributes['name'] = $this->_params['name'];
 
        if(!$this->_form['valid'])
        {
            $attributes['value'] = htmlspecialchars($_POST[$this->_params['name']]);
        }
 
        echo '<input';
        foreach($attributes as $name=>$value)
        {
            echo ' '.$name.'="'.$value.'"';
        }
        echo '/>';
    } // end display();
} // end formInput;

OPT passes the list of attributes of <opt:display/> tag in the component port. We may include them in the display process or simply ignore. In our case, they are treated as ordinary attributes of the form field and we add the component parameters to them, possibly overwriting some settings. The method does three things:

  1. Sets the field name using the name parameter.
  2. If the field is filled incorrectly, the value is rewritten from $_POST in order to give the user the possibility to correct the data.
  3. Generates the HTML code of the field, but without any extra layout, such as field names, descriptions or error messages, as this is defined in the template.

As we mentioned earlier, our gallery needs three types of components. Their implementations of display() methods are quite similar, so it will not be shown here. You may try to implement them on your own as an exercise, and we will register the components in OPT, assigning XML tags for them:

$tpl->register(Opt_Class::OPT_COMPONENT, 'opt:input', 'formInput');
$tpl->register(Opt_Class::OPT_COMPONENT, 'opt:textarea', 'formTextarea');
$tpl->register(Opt_Class::OPT_COMPONENT, 'opt:file', 'formFile');

The registration is not necessary, but it allows to give the components their own XML tags to create them entirely on the template side. Below, you can see a sample component port that automatically creates the object:

<opt:someComponent datasource="$dane" str:name="field">
  <opt:set str:name="title" str:value="Field name" />
  <com:div>
      <p><label parse:for="$system.component.name~'_id'">
         {$system.component.title}</label></p>
      <opt:display />
      <opt:onEvent name="error">
         <p class=”error”>Error: {$error}</p>
      </opt:onEvent>
  </com:div>
</opt:someComponent>

The description of the used tags and attributes:

  1. <opt:someComponent> - creates a port together with the component object of the specified type registered as opt:someComponent tag.
  2. datasource – the component data source mentioned earlier. This is an optional attribute.
  3. str:name – the rest of the attributes is treated as component parameters. The str namespace tells us that the value is not an OPT expression (i.e. a variable), but an ordinary string. It's just a syntactic sugar for name="'string'"
  4. <opt:set> - an alternative way to define component parameters.
  5. $system.component.name – the special variable $system provides the access to lots of useful information. In this case, we are given the access to all the current component parameters.
  6. <opt:display> - here the component object is actually displayed.
  7. <com:div> - the HTML tag captured by manageAttributes().
  8. <opt:onEvent> - an event definition. The name attribute defines the event name.

To create just a component port without the object, we use the <opt:component> tag instead of <opt:someComponent>. Furthermore, it requires the attribute from to be defined that specifies, where to load the component object from. Such port can be enclosed in a section, and the result is a dynamically constructed form. All the fields are created on the script side as component objects, packed into a section and rendered one after another. If the variable does not contain a valid component object, the port is simply not displayed.

And now, another trick, this time entirely for our gallery. Create the /templates/snippets.tpl file with the following content:

<?xml version="1.0" ?>
<opt:root>
    <opt:snippet name="formField">
        <com:div>
            <label parse:for="'l_'~$system.component.name">{$system.component.title}: </label>
            <opt:display parse:id="'l_'~$system.component.name" />
 
            <opt:onEvent name="error">
                <p class="error">{$error}</p>
            </opt:onEvent>
            <opt:onEvent name="notInitialized">
                <p class="error">The component was not initialized!</p>
            </opt:onEvent>
        </com:div>
    </opt:snippet>
</opt:root>

All the form fields in our project share the same layout, and it is a nonsense to repeat the same code for each of them. The port content is packed into a snippet (not the port itself – note that there are neither <opt:someComponent>-like tags nor <opt:component>) and we insert it into the component ports in the photo upload form:

<?xml version="1.0"?>
<opt:extend file="layout.tpl">
    <opt:snippet name="content">
        <opt:if test="not $form.valid">
            <p>The form has not been filled correctly.</p>
        </opt:if>
        <div class="form">
            <form method="post" action="index.php?action=photo" enctype="multipart/form-data">
            <opt:input name="title" template="formField">
                <opt:set str:name="title" str:value="Photo title" />
            </opt:input>
            <opt:file name="file" template="formField">
                <opt:set str:name="title" str:value="File" />
            </opt:file>
            <input type="submit" value="Upload" />
            </form>
        </div>
    </opt:snippet>
</opt:extend>

The base template layout.tpl loads snippets.tpl with the default field layout. We can insert it into the port with the template attribute. To change the form look, we simply modify the formField snippet and the changes are automatically visible in all our forms that use it. Note that OPT matches the snippet to the current port context. Similar effect is much harder to achieve in PHP and this is especially visible in the most popular frameworks, such as Symfony or Zend Framework – their developers were not able to create such an easy system, using pure PHP. They were forced to use complex object-oriented programming or... template templates that need a dedicated parser, too. This example is the most significant proof that template languages do not have to copy drawbacks of PHP, but can simplify lots of things. Another fact worth mentioning is that OPT processes the snippets during the template compilation and produces compact, simple and fast PHP code. It is possible, because this code does not have to be clean and readable by programmers.

Important

The snippets can be actually inserted to all the tags with the opt:use attribute. The component ports provide their own tag which does not remove the opt:set tags from the default port content, so that we could assign the parameter values more easily. Try to replace the template attribute with opt:use and see, what happens. The fields will not have the title set then.

The image upload action code will be saved in /actions/photo.php:

<?php
function action()
{
    if($_SERVER['REQUEST_METHOD'] == 'POST')
    {
        $form = array(
            'valid' => true,
            'title' => null,
            'file' => null,
        );
        // Form data validation
        validateLength(&$form, 'title', 3, 50, 'The title length must be between 3 and 50 characters.');
        validateUpload(&$form, 'file', './photos/', 'File upload failed.');
 
        if($form['valid'])
        {
            // Generating thumbnail
            resizeImage('./photos/'.$_FILES['file']['name'], './photos/thumb/'.$_FILES['file']['name']);
 
            // Saving the data to the database
            $photo = new Photo;
            $photo->title = $_POST['title'];
            $photo->filename = $_FILES['file']['name'];
            $photo->save();
 
            // Showing the message
            $view = new Opt_View('message.tpl');
            $view->title = 'Message';
            $view->message = 'The photo has been uploaded.';
            $view->redirect = 'index.php';
            return $view;
        }
        else
        {
            // The form has been incorrectly filled.
            $view = new Opt_View('photo_add.tpl');
            $view->form = $form;
            $view->title = 'Add photo';
            return $view;
        }
    }
    else
    {
        // Default form content.
        $view = new Opt_View('photo_add.tpl');
        $view->form = array(
            'valid' => true,
            'title' => null,
            'file' => null,
        );
        $view->title = 'Add image';
        return $view;
    }
} // end action();

validateLength() and validateUpload() perform the simplified data validation. We have not specified their code here, because this is not the main topic of the article – you can find them together with extra comments in the full source code included to the article. All we need to know now is that they fill the $form array with the form information used by the components. The action does the following:

  1. If the page is accessed for the first time, it displays a clean form.
  2. If the form has been filled incorrectly, the form is redrawn and the fields are filled with the user-entered content and the corresponding error messages.
  3. If the form has been filled correctly, the image is uploaded and Doctrine adds a new row to the database.

Creating a new row is very easy in Doctrine. Let's take a look at the source code once more:

$photo = new Photo;
$photo->title = $_POST['title'];
$photo->filename = $_FILES['file']['name'];
$photo->save();

Our task is reduced to create an object of the Photo class and set the proper object field values. As we know, the creation date is set automatically by the model. Finally, we call the save() method. Doctrine remembers if the row exists in the database and what fields have been modified and uses this data to generate a proper SQL query: INSERT or UPDATE. We can retrieve an object for an existing row, using the following piece of code:

Doctrine::getTable('Photo')->find($id);

9. Image preview

The photo gallery must also provide a photo preview together with the user comments and we are going to implement it right now. Save the following PHP code to /actions/preview.php:

function action()
{
    // Validate the data
    if(!isset($_GET['id']) || !ctype_digit($_GET['id']))
    {
        $view = new Opt_View('message.tpl');
        $view->title = 'Message';
        $view->message = 'Invalid action call!';
        $view->redirect = 'index.php';
        return $view;
    }
 
    // Try to load the image
    $photo = Doctrine::getTable('Photo')->find($_GET['id']);
    if(empty($photo))
    {
        $view = new Opt_View('message.tpl');
        $view->title = 'Message';
        $view->message = 'The requested image has not been found.';
        $view->redirect = 'index.php';
        return $view;
    }
 
    // Prepare the view
    $view = new Opt_View('preview.tpl');
    $view->title = 'Image preview "'.$photo->title.'"';
    $view->photo = $photo;
    $view->setFormat('photo', 'Objective');
 
    // Add the comments
    $view->comments = Doctrine_Query::create()
        ->select('id, author, date, content')
        ->from('Comment')
        ->where('photo_id = ?', $_GET['id'])
        ->orderBy('id')
        ->execute(array(), Doctrine::HYDRATE_ARRAY);
 
    return $view;
} // end action();

At the beginning, our action checks if the URL contains the image ID we want to display. Then it attempts to retrieve the image row, producing an error message in case of failure. Once we have all the data, we are ready to prepare a view for OPT. We already know the structure $container.item on the template side. So far, we have used them only for arrays, but now you will notice that it can be also used for objects. The $photo template variable is initialized with an object. Even if the templates actually allow to access the objects directly, it is not the best idea from the portability and flexibility point of view. Do we really have to know that a particular variable is an object while writing a template? Usually, we would only like to get and display some data. Let's assume that a couple of weeks later we want to change the objects to arrays due the performance reasons. With the direct object or array access, or templates would have to be rewritten, too. This is why we should use generic containers instead and select the data format with setFormat() method. The advantages:

  1. Templates are separated from the application logic and implementation details.

  2. The refactoring process may require to replace the existing PHP data structures with something else. OPT data formats free us from rewriting the templates.

  3. Data formats are something more than just data types. They are also used to provide concrete algorithms and implementations for sections and other template constructs. It allows us to reimplement them for example, to retrieve the data directly from various system structures. OPT itself provides two different algorithms for nested sections, implemented with data formats.

  4. No matter what data format we want to use, the template code remains the same.

  5. The template code can be easily copied between two parts of the application or event between the projects. In the new place, we configure the data formats once again and the code works without any modifications.

Let's take a look at the template (/templates/preview.tpl):

<?xml version="1.0"?>
<opt:extend file="layout.tpl">
<opt:snippet name="content">
    <p><img parse:src="'photos/'~$photo.filename" /></p>
    <p>Added: {$photo.date_text}</p>
    <h3>Comments</h3>
    <opt:show name="comments">
        <div class="comment" opt:section="comments">
            <p class="author">Written by {$comments.author} on {$comments.date_text}</p>
            <p>{$comments.content}</p>
        </div>
    <opt:showelse><p>No comments added.</p></opt:showelse>
    </opt:show>
 
    <p><a parse:href="'index.php?action=comment&id='~$photo.id">Add a comment</a></p>
    <p><a href="'index.php">Back</a></p>
</opt:snippet>
</opt:extend>

This template also shows another type of section – opt:section, provided as a tag attribute. The attribute forms of the instructions are just a syntactic sugar and simplify the code. The instruction itself is used to display linear lists with the elements shown one after another and it is the most commonly used section type. The other aspects of its functionality are the same, as in opt:grid. The section data for comments are retrieved directly from Doctrine. The model automatically formats the date for us and we can access it with {$comments.date_text}.

10. Conclusion

Our photo gallery still needs a comment form, but its structure is very similar to the image upload, so we will not provide it here. Let's see, what we have learned during the project implementation:

  1. We can configure Doctrine and OPT to work.
  2. We can design a database structure in YAML files.
  3. We can create and extend the Doctrine models, using event listeners and automatic accessor overriding.
  4. We have been introduced to the basics of DQL and retrieving the rows with Doctrine.
  5. We know, how to use the model objects to insert new rows to the database.
  6. We have learned, how to work with Doctrine command line interface.
  7. We can work with OPT views and output systems.
  8. We have learned, how to work with XML templates.
  9. We have been introduced to the template inheritance.
  10. We can use sections to display various types of lists.
  11. We can construct dynamic HTML forms with components.
  12. We can use the data formats to improve the portability of our templates and simplify our lifes.

The list is quite amazing, but remember that this article is just the beginning. It is impossible to introduce and explain all the features of such great libraries in just one text. Anyway, I hope that I managed to show the most important issues and encourage you to further experiments with them. I wish you many successful projects with Doctrine and Open Power Template.

Download the photo gallery source code: http://static.invenzzia.org/files/photogallery-en-0.4.tar.gz

Open Power Template 2.0 projects tutorial Doctrine

This text is licensed under Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States license.

Questions and feedback

This is the end of this article. Please, leave a comment.