Model - View - Controller (MVC)

Model - View - Controller (MVC)

Creating Model to display book listing View via Controller.

mvc.png
Parent post: Online Book Catalog Application Tutorial

To start an application, I generally prefer to start by creating the data structure and building the rest on it. Therefore, I will create the Book table first and its corresponding object class - entity in the Online Book Catalog application.

I believe "if there is data, then you have the minimum requirement to write an application".

Book Entity

  • id: Book id
  • alias: Book alias to be mainly used in the URL
  • title: Book title
  • author: Author of the book
  • description: Book description
  • publish_date: Date published
  • isbn: ISBN
  • language: Book language
-- Table structure for `book` table

CREATE TABLE `book` ( 
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
    `alias` VARCHAR(150) NOT NULL , 
    `title` VARCHAR(150) NOT NULL , 
    `author` VARCHAR(50) NOT NULL , 
    `description` VARCHAR(1000) NOT NULL , 
    `publish_date` DATE NOT NULL , 
    `isbn` VARCHAR(14) NOT NULL , 
    `language` VARCHAR(20) NOT NULL , 
    PRIMARY KEY (`id`), 
    UNIQUE (`alias`)
);

I defined the language field in this table to be a string type. However, I would later refactor the model to represent the language information by another entity called LanguageEntity. There is, however, a lot to do until then.

Next, added some data to have some content. I used the PHP Books page in Zend Store as of writing this post and got a few sample books from the beginning of the offered books list.

-- Sample data for `book` table

INSERT INTO `book` (`id`, `alias`, `title`, `author`, `description`, `publish_date`, `isbn`, `language`) VALUES
(1, 'learn-zf2-2013', 'Learn ZF2: Learning By Example', 'Slavey Karadzhov', 'Zend Framework 2 (ZF2) has changed the way to develop PHP applications and like every revolution takes time to be digested.\r\nThe book will help you understand the major components in ZF2 and how to use them as best as possible. The chapters in this book will lead you through the different components and in the process together with the author you will build a complete application.', '2013-09-25', '978-1492372219', 'English'),
(2, 'advanced-php-programming-2004', 'Advanced PHP Programming', 'George Schlossnagle', 'Over the past three years PHP has evolved from being a niche language use to add dynamic functionality to small sites to a powerful tool making strong inroads into large-scale, business-critical Web systems. While there are many books on learning PHP and developing small applications with it, there is a serious lack of information on "scaling" PHP for large-scale, business-critical systems. Schlossnagle''s Advanced PHP Programming fills that void, demonstrating that PHP is ready for enterprise Web applications by showing the reader how to develop PHP-based applications for maximum performance, stability, and extensibility', '2004-03-01', '978-0672325618', 'English'),
(3, 'php-and-mysql-web-development-2008', 'PHP and MySQL Web Development (4th Edition)', 'Luke Welling', 'PHP and MySQL Web Development shows how to use these tools together to produce effective, interactive Web applications. It clearly describes the basics of the PHP language, explains how to set up and work with a MySQL database, and then shows how to use PHP to interact with the database and the server.', '2008-10-11', '978-0672329166', 'English'),
(4, 'zend-framework-in-action-2009', 'Zend Framework in Action', 'Rob Allen', 'Zend Framework in Action is a comprehensive tutorial that shows how to use the Zend Framework to create web-based applications and web services. This book takes you on an over-the-shoulder tour of the components of the Zend Framework as you build a high quality, real-world web application. This book is organized around the techniques you''ll use every day as a web developer "data handling, forms, authentication, and so forth. As you follow the running example, you''ll learn to build interactive Ajax-driven features into your application without sacrificing nuts-and-bolts considerations like security and performance.', '2009-01-07', '978-1933988320', 'English');

I can now create the BookEntity class module representing the book table rows in my application.

# /module/Application/src/Model/Book/BookEntity.php

<?php
namespace Application\Model\Book;

/**
 * BookEntity
 *
 * Entity for Book Object
 */
class BookEntity
{
    /**
     * @var int
     */
    protected $id;
    
    /**
     *
     * @var string
     */
    protected $alias;
    
    /**
     *
     * @var string
     */
    protected $title;
    
