1. Home
  2. Documentation
  3. Tutorials
  4. MVC Tutorials
  5. In-Depth Tutorial
  6. Editing and Deleting Data

In-Depth Tutorial

In This Article

Editing and Deleting Data

In the previous chapter we've come to learn how we can use the zend-form andzend-db components forcreating new data-sets. This chapter will focus onfinalizing the CRUD functionality by introducing the concepts forediting anddeleting data.

Binding Objects to Forms

The one fundamental difference between our "add post" and "edit post" forms isthe existence of data. This means we need to find a way to get data from ourrepository into the form. Luckily, zend-form provides this via adata-binding feature.

In order to use this feature, you will need to retrieve aPost instance, andbind it to the form. To do this, we will need to:

  • Add a dependency in ourWriteController on ourPostRepositoryInterface, from which we will retrieve ourPost.
  • Add a new method to ourWriteController,editAction(), that will retrieve aPost, bind it to the form, and either display the form or process it.
  • Update ourWriteControllerFactory to inject thePostRepositoryInterface.

We'll begin by updating theWriteController:

  • We will import thePostRepositoryInterface.
  • We will add a property for storing thePostRepositoryInterface.
  • We will update the constructor to accept thePostRepositoryInterface.
  • We will add theeditAction() implementation.

The final result will look like the following:

<?php// In module/Blog/src/Controller/WriteController.php:namespace Blog\Controller;use Blog\Form\PostForm;use Blog\Model\Post;use Blog\Model\PostCommandInterface;use Blog\Model\PostRepositoryInterface;use InvalidArgumentException;use Zend\Mvc\Controller\AbstractActionController;use Zend\View\Model\ViewModel;class WriteController extends AbstractActionController{    /**     * @var PostCommandInterface     */    private $command;    /**     * @var PostForm     */    private $form;    /**     * @var PostRepositoryInterface     */    private $repository;    /**     * @param PostCommandInterface $command     * @param PostForm $form     * @param PostRepositoryInterface $repository     */    public function __construct(        PostCommandInterface $command,        PostForm $form,        PostRepositoryInterface $repository    ) {        $this->command = $command;        $this->form = $form;        $this->repository = $repository;    }    public function addAction()    {        $request   = $this->getRequest();        $viewModel = new ViewModel(['form' => $this->form]);        if (! $request->isPost()) {            return $viewModel;        }        $this->form->setData($request->getPost());        if (! $this->form->isValid()) {            return $viewModel;        }        $post = $this->form->getData();        try {            $post = $this->command->insertPost($post);        } catch (\Exception $ex) {            // An exception occurred; we may want to log this later and/or            // report it to the user. For now, we'll just re-throw.            throw $ex;        }        return $this->redirect()->toRoute(            'blog/detail',            ['id' => $post->getId()]        );    }    public function editAction()    {        $id = $this->params()->fromRoute('id');        if (! $id) {            return $this->redirect()->toRoute('blog');        }        try {            $post = $this->repository->findPost($id);        } catch (InvalidArgumentException $ex) {            return $this->redirect()->toRoute('blog');        }        $this->form->bind($post);        $viewModel = new ViewModel(['form' => $this->form]);        $request = $this->getRequest();        if (! $request->isPost()) {            return $viewModel;        }        $this->form->setData($request->getPost());        if (! $this->form->isValid()) {            return $viewModel;        }        $post = $this->command->updatePost($post);        return $this->redirect()->toRoute(            'blog/detail',            ['id' => $post->getId()]        );    }}

The primary differences betweenaddAction() andeditAction() are that thelatter needs to first fetch aPost, and this post isbound to the form. Bybinding it, we ensure that the data is populated in the form for the initialdisplay, and, once validated, the same instance is updated. This means that wecan omit the call togetData() after validating the form.

Now we need to update ourWriteControllerFactory. First, add a new importstatement to it:

// In module/Blog/src/Factory/WriteControllerFactory.php:use Blog\Model\PostRepositoryInterface;

Next, update the body of the factory to read as follows:

// In module/Blog/src/Factory/WriteControllerFactory.php:public function __invoke(ContainerInterface $container, $requestedName, array $options = null){    $formManager = $container->get('FormElementManager');    return new WriteController(        $container->get(PostCommandInterface::class),        $formManager->get(PostForm::class),        $container->get(PostRepositoryInterface::class)    );}

