Sections and data formats in OPT

  • Published on 22 December 2009 09:00:00 GMT, updated over 4 years ago by Tomasz Jędrzejewski
  • pl

This tutorial shows, how to use sections and data formats in Open Power Template, two unique and powerful features that make writing templates much simpler and faster.

Table of Contents

One of the most useful features in template engine Open Power Templare are sections. They are used to display lists, but contrary to ordinary loops known from other template engines and programming languages, it provides lots of unique options that make your life simpler. In this article we are going to show, how to use sections in your projects and what are data formats, another powerful feature which however may cause some problems at the beginning.

1. More about sections

As we mentioned in the introduction, sections are used to display lists. In ordinary programming languages the programmer may choose one of the classical loops, such as for or foreach. However, they were designed to be universal and in templates, their simplicity is the source of troubles due to a lot of technical stuff we have to deal with. Suppose we have an array with the content we would like to display, but in reversed order. There are many ways to do this:

$cnt = sizeof($array);
for($i = $cnt1; $i >= 0; $i--)
{
   echo display_element($array[$i]);
}

Or we can use array_reverse() function:

foreach(array_reverse($array) as $i => $data)
{
   echo display_element($data);
}

It is not a problem, if this is just one list. But in templates, there are dozens of lists; actually it is one of the most commonly used features we would like to have. Looking at the source code of an algorithm written in PHP or C it is hard to say, what it does and how without a detailed analysis. Templates are the same. Using many loops overloaded with implementation details lead to a messy code which is hard to read and modify. We could copy and paste many code snippets from one place to another, but this means we need to rewrite the loops, because too many things are left to the programmer.

Obviously, we need something new to display lists and OPT gives us such a tool: sections. They attempt to hide as many unnecessary details, as possible, making writing templates faster and more efficient (plus, OPT applies more precise optimizations that humans do).

Let's take a look, how a simple section in a template looks like. We can use it to display a list of news in our administration panel.

<table class="list">
 <thead>
  <tr>
   <th>#</th>
   <th>Title</th>
   <th>Date</th>
   <th>Options</th>
  </tr>
 </thead>
 <tbody>
  <opt:section name="items">
   <tr>
     <td>{$items.id}</td>
     <td>{$items.title}</td>
     <td>{$items.date_formatted}</td>
    <td><a parse:href="'/news/edit/'~$items.id">Edit</a></td>
   </tr>
  </opt:section>
 </tbody>
</table>

That's it. As we can see, section declaration is very simplified. All we have to do is to give it a name. The content of opt:section tags describes the layout of a single list element. The element variables are available through the special construct $sectionName.elementVariable. In templates, we do not consider, where the section takes the data from, what they are and how to cooperate with other sections. OPT encourages us to leave such details to the script and do not bother with them on the template side. We can notice that such section can be copied without any problems to other places thanks to the generality. Displaying the data in the reversed order is also possible:

  <opt:section name="items" order="desc">
   <tr>
     <td>{$items.id}</td>
     <td>{$items.title}</td>
     <td>{$items.date_formatted}</td>
    <td><a parse:href="'/news/edit/'~$items.id">Edit</a></td>
   </tr>
  </opt:section>

To hide the table, if the list does not have any elements, we can use opt:show:

<opt:show name="items">
<table class="list">
 <thead>
  <tr>
   <th>#</th>
   <th>Title</th>
   <th>Date</th>
   <th>Options</th>
  </tr>
 </thead>
 <tbody>
  <opt:section>
   <tr>
     <td>{$items.id}</td>
     <td>{$items.title}</td>
     <td>{$items.date_formatted}</td>
    <td><a parse:href="'/news/edit/'~$items.id">Edit</a></td>
   </tr>
  </opt:section>
 </tbody>
</table>
<opt:showelse>
 <p>Sorry, no elements.</p>
</opt:showelse>
</opt:show>

We pack the whole table to opt:show. The new tag also takes over the attributes from opt:section, which marks just a place that will be iterated now.