    /**
     *
     * @var string
     */
    protected $author;
    
    /**
     *
     * @var string
     */
    protected $description;
    
    /**
     *
     * @var string
     */
    protected $publish_date;
    
    /**
     *
     * @var string
     */
    protected $isbn;
    
    /**
     *
     * @var string
     */
    protected $language;
    
    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }
    
    /**
     * @param int $id
     * @return self
    */
    public function setId($id)
    {
        $this->id = $id;
        return $this;
    }
    
    /**
     * @return string
     */
    public function getAlias()
    {
        return $this->alias;
    }
    
    /**
     * @param string $alias
     * @return self
    */
    public function setAlias($alias)
    {
        $this->alias = $alias;
        return $this;
    }
    
    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }
    
    /**
     * @param string $title
     * @return self
    */
    public function setTitle($title)
    {
        $this->title = $title;
        return $this;
    }
    
    /**
     * @return string
     */
    public function getAuthor()
    {
        return $this->author;
    }
    
    /**
     * @param string $author
     * @return self
    */
    public function setAuthor($author)
    {
        $this->author = $author;
        return $this;
    }
    
    /**
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }
    
    /**
     * @param string $description
     * @return self
    */
    public function setDescription($description)
    {
        $this->description = $description;
        return $this;
    }
    
    /**
     * @return string
     */
    public function getPublishDate()
    {
        return $this->publish_date;
    }
    
    /**
     * @param string $publish_date
     * @return self
    */
    public function setPublishDate($publish_date)
    {
        $this->publish_date = $publish_date;
        return $this;
    }
    
    /**
     * @return string
     */
    public function getIsbn()
    {
        return $this->isbn;
    }
    
    /**
     * @param string $isbn
     * @return self
    */
    public function setIsbn($isbn)
    {
        $this->isbn = $isbn;
        return $this;
    }
    
    /**
     * @return string
     */
    public function getLanguage()
    {
        return $this->language;
    }
    
    /**
     * @param string $language
     * @return self
    */
    public function setLanguage($language)
    {
        $this->language = $language;
        return $this;
    }
}
I am using method chaining in the entity setters by returning the class itself to be able to set multiple properties of the object in single statement later.

I saved the BookEntity class module in the directory structure shown in Fig1. I will save all model-related class modules into the related model's folder.

Application directory structure with Model folder
Application module directory with the new Model folder

I currently have the default controller of the Skeleton Application named InderController which shows the home page that I previously modified through its only action method indexAction(). However, I will create a separate controller CatalogController for building the actions such as displaying the list of books as well as displaying the book detail as separate views. Eventually, I would also route the home page to the catalog index.

# /module/Application/src/Controller/CatalogController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

/**
 * CatalogController
 * 
 * Controller for Catalog Model
 */
class CatalogController extends AbstractActionController
{
    /**
     * 
     * Action called for displaying the book listing
     *
     * @return \Zend\View\Model\ViewModel 
     */
    public function indexAction()
    {
        return new ViewModel();       
    }
}
Why not BookController but CatalogController? CatalogController is not about managing the book model but displaying the views containing one or more book records. BookController will join the game when the Administrator user starts managing the book records. Ok, it is still possible to use a single controller for both cases but this is the way I prefer to maintain real-life modeling - like books are children of the catalog and they are separated models. In fact, also being prepared for the resource permissions that I will deal with later. There is no user or permissions yet, I am simply creating a public view that every visitor can access but I also know it is better to separate these controllers at the beginning.

CatalogController is useless at the moment. I have to let my Application module informed about it and I will do that by adding the necessary controller into the controllers key as a factory key => value pair in module.config.php file.

# /module/Application/config/module.config.php

return [
    // ...
    
    'controllers' => [
        'factories' => [
            Controller\IndexController::class => InvokableFactory::class,
            Controller\CatalogController::class => InvokableFactory::class,
        ],
    ],
    
    // ...
];
I defined the new controller by using the default InvokableFactory. I will change it to be created by its own factory class when it is time to inject dependencies to its constructor, which would be BookMapper that is controlling table operations for Book Model.