The controller and model are now wired together, so it's time to turn torouting.

Adding the edit route

The edit route is identical to theblog/detail route we previously defined,with two exceptions:

  • it will have a path prefix,/edit
  • it will route to ourWriteController

Update the 'blog'child_routes to add the new route:

// In module/Blog/config/module.config.php:use Zend\Router\Http\Segment;return [    'service_manager' => [ /* ... */ ],    'controllers'     => [ /* ... */ ],    'router'          => [        'routes' => [            'blog' => [                /* ... */                'child_routes' => [                    /* ... */                    'edit' => [                        'type' => Segment::class,                        'options' => [                            'route'    => '/edit/:id',                            'defaults' => [                                'controller' => Controller\WriteController::class,                                'action'     => 'edit',                            ],                            'constraints' => [                                'id' => '[1-9]\d*',                            ],                        ],                    ],                ],            ],        ],    ],    'view_manager'    => [ /* ... */ ],];

Creating the edit template

Rendering the form remains essentially the same between theadd andedittemplates; the only difference between them is the form action. As such, we willcreate a newpartial script for the form, update theadd template to use it,and create a newedit template.

Create a new file,module/Blog/view/blog/write/form.phtml, with the followingcontents:

<?php$form = $this->form;$fieldset = $form->get('post');$title = $fieldset->get('title');$title->setAttribute('class', 'form-control');$title->setAttribute('placeholder', 'Post title');$text = $fieldset->get('text');$text->setAttribute('class', 'form-control');$text->setAttribute('placeholder', 'Post content');$submit = $form->get('submit');$submit->setValue($this->submitLabel);$submit->setAttribute('class', 'btn btn-primary');$form->prepare();echo $this->form()->openTag($form);?><fieldset><div class="form-group">    <?= $this->formLabel($title) ?>    <?= $this->formElement($title) ?>    <?= $this->formElementErrors()->render($title, ['class' => 'help-block']) ?></div><div class="form-group">    <?= $this->formLabel($text) ?>    <?= $this->formElement($text) ?>    <?= $this->formElementErrors()->render($text, ['class' => 'help-block']) ?></div></fieldset><?phpecho $this->formSubmit($submit);echo $this->formHidden($fieldset->get('id'));echo $this->form()->closeTag();

Now, update theadd template,module/Blog/view/write/add.phtml to read asfollows:

<h1>Add a blog post</h1><?php$form = $this->form;$form->setAttribute('action', $this->url());echo $this->partial('blog/write/form', [    'form' => $form,    'submitLabel' => 'Insert new post',]);

The above retrieves the form, sets the form action, provides acontext-appropriate label for the submit button, and renders it with our newpartial view script.

Next in line is the creation of the new template,blog/write/edit:

<h1>Edit blog post</h1><?php$form = $this->form;$form->setAttribute('action', $this->url('blog/edit', [], true));echo $this->partial('blog/write/form', [    'form' => $form,    'submitLabel' => 'Update post',]);

The three differences between theadd andedit templates are:

  • The heading at the top of the page.
  • The URI used for the form action.
  • The label used for the submit button.

Because the URI requires the identifier, we need to ensure the identifier ispassed. The way we've done this in the controllers is to pass the identifier asa parameter:$this->url('blog/edit/', ['id' => $id]). This would require thatwe pass the originalPost instance or the identifier we pull from it to theview, however. zend-router allows another option, however: you can tell it tore-use currently matched parameters. This is done by setting the last parameterof the view-helper totrue:$this->url('blog/edit', [], true).

If you try and update the post, you will receive the following error:

Call to member function getId() on null

That is because we have not yet implemented the update functionality inour command class which will return a Post object on success. Let's do that now.

Edit the filemodule/Blog/src/Model/ZendDbSqlCommand.php, and update theupdatePost() method to read as follows:

public function updatePost(Post $post){    if (! $post->getId()) {        throw new RuntimeException('Cannot update post; missing identifier');    }    $update = new Update('posts');    $update->set([            'title' => $post->getTitle(),            'text' => $post->getText(),    ]);    $update->where(['id = ?' => $post->getId()]);    $sql = new Sql($this->db);    $statement = $sql->prepareStatementForSqlObject($update);    $result = $statement->execute();    if (! $result instanceof ResultInterface) {        throw new RuntimeException(            'Database error occurred during blog post update operation'        );    }    return $post;}

This looks very similar to theinsertPost() implementation we did earlier. Theprimary difference is the usage of theUpdate class; instead of calling avalues() method on it, we call:

  • set(), to provide the values we are updating.
  • where(), to provide criteria to determine which records (record singular, in our case) are updated.

Additionally, we test for the presence of an identifier before performing theoperation, and, because we already have one, and thePost submitted to uscontains all the edits we submitted to the database, we return it verbatim onsuccess.

Implementing the delete functionality

Last but not least, it's time to delete some data. We start this process byimplementing thedeletePost() method in ourZendDbSqlCommand class:

// In module/Blog/src/Model/ZendDbSqlCommand.php:public function deletePost(Post $post){    if (! $post->getId()) {        throw new RuntimeException('Cannot update post; missing identifier');    }    $delete = new Delete('posts');    $delete->where(['id = ?' => $post->getId()]);    $sql = new Sql($this->db);    $statement = $sql->prepareStatementForSqlObject($delete);    $result = $statement->execute();    if (! $result instanceof ResultInterface) {        return false;    }    return true;}

The above usesZend\Db\Sql\Delete to create the SQL necessary to delete thepost with the given identifier, which we then execute.

Next, let's create a new controller,Blog\Controller\DeleteController, in anew filemodule/Blog/src/Controller/DeleteController.php, with the followingcontents:

<?phpnamespace Blog\Controller;use Blog\Model\Post;use Blog\Model\PostCommandInterface;use Blog\Model\PostRepositoryInterface;use InvalidArgumentException;use Zend\Mvc\Controller\AbstractActionController;use Zend\View\Model\ViewModel;class DeleteController extends AbstractActionController{    /**     * @var PostCommandInterface     */    private $command;    /**     * @var PostRepositoryInterface     */    private $repository;    /**     * @param PostCommandInterface $command     * @param PostRepositoryInterface $repository     */    public function __construct(        PostCommandInterface $command,        PostRepositoryInterface $repository    ) {        $this->command = $command;        $this->repository = $repository;    }    public function deleteAction()    {        $id = $this->params()->fromRoute('id');        if (! $id) {            return $this->redirect()->toRoute('blog');        }        try {            $post = $this->repository->findPost($id);        } catch (InvalidArgumentException $ex) {            return $this->redirect()->toRoute('blog');        }        $request = $this->getRequest();        if (! $request->isPost()) {            return new ViewModel(['post' => $post]);        }        if ($id != $request->getPost('id')            || 'Delete' !== $request->getPost('confirm', 'no')        ) {            return $this->redirect()->toRoute('blog');        }        $post = $this->command->deletePost($post);        return $this->redirect()->toRoute('blog');    }}

Like theWriteController, it composes both ourPostRepositoryInterface andPostCommandInterface. The former is used to ensure we are referencing a validpost instance, and the latter to perform the actual deletion.

When a user requests the page via theGET method, we will display a pagecontaining details of the post, and a confirmation form. When submitted, we'llcheck to make sure they confirmed the deletion before issuing our deletecommand. If any conditions fail, or on a successful deletion, we redirect to ourblog listing page.

Like the other controllers, we now need a factory. Create the filemodule/Blog/src/Factory/DeleteControllerFactory.php with the followingcontents:

<?phpnamespace Blog\Factory;use Blog\Controller\DeleteController;use Blog\Model\PostCommandInterface;use Blog\Model\PostRepositoryInterface;use Interop\Container\ContainerInterface;use Zend\ServiceManager\Factory\FactoryInterface;class DeleteControllerFactory implements FactoryInterface{    /**     * @param ContainerInterface $container     * @param string $requestedName     * @param null|array $options     * @return DeleteController     */    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)    {        return new DeleteController(            $container->get(PostCommandInterface::class),            $container->get(PostRepositoryInterface::class)        );    }}