Once more we emphasise: moving the attributes from opt:section to opt:show is very important. Without it, OPT would make a separate section from it, instead of linking it with opt:show.

2. More about data formats

Our play with XML tags does not contain contain anything that would help OPT to determine, where the sections should get the data from and what they exactly are. Now we are going to introduce a concept of data formats which will allow us to do that. The idea of data formats is quite similar to data types in programming languages, except that they can be applied to any element of a template. They decide, what a particular element should be after translating it to the PHP code during the compilation. In OPT 2.0, data formats work with variables and sections, but the future versions are going to implement them in much more template instructions. In our case, the data formats decide on the nature of the sections and their data.

The default data format is called Array and it is automatically set to all sections and variables. It treats the list as an array with indexes enumerated from 0 to n-1 (n is the number of elements in the array). Each list element can be either a scalar value or an array of variables. Moreover, it tells the section to look for the array in a template variable named the same, as the section. Our PHP code that assings the data to the section would look like this:

$view = new Opt_View('list.tpl');
$view->items = array(0 =>
 array('id' => 1, 'title' => 'News 1', 'date_formatted' => '15.10.2009'),
 array('id' => 2, 'title' => 'News 2', 'date_formatted' => '16.10.2009'),
 array('id' => 3, 'title' => 'News 3', 'date_formatted' => '18.10.2009'),
 array('id' => 4, 'title' => 'News 4', 'date_formatted' => '21.10.2009'),
);

Such a list can be easily created from the database results. Below, we show and example for PDO library:

$stmt = $pdo->query('SELECT * FROM news ORDER BY `date` DESC');
$items = array();
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
  $row['date_formatted'] = date($config->dateFormat, $row['date']);
  $items[] = $row;
}
$stmt->closeCursor();
$view->items = $items;

Probably the arrays will be the most commonly used data format, but it is worth to remember that at any time we can make a section to iterate through an object:

$view = new Opt_View('list.tpl');
$list = new SplDoublyLinkedList();
$list->push(new News(1));
$list->push(new News(2));
$list->push(new News(3));
$view->items = $list;
$view->setFormat('items', 'Objective');

This time our list is an ojbect of SplDoublyLinkedList class available since PHP 5.3.0 in the Standard PHP Library. We have also a new method call: setFormat(). It sets the data format to the specified element. The Objective format causes that OPT treats both the list and its elements as objects.

The data formats cannot be changed dynamically. Every change requires the template to be recompiled manually. You need to find the corresponding PHP file in the compileDir directory and remove it.

It is the right time to more advanced examples. Section data formats can control, where to take the data from. By default, OPT looks for a template variable named exactly the same, as the section, but it is not a rule. Suppose we are writing a CMS with a skin system. The script generates various lists with the data for the templates (newest comments, recommended links, banners etc.), but the skin authors do not have to use them all. We would like to optimize the script not to waste the time to retrieve the unused data. Here, the StaticGenerator format will help us a lot. Instead of the data, the section retrieves an object that knows, how to generate the data. If we are using the MVC pattern, all we have to do is to make our models implementing a proper interface.

class News_Model implements Opt_Generator_Interface
{
    /**
     * Generate the data for OPT.
     *
     * @param string $what Section name
     * @return array
     */
    public function generate($what)
    {
       $db = Registry::get('db');
       $stmt = $db->query('SELECT * FROM `news` ORDER BY `date` DESC');
       $list = array();
       while($row = $stmt->fetch(PDO::FETCH_ASSOC))
       {
            $row['date_formatted'] = date($config->dateFormat, $row['date']);
            $list[] = $row;
       }
       $stmt->closeCursor();
       return $list;
    } // end generate();
} // end News_Model;
 
$view = new Opt_View('list.tpl');
$view->items = new News_Model;
$view->setFormat('items', 'StaticGenerator/Array');

The first time the template accesses the section, OPT executes the generate() method, retrieving the data from the database and returing them to the section. If the skin does not use a particular section, the query is not executed and we safe some time and improve the system resource usage. Note how the data format is set. After the StaticGenerator name, we put a slash and the name of another data format name. The OPT data formats implement the decorator design pattern which means that we can create complex data format compositions. StaticGenerator does not work alone (you can try - it will report an exception), because it only decides, where to get the data from, but the section still needs information, what they are (arrays, objects, etc.).