The new controller is still useless since there is no route associated with it. I can't use the default application route accepting action parameter since I am not creating a new action in the default IndexController but using a completely separate controller. Therefore, I will create a brand new route identified as catalog that will trigger my new controller actions in the same module configuration file.

# /module/Application/config/module.config.php

return [
    'router' => [
        'routes' => [
            // ...
            
            'catalog' => [
                'type'    => Literal::class,
                'options' => [
                    'route'    => '/books/',
                    'defaults' => [
                        'controller' => Controller\CatalogController::class,
                        'action'     => 'index',
                    ],
                ],
            ],
        ],
    ],
    
    // ...
];

This route simply tells to Zend Framework to execute indexAction() method in the CatalogController class when http://bc.onlinebookcatalog.com/books/ is requested. Note that there is no parameter in the route and I am using Literal route type for now.

I will be using a trailing slash for the routes in my routers. I would later create an .htaccess rule to make it work for all links to be working with trailing slash, so all my website URLs would consistently have a trailing slash. It is not a requirement at all, but only my choice here to maintain URL consistency.

I will also add a new menu item into the layout.html to access this route easily.

# /module/Application/view/application/layout/layout.phtml

<?= $this->doctype() ?>
<html lang="en">
    // ...

    <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
            <li><a href="<?= $this->url('home') ?>">Home</a></li>
            <li class="active"><a href="<?= $this->url('catalog') ?>">Book Listing</a></li>
        </ul>
    </div>

    // ...
</html>

Let's visit the Book Listing page that we just linked with the catalog route to find out how it fails as shown in Fig2.

Missing view template error.
This is all about a missing view template

Every action returning a view model requires an associated view - index.phtml for the indexAction() method of the new controller in this case. I will create a simple .phtml file to fullfill the controller action for now and show the directory structure to get familiarized with the controllers and associated view template paths.

# /module/Application/view/application/catalog/index.phtml

<div class="jumbotron">
    <p>
        This is the view template for indexAction() method.
    </p>
</div>

And the expected result:

Catalog index action result.
Catalog index action is associated with a view template

Current module directory structure:

Application directory structure with catalog view template folder
Catalog view template folder included module directory structure

Time to display something real! I will change the indexAction() method and create book entities from the data array I will provide hard-coded in the function directly.

# /module/Application/src/Controller/CatalogController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;


// ...
class CatalogController extends AbstractActionController
{
    public function indexAction()
    {
        $data = [
            1 => [
                'id'            => '1', 
                'alias'         => 'learn-zf2-2013', 
                'title'         => 'Learn ZF2: Learning By Example', 
                'author'        => 'Slavey Karadzhov', 
                'description'   => 'Learn ZF2: Learning By Example by Slavey Karadzhov', 
                'publish_date'  => '2013-09-25', 
                'isbn'          => '978-1492372219',
                'language'      => 'English'],
            2 => [
                'id'            => '2', 
                'alias'         => 'advanced-php-programming-2004', 
                'title'         => 'Advanced PHP Programming', 
                'author'        => 'George Schlossnagle', 
                'description'   => 'Advanced PHP Programming by George Schlossnagle', 
                'publish_date'  => '2004-03-01', 
                'isbn'          => '978-0672325618', 
                'language'      =>'English'],
            3 => [
                'id'            => '3', 
                'alias'         => 'php-and-mysql-web-development-2008', 
                'title'         => 'PHP and MySQL Web Development (4th Edition)', 
                'author'        => 'Luke Welling', 
                'description'   => 'Advanced PHP Programming by Luke Welling', 
                'publish_date'  => '2008-10-11', 
                'isbn'          => '978-0672329166', 
                'language'      =>'English'],
            4 => [
                'id'            => '4', 
                'alias'         => 'zend-framework-in-action-2009', 
                'title'         => 'Zend Framework in Action', 
                'author'        => 'Rob Allen', 
                'description'   => 'Zend Framework in Action by Rob Allen', 
                'publish_date'  => '2009-01-07', 
                'isbn'          => '978-1933988320', 
                'language'      => 'English'],
        ];

        
        $books = [];

        foreach ($data as $book_data) {
            $book = new \Application\Model\Book\BookEntity();
            $book->setId($book_data['id'])
                ->setAlias($book_data['alias'])
                ->setTitle($book_data['title'])
                ->setAuthor($book_data['author'])
                ->setDescription($book_data['description'])
                ->setPublishDate($book_data['publish_date'])
                ->setIsbn($book_data['isbn'])
                ->setLanguage($book_data['language']);
                
            $books[] = $book;
        }
        
        return new ViewModel([
            'books' => $books,
        ]);       
    }
    
