Invenzzia » Resources / Articles / A photo gallery tutorial / Photo upload form

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);