Decoration can be used in other situations, too. Suppose our list is an array, but each element is an object representing a single database row. It is a common situation if we are working with an ORM library. Then we set the format Array/Objective. OPT now iterates through the array, but accesses the element variables as object fields. If we reverse the order, the list becomes an object, and the elements - arrays. As an exercise, we encourage to think what the StaticGenerator/Objective/Array means for OPT. The answer can be found at the end of the article.

It is time to sum up everything that we have learnt so far. Sections are used to display list, but contrary to ordinary loops, they hide as many technical and implementation details as possible from the template and template designers. They can focus on their task without worrying about them and the script structure. Of course, OPT is not an oracle and does not throw them away. The technical decisions are left to the script which selects the data formats. They decide, what the sections are and how they actually work. Think, what are the pros of moving these decisions from a template to scripts?

3. Nesting sections

We know how the sections work and how data formats modify their behaviour, but single sections were quite simple. In real world applications, we often encounter nested lists, such as:

  1. the blog posts and their tags,
  2. two-level menu,
  3. the list of categories and their forums on a discussion board.

Modelling the nested sections seems to be a bit more complicated at the first time, but once you understand a rule behind it, you will see that this is very easy, too. Let's take a look at the first situation. To display a list of blog posts and their tags, we just nest two sections one within another:

<opt:section name="entries">
<div class="entry">
  <h1>{$entries.title}</h1>
  <p class="tags">
   <opt:section name="tags" str:separator=", ">
    <span><a parse:href="'/tag/'~$tags.slug">{$tags.name}</a></span>
   </opt:section>
  </p>
  <p>{u:$entries.body}</p>
</div>
</opt:section>

In OPT, we do not have to remember about connecting the sections manually. The nesting is enough for the compiler to link them with a one-to-many relationship: it must list only those tags that belong to the currently displayed post. The extra attribute str:separator is another thing that makes our life easier. It puts a comma between every two tags and moreover it is smart enough not to put it before the first or after the last element.

Because the template still does not contain any technical details, how to connect the sections, it is very easy to notice that this process is also controlled by data formats. We will show now, how to prepare a data for the nested sections in the default Array format:

$view = new Opt_View('blog_index.tpl');
$view->entries = array(0 =>
  array('title' => 'Post 1', 'body' => 'Some content'),
  array('title' => 'Post 2', 'body' => 'Some content'),
  array('title' => 'Post 3', 'body' => 'Some content'),
);
$view->tags = array(0 =>
  // tags for post 1
  array(0 =>
   array('slug' => 'sport', 'title' => 'Sport'),
   array('slug' => 'news', 'title' => 'News'),
  ),
  // tags for post 2
  array(0 =>
   array('slug' => 'programming', 'title' => 'Programming'),
   array('slug' => 'php', 'title' => 'PHP'),
  ),
  // tags for post 3
  array(0 =>
   array('slug' => 'computers', 'title' => 'Computers'),
   array('slug' => 'linux', 'title' => 'Linux'),
   array('slug' => 'opensource', 'title' => 'Open source'),
  ),
);

We do not pack the data together with the posts. The tags must have their own array, but with more dimensions. If $view->entries[1] represents a second post, then $view->tags[1][0] will represent the first tag of the second post. If we had three sections, the elements of the deepest sections would have the following address: $view->deepest[1][2][3]: the fourth deepest element of the third middle element of the second top-level element.

If we are using the MVC pattern, returing the data in this format may be very problematic. This is why OPT provides another data format, SingleArray, where the data for the nested sections can be packed to one, huge array. Except this particular difference, the new format works in exactly the same way, as Array. Here, our script looks like this:

