Listing tasks

In order to list the tasks, we need to retrieve them from the model layer and pass them to the view. To do this, we fill in indexAction() within TaskController. Update the indexAction() like this:

module/Checklist/src/Checklist/Controller/TaskController.php:

1
2
3
4
5
 public function indexAction()
 {
     $mapper = $this->getTaskMapper();
     return new ViewModel(array('tasks' => $mapper->fetchAll()));
 }

You’ll also need to add use Zend\View\Model\ViewModel; to list of use statements at the top of the file.

To provide variables to the view layer, we return a ViewModel instance where the first parameter of the constructor is an array from the action containing data we need. These are then automatically passed to the view script. The ViewModel object also allows us to change the view script that is used, but the default is to use {controller name}/{action name}. You can also return an array from a controller as Zend Framework will construct a ViewModel behind the scenes for you.

We can now fill in the task/index.phtml view script. Replace the contents with this new code:

module/Checklist/view/checklist/task/index.phtml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 <?php
 $title = 'My task list';
 $this->headTitle($title);
 ?>
 <h1><?php echo $this->escapeHtml($title); ?></h1>
 <p><a href="<?php echo $this->url('task', array(
         'action'=>'add'));?>">Add new item</a></p>


 <table class="table">
 <tr>
     <th>Task</th>
     <th>Created</th>
     <th>Completed?</th>
     <th>&nbsp;</th>
 </tr>
 <?php foreach ($tasks as $task): ?>
 <tr>
     <td>
         <a href="<?php echo $this->url('task',
             array('action'=>'edit', 'id' => $task->getId()));?>">
             <?php echo $this->escapeHtml($task->getTitle()); ?></a>
     </td>
     <td><?php echo $this->escapeHtml($task->getCreated()); ?></td>
     <td><?php echo $task->getCompleted() ? 'Yes' : 'No'; ?></td>
     <td>
         <a href="<?php echo $this->url('task',
             array('action'=>'delete', 'id' => $task->getId()));?>">Delete</a>
     </td>
 </tr>
 <?php endforeach; ?>
 </table>

The first thing we do is to set the title for the page (used in the layout) and also set the title for the <head> section using the headTitle() view helper which will display in the browser’s title bar. We then create a link to add a new item using the url() view helper.

The url() view helper is provided by Zend Framework and is used to create the links we need. The first parameter to url() is the route name that we wish to use for construction of the URL and then the second parameter is an array of all the variables to fit into the place-holders to use. In this case we use our task route which is set up to accept two place-holder variables: action and id.

We iterate over the $tasks that we assigned from the controller action within an HTML table. The Zend Framework view system automatically ensures that these variables are extracted into the scope of the view script. Alternatively, you can also prefix with $this-> if you would like.

For each row, we display each task’s title, creation date, completion date and provide links to allow for editing and deleting the record. A standard foreach: loop is used to iterate over the list of tasks, and we use the alternate form using a colon and endforeach; as it is easier to scan than to try and match up braces. Again, the url() view helper is used to create the edit and delete links.

Note that we always use the escapeHtml() view helper to help protect ourselves from XSS vulnerabilities.

If you now run the application from within Zend Studio and navigate to http://localhost:10088/MyTaskList/public/task you should see this:

../_images/getting-started-with-zend-studio.studio9.png

Redirect the home page

When you first pressed the Run button, you saw the application’s home page which is the skeleton’s welcome page. It would be helpful if we could redirect immediately to /tasks to save us having to edit the URL each time.

To do this, go to Navigate -> Open Type… in Zend Studio and type IndexController in the search box of the Open PHP Type dialog and press return. This will open module/Application/src/Application/Controller/IndexController.php for you. Change the indexAction() method so that it reads:

module/Application/src/Application/Controller/IndexController.php:

1
2
3
4
 public function indexAction()
 {
     return $this->redirect()->toRoute('task');
 }

We use the redirect controller plugin to redirect the request for the home page to the URL defined by the route name task which we set up earlier. Now, when you press the green “Run” button, you will be taken directly to the list of tasks.

Styling

We’ve picked up the skeleton application’s layout which is fine for this tutorial, but we need to change the title and remove the copyright message.