    // ...
}

I created the view template by injecting the $books array variable, so I can use this variable in the view template to create the book listing.

I didn't use the database connection yet, even not a mapper to create the entities so far. Just created the data array in the controller action for demonstration purposes - which shouldn't happen in real. Controller code will be completely rewritten by the end of this post, however it is beautiful to see that the view template will not even affected by a single line with this rewrite.
# /module/Application/view/application/catalog/index.phtml

<h1>Book Listing</h1>
<div class="row">
    <div class="col-xs-12">
        <table class="table table-condensed table-striped">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>Author</th>
                    <th>Publish Date</th>
                    <th>ISBN</th>
                    <th>Language</th>
                </tr>
            </thead>
            <tbody>
                <?php /* @var $book \Application\Model\Book\BookEntity */?>
                <?php foreach ($this->books as $book): ?>
                <tr>
                    <td><?= $book->getTitle() ?></td>
                    <td><?= $book->getAuthor() ?></td>
                    <td><?= $book->getPublishDate() ?></td>
                    <td><?= $book->getIsbn() ?></td>
                    <td><?= $book->getLanguage() ?></td>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
</div>

Here is the current result of the catalog index:

Online book catalog index page
Online Book Catalog Index Page

Book entities will not be created in the controller action as I hard-coded above but are supposed to be provided by the associated mapper. Therefore controller wouldn't be bothered where and how the data is being retrieved.

It is time to create BookMapper.

# /module/Application/src/Model/Book/BookMapper.php

<?php
namespace Application\Model\Book;
 
/**
 * BookMapper
 * 
 * Mapper for Book Model
 */
class BookMapper
{
    /**
     *
     * @var BookEntity[]
     */
    protected $books;
    
    /**
     * Constructor
     */
    public function __construct()
    {
        $data = [
            1 => [
                'id'            => '1',
                'alias'         => 'learn-zf2-2013',
                'title'         => 'Learn ZF2: Learning By Example',
                'author'        => 'Slavey Karadzhov',
                'description'   => 'Learn ZF2: Learning By Example by Slavey Karadzhov',
                'publish_date'  => '2013-09-25',
                'isbn'          => '978-1492372219',
                'language'      => 'English'],
            2 => [
                'id'            => '2',
                'alias'         => 'advanced-php-programming-2004',
                'title'         => 'Advanced PHP Programming',
                'author'        => 'George Schlossnagle',
                'description'   => 'Advanced PHP Programming by George Schlossnagle',
                'publish_date'  => '2004-03-01',
                'isbn'          => '978-0672325618',
                'language'      =>'English'],
            3 => [
                'id'            => '3',
                'alias'         => 'php-and-mysql-web-development-2008',
                'title'         => 'PHP and MySQL Web Development (4th Edition)',
                'author'        => 'Luke Welling',
                'description'   => 'Advanced PHP Programming by Luke Welling',
                'publish_date'  => '2008-10-11',
                'isbn'          => '978-0672329166',
                'language'      =>'English'],
            4 => [
                'id'            => '4',
                'alias'         => 'zend-framework-in-action-2009',
                'title'         => 'Zend Framework in Action',
                'author'        => 'Rob Allen',
                'description'   => 'Zend Framework in Action by Rob Allen',
                'publish_date'  => '2009-01-07',
                'isbn'          => '978-1933988320',
                'language'      => 'English'],
        ];
        
        foreach ($data as $book_data) {
            $book = new BookEntity();
            $book->setId($book_data['id'])
                ->setAlias($book_data['alias'])
                ->setTitle($book_data['title'])
                ->setAuthor($book_data['author'])
                ->setDescription($book_data['description'])
                ->setPublishDate($book_data['publish_date'])
                ->setIsbn($book_data['isbn'])
                ->setLanguage($book_data['language']);
        
            $this->books[] = $book;
        }
    }
 