$view = new Opt_View('blog_index.tpl');
$view->entries = array(0 =>
  array(
   'title' => 'Post 1',
   'body' => 'Some content',
   'tags' => array(0 =>
     array('slug' => 'sport', 'title' => 'Sport'),
     array('slug' => 'news', 'title' => 'News'),
   )
  ),
  array(
   'title' => 'Post 2',
   'body' => 'Some content',
   'tags' => array(0 =>
     array('slug' => 'programming', 'title' => 'Programming'),
     array('slug' => 'php', 'title' => 'PHP'),
   )
  ),
  array(
   'title' => 'Post 3',
   'body' => 'Some content',
   'tags' => array(0 =>
     array('slug' => 'computers', 'title' => 'Computers'),
     array('slug' => 'linux', 'title' => 'Linux'),
     array('slug' => 'opensource', 'title' => 'Open source'),
   )
  ),
);
$view->setFormat('tags', 'SingleArray');

It is a good illustration of the information we have mentioned in the previous chapter: the section data do not have to come from a template variable. We do not have a $tags variable here. Instead, OPT looks for the tag data in the currently displayed element variables. It is worth noting that SingleArray does not have to be set for the top-level section, as it does not modify the behaviour of the top-level sections.

At the end, we have another exercise for you to help you better understand the topic. We would like the top-level post list to be an object, the posts - arrays, and the tags to be returned by a generator. How to set the data formats?

4. More nesting

OPT does not limit the number of nested sections, but in case of deeper nesting we need a way to control it more precisely. Sometimes we do not want the section to be automatically connected with the parent. The template language provides us the parent attribute which allows us to control the relations between sections.

<ol>
<opt:section name="top">
  <li>Top: {$top.text}<ol>
  <opt:section name="middle">
    <li>Middle: {$middle.text}<ol>
    <opt:section="bottom" parent="top">
      <li>Bottom: {$bottom.text}</li>
    </opt:section>
    </ol></li>
  </opt:section>
  </ol></li>
</opt:section>
</ol>

Our deepest section was intentionally linked with the top-level section, avoiding the "natural" parent, middle. It means that the data must look like this:

$view = new Opt_View('sections.tpl');
$view->top = array(0 =>
  array(
   'text' => 'Top 1'
   'middle' => array(0 =>
     array('text' => 'Middle 1'),
     array('text' => 'Middle 2'),
   ),
   // elements of "bottom" belong to "top", not "middle".
   'bottom' => array(0 =>
     array('text' => 'Bottom 1'),
     array('text' => 'Bottom 2'),
   ),
  ),
  array(
   'text' => 'Top 2'
   'middle' => array(0 =>
     array('text' => 'Middle 1a'),
     array('text' => 'Middle 2a'),
   ),
   'bottom' => array(0 =>
     array('text' => 'Bottom 1a'),
     array('text' => 'Bottom 2a'),
   ),
  )
);
$view->setFormat('middle', 'SingleArray');
$view->setFormat('bottom', 'SingleArray');

Without the parent attribute, the elements of bottom section would have to be defined for each middle element separately. After rendering, our list should look like this:

  1. Top 1
    1. Middle 1
      1. Bottom 1
      2. Bottom 2
    2. Middle 2
      1. Bottom 1
      2. Bottom 2
  2. Top 2
    1. Middle 1a
      1. Bottom 1a
      2. Bottom 2a
    2. Middle 2a
      1. Bottom 1a
      2. Bottom 2a

The attribute has one more usage, this time shown on a practical example. We want to create a universal template for our control panel that would handle displaying the data from different database tables. Each element is equipped with some options, such as "Edit" or "Remove", but for different tables, we may need different option sets: sometimes the user should not be able to remove a row, otherwise - we want to give him the ability to do something extra. The simplest solution is to pack the option list to a section, but normally it would be automatically connected with the row displaying section and we would be forced to add the same options to each element separately. Of course we do not want the options to be connected with rows and we can achieve that by adding an empty parent attribute to the instruction.

<opt:section name="items">
<tr>
  <td>{$items.id}</td>
  <td>{$items.title}</td>
  <td><opt:section="options" parent="">
    <a parse:href="buildUrl($options.url, $items.id)">{$options.title}</a>
  </opt:section></td>