We'll now wire this into the application, mapping the controller to its factory,and providing a new route. Open the filemodule/Blog/config/module.config.phpand make the following edits.

First, map the controller to its factory:

'controllers' => [    'factories' => [        Controller\ListController::class => Factory\ListControllerFactory::class,        Controller\WriteController::class => Factory\WriteControllerFactory::class,        // Add the following line:        Controller\DeleteController::class => Factory\DeleteControllerFactory::class,    ],],

Now add another child route to our "blog" route:

'router' => [    'routes' => [        'blog' => [            /* ... */            'child_routes' => [                /* ... */                'delete' => [                    'type' => Segment::class,                    'options' => [                        'route' => '/delete/:id',                        'defaults' => [                            'controller' => Controller\DeleteController::class,                            'action'     => 'delete',                        ],                        'constraints' => [                            'id' => '[1-9]\d*',                        ],                    ],                ],            ],        ],    ],],

Finally, we'll create a new view script,module/Blog/view/blog/delete/delete.phtml, with the following contents:

<h1>Delete post</h1><p>Are you sure you want to delete the following post?</p><ul class="list-group">    <li class="list-group-item"><?= $this->escapeHtml($this->post->getTitle()) ?></li></ul><form action="<?php $this->url('blog/delete', [], true) ?>" method="post">    <input type="hidden" name="id" value="<?= $this->escapeHtmlAttr($this->post->getId()) ?>" />    <input class="btn btn-default" type="submit" name="confirm" value="Cancel" />    <input class="btn btn-danger" type="submit" name="confirm" value="Delete" /></form>

This time around, we're not using zend-form; as it consists of just a hiddenelement and cancel/confirm buttons, there's no need to provide an OOP model for it.

From here, you can now visit one of the existing blog posts, e.g.,http://localhost:8080/blog/delete/1 to see the form. If you chooseCancel,you should be taken back to the list; if you chooseDelete, it should deletethe post and then take you back to the list, and you should see the post is nolonger present.

Making the list more useful

Our blog post list currently lists everything about all of our blog posts;additionally, it doesn't link to them, which means we have to manually updatethe URL in our browser in order to test functionality. Let's update the listview to be more useful; we'll:

  • List just the title of each blog post;
  • linking the title to the post display;
  • and providing links for editing and deleting the post.
  • Add a button to allow users to add a new post.

In a real-world application, we'd probably use some sort of access controls todetermine if the edit and delete links will be displayed; we'll leave that foranother tutorial, however.

Open yourmodule/Blog/view/blog/list/index.phtml file, and update it to readas follows:

<h1>Blog Posts</h1><div class="list-group"><?php foreach ($this->posts as $post): ?>  <div class="list-group-item">    <h4 class="list-group-item-heading">      <a href="<?= $this->url('blog/detail', ['id' => $post->getId()]) ?>">        <?= $post->getTitle() ?>      </a>    </h4>    <div class="btn-group" role="group" aria-label="Post actions">      <a class="btn btn-xs btn-default" href="<?= $this->url('blog/edit', ['id' => $post->getId()]) ?>">Edit</a>      <a class="btn btn-xs btn-danger" href="<?= $this->url('blog/delete', ['id' => $post->getId()]) ?>">Delete</a>    </div>  </div>    <?php endforeach ?></div><div class="btn-group" role="group" aria-label="Post actions">  <a class="btn btn-primary" href="<?= $this->url('blog/add') ?>">Write new post</a></div>

At this point, we have a far more functional blog, as we can move around betweenpages using links and buttons.

Summary

In this chapter we've learned how data binding within the zend-form componentworks, and used it to provide functionality for our update routine. We alsolearned how this allows us to de-couple our controllers from the details of howa form is structured, helping us keep implementation details out of ourcontroller.

We also demonstrated the use of view partials, which allow us to split outduplication in our views and re-use them. In particular, we did this with ourform, to prevent needlessly duplicating the form markup.

Finally, we looked at two more aspects of theZend\Db\Sql subcomponent, andlearned how to performUpdate andDelete operations.

In the next chapter we'll summarize everything we've done. We'll talk about thedesign patterns we've used, and we'll cover several questions that likely aroseduring the course of this tutorial.

Found a mistake or want to contribute to the documentation? Edit this page on GitHub!