    /**
     * FetchAll for Book Model
     * 
     * @return array \Application\Model\Book\BookEntity[]
     */
    public function fetchAll()
    {
        return $this->books;
    }
}

I am still not retrieving data from the database annoyingly but just moved the demonstrative entity creation logic from the controller to the mapper constructor and creaating a $books property of the mapper to be returned by fetchAll() method.

This is also not the final BookMapper of the Online Book Catalog application but created to be used in this post to demonstrate entity creation. The controller wouldn't mind how the data comes, so I will change the mapper in its own post and you'll see there won't be a single line change in the controller code.

I will change CatalogController to use the new mapper, however I need to introduce BookMapper to my application module first. It is done in Module.php implements Zend\ModuleManager\Feature\ServiceProviderInterface and returns the mapper via getServiceConfig() method.

# /module/Application/src/Module.php

<?php
namespace Application;

use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
use Application\Model\Book\BookMapper;

class Module implements ServiceProviderInterface
{
    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }
    
    public function getServiceConfig()
    {
        return [
            'factories' => [
                BookMapper::class => InvokableFactory::class,
            ]
        ];
    }
}

And it is time to rebuild CatalogController by creating it via CatalogControllerFactory to inject BookMapper.

I created the controller by using Zend\ServiceManager\Factory\InvokableFactory at first. However, the controller now has a dependency object, mapper in this case, so I have to create my controller via its factory class and inject the necessary objects through its constructor.

Therefore, I will create CatalogControllerFactory as second step.

# /module/Application/src/Controller/Factory/CatalogControllerFactory.php

<?php
namespace Application\Controller\Factory;

use Zend\ServiceManager\Factory\FactoryInterface;
use Interop\Container\ContainerInterface;
use Application\Controller\CatalogController;
use Application\Model\Book\BookMapper;

/**
 * CatalogControllerFactory
 * 
 * Controller Factory for Catalog Controller
 */
class CatalogControllerFactory implements FactoryInterface
{
    /**
     * Handle invoke calls injecting dependencies
     *
     * @return CatalogController
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $bookMapper = $container->get(BookMapper::class);

        $controller = new CatalogController($bookMapper);
        
        return $controller;
    }
}

And rewrite CatalogController to get the injected mapper in the constructor and set it as a private property to be used in its actions to access whenever I need to use the mapper methods to retrieve data initially.

# /module/Application/src/Controller/CatalogController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Model\Book\BookMapper;

// ...
class CatalogController extends AbstractActionController
{
    /**
     *
     * @var BookMapper
     */
    private $bookMapper;
    
    public function __construct(BookMapper $bookMapper)
    {
        $this->bookMapper = $bookMapper;
    }
    
    public function indexAction()
    {
        $books = $this->bookMapper->fetchAll();
        
        return new ViewModel([
            'books' => $books,
        ]);       
    }
}

Finally I remember that I initially created CatalogController as an InvokableFactory and I actually need to inform my module to create it through its factory now. I need to change module.config.php as shown below.

# /module/Application/config/module.config.php

return [
    // ...
    
    'controllers' => [
        'factories' => [
            Controller\IndexController::class => InvokableFactory::class,
            Controller\CatalogController::class => Controller\Factory\CatalogControllerFactory::class,
        ],
    ],
    
    // ...
];

I am done! I don't even need to make a single line change in the view template as it has nothing to do with all these improvements in my code but simply use what it is served - $books variable.

Online book catalog index page
Exactly the same output before the mapper and factory usage

I know that I said "To start an application, I generally prefer to start by creating the data structure and build the rest on it.", in fact I created the book table and even imported data into it but never used a database connection so far! I don't even have Zend\Db included in my project! Ok, I never said, "I need to use database connection in this post". The database structure has been only used to create the book entity so far. I will use database connection in the next post of this series.

Remember, Controller has nothing to do with how data is retrieved, it is just asking Model to get it and passing to View to be displayed.

Fig7 shows the final application module directory structure with the new classes included.

Application module directory structure
Application module directory structure

Published on Dec 22, 2016