</tr>
</opt:section>

The code that generates the data for us:

$view = new Opt_View('universal_list.tpl');
$view->items = $model->generateList();
$view->options = array(0 =>
  array('url' => '/panel/edit/', 'title' => 'Edit'),
  array('url' => '/panel/remove/', 'title' => 'Remove'),
  array('url' => '/panel/search/', 'title' => 'Search'),
);

That's it. Now OPT treats both of the sections as they are not nested.

As an exercise we suggest solving the following problem: does the following code work and why or why not?

<opt:show name="top">
  <opt:section name="middle" parent="top">
    Element.
  </opt:section>
 
  <opt:section>
    List element.
  </opt:section>
</opt:show>

5. Other section types

Nesting sections and playing with data format are just the beginning of the real fun. Although the ordinary, linear lists are the most common, they do not cover all the situations and problems. Actually, opt:section is only the one of a couple of instructions called sections. The rest of them are:

  1. opt:selector - a connection of sections and switch statement. We can define several ways to render an element.
  2. opt:grid - displays elements in columns.
  3. opt:tree - renders tree data, lists with unlimited number of nesting levels.

What is more, they can cooperate one with another. For example, if we nest opt:grid within opt:section, the compiler still creates a connection between them, identical as those shown in previous chapters. Let's take a look what sections really can do. Below you can find a template for an image gallery:

<table class="gallery">
  <opt:grid name="images" cols="5">
   <tr>
     <opt:item><td><img parse:src="$images.picture" /></td></opt:item>
     <opt:emptyItem><td> </td></opt:emptyItem>
   </tr>
  </opt:grid>
</table>

Writing the some code in pure PHP would give us a classic example of a write-only code. Fortunately, OPT frees the template designers from dealing with technical details. We simply define, how the element with a photo should look like, and how - the empty element used in the last row to complete the table if the number of photos is not a multiplicity of a column number.

We encourage to explore the other section types.

6. Answers

In the first problem your task was to decide, how OPT understands the data format StaticGenerator/Objective/Array. Starting from the left we have: the list is returned by the generator, it is an object, and the list elements are arrays.

Later, we asked for the pros of moving the implementation details from templates to the script:

  1. The template can be created earlier than the script or developed indepentendly. Moreover, the designer can do it much faster, because he does not have to waste his time for implementation details and spend a lot of time on testing the connection.
  2. The templates can serve as a dummy model of the website for the client or for the programmers. They can be introduced to the suggested website structure, and moreover the same templates can be used in the final version of the code without any changes. All we have to do is to change the data formats and feed them with the real data.
  3. Simplified code refactoring. The data are provided as arrays or objects. Sometimes we need to change it due to the optimization or new feature purposes (or both). In OPT we simply change the data format and recompile the template, while in other template engines we would have to rewrite it.

In the third exercise was to select the data formats for nested sections. We want the list of posts to be object, a single post - an array and the tags taken from a generator:

$view->setFormat('entries', 'Objective/Array');
$view->setFormat('tags', 'StaticGenerator/Array');

The StaticGenerator format looks for the generator object in the parent section element, like in SingleArray. After the slash we must add one more format that describes what the generator returns.

The last problem concerns opt:show and nested sections:

<opt:show name="top">
  <opt:section name="middle" parent="top">
    Element.
  </opt:section>
 
  <opt:section>
    Element listy.
  </opt:section>
</opt:show>

The question is whether this code works... and why not. The actual section consists of the repeated code only. The opt:show tag defines the section neighbourhood only and this is why our middle section cannot be connected to it in this particular place. The reason is simple. What parent element we should be connected with, if we have not started displaying the parent elements yet? To fix this code, we must either remove the parent attribute (the middle section is not nested then) or move the section between the empty opt:section tags.

7. Conclusion

We hope that information in this article was useful and helped you understanding sections and data formats. However, please remember that this is not enough in this topic and we encourage to experiment with new section types and their features which are described in OPT documentation.

Open Power Template 2.0 projects tutorial

Questions and feedback

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