The Zend Skeleton Application is set up to use Zend\I18n’s translation functionality for all the text. This allows you to translate all the text strings in the application into a different language if you need to.

The translation data is stored in separate files in the gettext format which have the extension .po and are stored in the application/language folder. The title of the application is “Skeleton Application” and to change this, you need to use the poedit application (http://www.poedit.net/download.php/). Start poedit and open application/language/en_US.po. Click on “Skeleton Application” in the list of original strings and then type in “My Task List” as the translation.

../_images/getting-started-with-zend-studio.studio10.png

Press Save in the toolbar and poedit will create an updated en_US.mo file.

Alternatively, the gted Eclipse plugin allows for editing PO files directly in Zend Studio or PDT. To install gted, select the Help > Install New Software menu, and press the “Add…” button. Enter the gted for the Name, http://gted.sourceforge.net/update as the Location and then press the “OK” button. You will see the gted name appear in the list. Click on the checkbox next to gted and work through the install wizard by pressing “Next button as required. At the end of the installation you will be able to create or edit the PO files using the gted plugin:

../_images/getting-started-with-zend-studio.studio12.png

It follows that as Zend Studio and PDT are based on Eclipse you can install any other Eclipse plugins that are listed on http://marketplace.eclipse.org/ using the same process.

The next thing to do is to remove the copyright message, we need to edit the Application module’s layout.phtml view script:

module/Application/view/layout/layout.phtml:

Remove this line:

1
 <p>&copy; 2005 - <?php echo date('Y') ?> by Zend Technologies Ltd. <?php echo $this->translate('All rights reserved.') ?></p>

The page looks a little better now!

Adding new tasks

We can now write the functionality to add new tasks. There are two things we need to do:

  • Display a form for user to provide the task information
  • Process the form submission and store to database

We use Zend\Form to do this. The Zend\Form component manages the form and works in tandem with the Zend\InputFilter component which will provide validation.

Create a new folder in module/Checklist/src/Checklist called Form and then within the Form folder, create a new PHP file called TaskForm.php with these contents:

module/Checklist/src/Checklist/Form/TaskForm.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
 <?php
 namespace Checklist\Form;

 use Zend\Form\Form;
 use Zend\Stdlib\Hydrator\ClassMethods;

 class TaskForm extends Form
 {
     public function __construct($name = null, $options = array())
     {
         parent::__construct('task');

         $this->setAttribute('method', 'post');
         $this->setInputFilter(new TaskFilter());
         $this->setHydrator(new ClassMethods());

         $this->add(array(
             'name' => 'id',
             'type' => 'hidden',
         ));

         $this->add(array(
             'name' => 'title',
             'type' => 'text',
             'options' => array(
                 'label' => 'Title',
             ),
             'attributes' => array(
                 'id' => 'title',
                 'maxlength' => 100,
             )
         ));

         $this->add(array(
             'name' => 'completed',
             'type' => 'checkbox',
             'options' => array(
                 'label' => 'Completed?',
                 'label_attributes' => array('class'=>'checkbox'),
             ),
         ));

         $this->add(array(
             'name' => 'submit',
             'attributes' => array(
                 'type'  => 'submit',
                 'value' => 'Go',
                 'class' => 'btn btn-primary',
             ),
         ));
     }
 }

Within the constructor of TaskForm, we set the name when we call the parent’s constructor and then set the method and the input filter that we want to use. We also set the form’s hydrator to be ClassMethods, as a form object uses hydration to transfer data to and from an entity object in exactly the same way as the Zend\Db components do. Finally, we create the form elements for the id, title, whether the task is complete and the submit button. For each item we set various attributes and options, including the label to be displayed.

We also need to set up validation for this form. In Zend Framework is this done using an input filter which can either be standalone or within any class that implements InputFilterAwareInterface, such as a model entity. For this application we are going to create a separate class for our input filter.

Create a new PHP file called TaskFilter.php in the module/Checklist/src/Checklist/Form folder with these contents:

module/Checklist/src/Checklist/Form/TaskFilter.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 <?php
 namespace Checklist\Form;

 use Zend\InputFilter\InputFilter;

 class TaskFilter extends InputFilter
 {
     public function __construct()
     {
         $this->add(array(
             'name' => 'id',
             'required' => true,
             'filters' => array(
                 array('name' => 'Int'),
             ),
         ));

         $this->add(array(
             'name' => 'title',
             'required' => true,
             'filters' => array(
                 array('name' => 'StripTags'),
                 array('name' => 'StringTrim'),
             ),
             'validators' => array(
                 array(
                     'name' => 'StringLength',
                     'options' => array(
                         'encoding' => 'UTF-8',
                         'max' => 100
                     ),
                 ),
             ),
         ));

         $this->add(array(
             'name' => 'completed',
             'required' => false,
         ));
     }
 }

In the constructor for the TaskFilter, we create inputs for each property that we want to filter. Each input can have a name, a required property a list of filters and a list of validators. All are optional other than the name property. The difference between filters and validators is that a filter changes the data passed through it and a validator tests if the data matches some specific criteria. For the title, we filter the string with StripTags and StringTrim and finally ensure that the string is no longer than 100 characters with the StringLength validator. For the completed element, we simply set required to false.

We now need to display the form and process it on submission. This is done within the TaskController’s addAction(). Open TaskController.php (Navigate -> Open Resource… is a convenient way to do this) and add a new method called addAction() to the class that looks like this:

module/Checklist/src/Checklist/Controller/TaskController.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 public function addAction()
 {
     $form = new TaskForm();
     $task = new TaskEntity();
     $form->bind($task);

     $request = $this->getRequest();
     if ($request->isPost()) {
         $form->setData($request->getPost());
         if ($form->isValid()) {
             $this->getTaskMapper()->saveTask($task);

             // Redirect to list of tasks
             return $this->redirect()->toRoute('task');
         }
     }

     return array('form' => $form);
 }

Add use Checklist\Model\TaskEntity; and use Checklist\Form\TaskForm; to the list of use statements at the top of the file.

Let’s look at what the addAction() does in detail.

1
2
3
 $form = new TaskForm();
 $task = new TaskEntity();
 $form->bind($task);

We instantiate a new TaskForm object and an empty TaskEntity which we bind to the form for use by the form later. The form’s bind() method attaches the model to the form. This is used in two ways:

  1. When displaying the form, the initial values for each element are extracted from the model.
  2. After successful validation in isValid(), the data from the form is put back into the model.

When adding a new task, we only need to worry about point 2, however for editing an item, we need data transfer in both directions.

1
2
3
4
 $request = $this->getRequest();
 if ($request->isPost()) {
     $form->setData($request->getPost());
     if ($form->isValid()) {

For a submitted form, we set the posted data to the form and check to see if it is valid using the isValid() member function of the form. The isValid() method uses the form’s input filter to test for validity and if it returns true, it will then transfer the filtered data values to the entity object that is bound to the form using the registered hydrator. This means that after isValid() is called, $task now contains the submitted form data.

1
 $this->getTaskMapper()->saveTask($task);

As the form is valid, we can save $task to the database using the mapper’s saveTask() method.

1
2
 // Redirect to list of tasks
 return $this->redirect()->toRoute('task');

After we have saved the new task, we redirect back to the list of tasks using the Redirect controller plugin.

1
 return array('form' => $form);

Finally, if this request is not a POST, we return the variables that we want assigned to the view. In this case, just the form object.

We also need to add the saveTask() method to the TaskMapper class. Open module/Checklist/src/Checklist/Model/TaskMapper.php and add this method to the end of the class:

module/Checklist/src/Checklist/Model/TaskMapper.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 public function saveTask(TaskEntity $task)
 {
     $hydrator = new ClassMethods();
     $data = $hydrator->extract($task);

     if ($task->getId()) {
         // update action
         $action = $this->sql->update();
         $action->set($data);
         $action->where(array('id' => $task->getId()));
     } else {
         // insert action
         $action = $this->sql->insert();
         unset($data['id']);
         $action->values($data);
     }
     $statement = $this->sql->prepareStatementForSqlObject($action);
     $result = $statement->execute();

     if (!$task->getId()) {
         $task->setId($result->getGeneratedValue());
     }
     return $result;

 }

The saveTask() method handles both inserting a new record if $task doesn’t have an id or updating it if it does. In either case, we need the data from the entity as an array, so we can use the hydrator to do this. If we are updating, then we use the Sql object’s update() method to create an Update object where we can set the data and a where clause. For inserting, we need an Insert object to which we set the values. Obviously, when inserting, the database will auto-increment the id, so we do not need the id property in the values list. In either case, we create a statement object and then execute it. Finally, if we are inserting, we populate the task entity’s id with the value of the auto-generated id.

We now need to render the form in the add.phtml view script. Create a new PHP file called add.phtml in the module/Checklist/view/checklist/task folder and add this code:

module/Checklist/view/checklist/task/add.phtml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
 $title = 'Add new task';
 $this->headTitle($title);
 ?>
 <h1><?php echo $this->escapeHtml($title); ?></h1>

 <?php
 $form = $this->form;
 $form->setAttribute('action', $this->url('task', array('action' => 'add')));
 $form->get('submit')->setAttribute('value', 'Add');
 $form->prepare();

 echo $this->form()->openTag($form);
 echo $this->formHidden($form->get('id'));
 echo $this->formRow($form->get('title'));
 ?>
 <div>
 <?php echo $this->formInput($form->get('submit')); ?>
 </div>
 <?php
 echo $this->form()->closeTag($form);

Again, we display a title as before and then we render the form. Zend Framework provides some view helpers to make this a little easier. The form() view helper has an openTag() and closeTag() method which we use to open and close the form. Then for the title element, which has a label, we can use formRow() view helper which will render the HTML for the label, the element and any validator messages that may exist. For the id and submit elements, we use formHidden() and formInput() respectively as we only need to render the element itself. We also want the submit button on its own line, so we put it within a div. Note that the formRow view helper is just a convenience - we could have used formInput(), formLabel() and formElementErrors() separately had we wanted to.

If you now run the application from within Zend Studio and click the “Add new item” link from the task list page, you should see:

../_images/getting-started-with-zend-studio.studio11.png

You can now add a new task item and see it in the list of tasks.

Editing a task

Editing a task is almost identical to adding one, so the code is very similar. This time we use editAction() in the TaskController. Open TaskController.php and add this method to it:

module/Checklist/src/Checklist/Controller/TaskController.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 public function editAction()
 {
     $id = (int)$this->params('id');
     if (!$id) {
         return $this->redirect()->toRoute('task', array('action'=>'add'));
     }
     $task = $this->getTaskMapper()->getTask($id);

     $form = new TaskForm();
     $form->bind($task);

     $request = $this->getRequest();
     if ($request->isPost()) {
         $form->setData($request->getPost());
         if ($form->isValid()) {
             $this->getTaskMapper()->saveTask($task);

             return $this->redirect()->toRoute('task');
         }
     }

     return array(
         'id' => $id,
         'form' => $form,
     );
 }

This code should look familiar. Let’s look at the only difference from adding a task: We look for the id that is in the matched route and use it to load the task to be edited:

1
2
3
4
5
 $id = (int)$this->params('id');
 if (!$id) {
     return $this->redirect()->toRoute('task', array('action'=>'add'));
 }
 $task = $this->getTaskMapper()->getTask($id);

The params() method is a controller plugin that provides a convenient way to retrieve parameters from the matched route. We use it to retrieve the id parameter that we defined in the task route that we created in the module.config.php. If the id is zero, then we redirect to the add action, otherwise, we continue by getting the task entity from the database.

As we use the form’s bind() method with its hydrator, we do not need to populate the $task’s data into the form manually as it will automatically be transferred for us.

We also need to write a getTask() method in the TaskMapper to get a single record from the database, so let’s do that now. Open TaskMapper.php and add this method:

module/Checklist/src/Checklist/Model/TaskMapper.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 public function getTask($id)
 {
     $select = $this->sql->select();
     $select->where(array('id' => $id));

     $statement = $this->sql->prepareStatementForSqlObject($select);
     $result = $statement->execute()->current();
     if (!$result) {
         return null;
     }

     $hydrator = new ClassMethods();
     $task = new TaskEntity();
     $hydrator->hydrate($result, $task);

     return $task;
 }

This method simply sets a where clause on the Sql’s Select object and then executes it. Calling current() on the result from execute() will return either the array of data for the row or false. If we retrieved data, then we use the hydrator to populate a new TaskEntity ($task) with $data.

In the same way as with the action methods, the view template, edit.phtml, looks very similar to the one for adding an task. Create a new PHP file called edit.phtml in in the module/Checklist/view/checklist/task folder and add this code:

module/Checklist/view/checklist/task/edit.phtml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 <?php
 $title = 'Edit task';
 $this->headTitle($title);
 ?>
 <h1><?php echo $this->escapeHtml($title); ?></h1>

 <?php
 $form = $this->form;
 $url = $this->url('task', array('action' => 'edit', 'id' => $id));
 $form->setAttribute('action', $url);
 $form->get('submit')->setAttribute('value', 'Edit');
 $form->prepare();

 echo $this->form()->openTag($form);
 echo $this->formHidden($form->get('id'));
 echo $this->formRow($form->get('title'));
 echo $this->formRow($form->get('completed'));
 ?>
 <div>
 <?php echo $this->formInput($form->get('submit')); ?>
 </div>
 <?php
 echo $this->form()->closeTag($form);

Compared to the add view script, we set the title to ‚’Edit Task’, and update the action URL to the edit action with the correct id. We also change the label of the button to ‚’edit’ and render the completed form element.

You should now be able to edit tasks.

Deleting a task

To round out the core functionality of our application, we need to be able to delete a task. We have a Delete link next to each task on our list page and the na√Øve approach would be to run the delete action when it’s clicked. This would be wrong. Remembering the HTTP specification, we recall that you shouldn’t do an irreversible action using GET and should use POST instead.

We shall therefore show a confirmation form when the user clicks delete and if they then click “Yes”, we will do the deletion. As the form is trivial, we’ll code it directly into our view (Zend\Form is, after all, optional!).

Let’s start by adding the deleteAction() method to the TaskController. Open TaskController.php and add this method to it:

module/Checklist/src/Checklist/Controller/TaskController.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public function deleteAction()
 {
     $id = $this->params('id');
     $task = $this->getTaskMapper()->getTask($id);
     if (!$task) {
         return $this->redirect()->toRoute('task');
     }

     $request = $this->getRequest();
     if ($request->isPost()) {
         if ($request->getPost()->get('del') == 'Yes') {
             $this->getTaskMapper()->deleteTask($id);
         }

         return $this->redirect()->toRoute('task');
     }

     return array(
         'id' => $id,
         'task' => $task
     );
 }

As before, we get the id from the matched route and retrieve the task object. We then check the Request object’s isPost() to determine whether to show the confirmation page or to delete the task. We use the TaskMapper’s deleteTask() method to delete the row and then redirect back to the list of tasks. If the request is not a POST, then we assign the task to the view, along with the id.

We also need to write deleteTask(), so open TaskMapper.php and add this method:

module/Checklist/src/Checklist/Model/TaskMapper.php:

1
2
3
4
5
6
7
8
 public function deleteTask($id)
 {
     $delete = $this->sql->delete();
     $delete->where(array('id' => $id));

     $statement = $this->sql->prepareStatementForSqlObject($delete);
     return $statement->execute();
 }

This code should look fairly familiar as we again use a Delete object from Zend\Db\Sql and execute the statement from it. As we are using a Delete object, we set the where clause to avoid deleting every row in the table.

The view script is a simple HTML form. Create a new PHP file, delete.phtml in the module/Checklist/view/checklist/task folder with this content:

module/Checklist/view/checklist/task/delete.phtml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <?php
 $title = 'Delete task';
 $this->headTitle($title);
 ?>
 <h1><?php echo $this->escapeHtml($title); ?></h1>

 <p>Are you sure that you want to delete the
   '<?php echo $this->escapeHtml($task->getTitle()); ?>' task?
 </p>
 <?php
 $url = $this->url('task', array('action' => 'delete', 'id'=>$id)); ?>
 <form action="<?php echo $url; ?>" method="post">
 <div>
   <input type="submit" name="del" value="Yes" />
   <input type="submit" name="del" value="No" />
 </div>
 </form>

In this view script, we display a confirmation message and then a form with just Yes and No buttons. In the action, we checked specifically for the “Yes” value when doing the deletion.

That’s it - you now have a fully working application!