MVC Application - Need some guidance

Hi all, I am building an Ajax driven PHP application that uses a single page and want to implement the MVC pattern. Every time I think I have my head around the concepts I try to implement them and hit another wall. I was hoping to use this thread to post the issues I come across as I encounter than and hopefully work through the problems I’m having.

Question 1: Is the model in MVC a domain object?

[INDENT]Let’s say for instance I have a library application and need to handle searching for, getting details of, adding and deleting books. Should my model be the book object, or should it use the book objects?

Examples:

1. Without Book domain object.


class BookModel extends BaseModel
{


    public function getAll()
    {
        $sql    = 'SELECT * FROM Books';
        $stmt   = $this->database->query($sql);
        $values = array();

        while ($row = $stmt->fetch()) {
            array_push($values, $row);
        }

        return $values;

    }//end getAll()


    public function getBookByName($name)
    {
        $sql  = 'SELECT * FROM Books WHERE bookName = '.$name;
        $stmt = $this->database->query($sql);

        $row = $stmt->fetch();
        return $row;

    }//end getBookByName()


}//end class

2. With Book domain object.


class BookModel extends BaseModel
{


    require '../app/classes/Book.php';

    public function getAll()
    {
        $sql    = 'SELECT * FROM Books';
        $stmt   = $this->database->query($sql);
        $values = array();

        while ($row = $stmt->fetch()) {
            $book = new Book($row['id'], $row['title'], $row['isdn'], $row['author']);
            array_push($values, $book);
        }

        return $values;

    }//end getAll()


    public function getBookByName($name)
    {
        $sql  = 'SELECT * FROM Books WHERE bookName = '.$name;
        $stmt = $this->database->query($sql);

        $row = $stmt->fetch();
        $book = new Book($row['id'], $row['title'], $row['isdn'], $row['author']);
        return $book;

    }//end getBookByName()


}//end class

I am assuming if I am using domain objects I would want to be even smarter than this?


class BookModel extends BaseModel
{


    require '../app/classes/Book.php';

    public function getBookByName($name)
    {
        $book = new Book();
        $book->find($name);
        return $book;

    }//end getBookByName()


}//end class

The above seems to make more sense, but how would you implement the findAll() function then? It can’t be the method of a single book, do I need an object for Book and an object for BookCollection?


class BookModel extends BaseModel
{


    require '../app/classes/Book.php';
    require '../app/classes/BookCollection.php';

    public function getAll()
    {
        $bookCollection = new BookCollection();
        $bookCollection->getAll();
        return $bookCollection();

    }//end getAll()


}//end class

[/INDENT]

Question 2: Should database access exist in the model?

[INDENT]It doesn’t feel write to me, but how do you implement database access otherwise? Does the below make more sense?

Controller  <->  Model  <->  Domain Object  <->  Database Access
            <->  View

If so; how do you structure you Models, Domain Objects and Database Access components to talk to each other?[/INDENT]

I have more questions but I’d like to get my head clear around these two first. Then I can take this further.

Appreciate any advice.

Hi Jordan,

In answer to your first question, the Model in MVC is a layer rather than a specific object or type of object. Domain objects form part of your model layer, as does persistence (this article gives a nice breakdown of the MVC pattern).

As for your second question, Active Record based ORMs encourage you to combine domain logic with persistence, but this is widely considered to be bad practice as it violates the Single Responsibility Principle (SRP). Have a look at the data mapper pattern as a better solution.

Thanks @fretburner ;, this clears things up a little. If I understand correctly the M in MVC = Domain Objects + Data Acess Layer.

Would this be how you implement?


class BookModel extends BaseModel 
{ 


    require 'BookMapper.php';

    private $__bookMapper;


    public function __construct()
    {
        $this->__bookMapper = new BookMapper();

    }//end __construct()


    public function getBooks() 
    { 
        $books = $this->__bookMapper->fetchAll(); 
        return $books; 

    }//end getBooks() 


    public function getBookByTitle($title) 
    { 
        $book = $this->__bookMapper->fetchByTitle($title); 
        return $book; 

    }//end getBookByTitle() 


}//end class  


class Book 
{ 

    public $id;
    public $title;
    public $isbn;
    public $author;

    public function __construct($id, $title, $isbn, $author)
    {
        $this->id     = $id;
        $this->title  = $title;
        $this->isbn   = $isbn;
        $this->author = $author;

    }//end __construct()

   
}//end class 


class BookMapper
{

    require 'Book.php'; 

    protected $database;

    public function __construct()
    {
        $appconfig = parse_ini_file('../app/config/myProject.cfg');
        $config    = new Doctrine\\DBAL\\Configuration();

        $connectionParams = array(
                             'dbname'   => $appconfig['dbname'],
                             'user'     => $appconfig['user'],
                             'password' => $appconfig['password'],
                             'host'     => $appconfig['host'],
                             'driver'   => $appconfig['driver'],
                            );

        $this->database = \\Doctrine\\DBAL\\DriverManager::getConnection($connectionParams, $config);

    }//end __construct()


    public function fetchAll()
    {

        $sql    = 'SELECT * FROM books';
        $stmt   = $this->database->query($sql);
        $books = array();

        while ($row = $stmt->fetch()) {
            $book = new Book($row['id'], $row['title'], $row['isbn'], $row['author']);
            array_push($books, $book);
        }

        return $books;

    }//end fetchAll()


    public function fetchByTitle($title)
    {
        $sql  = 'SELECT * FROM books WHERE bookTitle = '.$title;
        $stmt = $this->database->query($sql);

        $row = $stmt->fetch();
        $book = new Book($row['id'], $row['title'], $row['isbn'], $row['author']);

        return $book;

    }//end fetchByTitle()


}//end class

And my controller:


class Books extends BaseController
{


    protected function getAll()
    {
        $viewmodel = new BookModel();
        $this->ReturnView($viewmodel->getBooks(), false);

    }//end getAll()


    protected function getBook()
    {
        $viewmodel = new BookModel();
        $this->ReturnView($viewmodel->getBookById($this->urlvalues['title']), false);

    }//end getPromotion()


}//end class


abstract class BaseController
{

    protected $urlvalues;

    protected $action;


    public function __construct($action, $urlvalues)
    {
        $this->action    = $action;
        $this->urlvalues = $urlvalues;

    }//end __construct()


    public function executeAction()
    {
        return $this->{$this->action}();

    }//end executeAction()


    protected function returnView($viewmodel, $fullview)
    {
        $viewloc = '../app/views/'.get_class($this).'/'.$this->action.'.php';
        if ($fullview === true) {
            include '../app/views/maintemplate.php';
        } else {
            include $viewloc;
        }

    }//end returnView()


}//end class

Hi Jordan,

You don’t need the BookModel class… you can just inject BookMapper into your controller and call fetchAll / fetchByTitle directly.

A couple of points about your mapper class: First, you don’t really want to be including files within your classes, use autoloading instead. Second, the contructor knows way too much about the database connection… build the connection object first and then pass it into the mapper.

Thanks, I have taken this a bit further now including autoloading. I have also set up a base DataMapper class that the domain specific mappers extend, and this has the database connection. If I was going to build it first, where would it belong? Below is what I have so far.

index.php


<?php

use FightSportsDB\\App\\Common;

// Set up Autoloaders
require_once '../App/Common/SplClassLoader.php';
$classLoader = new Common\\SplClassLoader('FightSportsDB', '.\\..\\..');
$classLoader->register();

$doctrineClassLoader = new Common\\SplClassLoader('Doctrine', '../App/lib');
$doctrineClassLoader->register();

// Create the controller and execute the action.
$loader     = new Common\\Loader($_GET);
$controller = $loader->CreateController();
$controller->ExecuteAction();

FightSportsDB\App\Common\Loader


<?php

namespace FightSportsDB\\App\\Common;

use FightSportsDB\\App\\Controller;

/**
 * FightSportsDB Loader Doc Comment
 *
 * PHP version 5
 *
 * @category Application
 * @package  FightSportsDB
 * @author   Jordan Windebank
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @link     http://fightsportsdb.com
 *
 */

/**
 * Class Loader
 *
 * PHP version 5
 *
 * @category Application
 * @package  FightSportsDB
 * @author   Jordan Windebank
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @link     http://fightsportsdb.com
 */
class Loader
{

    private $_controller;

    private $_action;

    private $_urlvalues;

    /**
     * Store the URL values on object creation
     *
     * @param array $urlvalues Input parameters
     *
     * @return mixed
     */

    public function __construct($urlvalues)
    {
        $this->_urlvalues = $urlvalues;
        if ($this->_urlvalues['controller'] === '' || isset($this->_urlvalues['controller']) === false) {
            $this->_controller = 'Home';
        } else {
            $this->_controller = 'FightSportsDB\\\\App\\\\Controller\\\\'.$this->_urlvalues['controller'].'Controller';
        }

        if ($this->_urlvalues['action'] === '' || isset($this->_urlvalues['action']) === false) {
            $this->_action = 'index';
        } else {
            $this->_action = $this->_urlvalues['action'];
        }

    }//end __construct()


    /**
     * Establish the requested controller as an object
     *
     * @return mixed
     */

    public function createController()
    {
        // Does the class exist?
        if (class_exists($this->_controller) === true) {
            $parents = class_parents($this->_controller);
            // Does the class extend the controller class?
            if (in_array('FightSportsDB\\\\App\\\\Controller\\\\BaseController', $parents) === true) {
                // Does the class contain the requested method?
                if (method_exists($this->_controller, $this->_action) === true) {
                    return new $this->_controller($this->_action, $this->_urlvalues);
                } else {
                    // Bad method error.
                    return new Controller\\ErrorController('invalidAction', $this->_urlvalues);
                }
            } else {
                // Bad controller error.
                return new Controller\\ErrorController('invalidController', $this->_urlvalues);
            }
        } else {
            // Bad controller error.
            return new Controller\\ErrorController('invalidController', $this->_urlvalues);
        }

    }//end createController()


}//end class

FightSportsDB\App\Controller\BaseController


<?php

namespace FightSportsDB\\App\\Controller;

abstract class BaseController
{

    protected $urlvalues;

    protected $action;


    public function __construct($action, $urlvalues)
    {
        $this->action    = $action;
        $this->urlvalues = $urlvalues;

    }//end __construct()


    public function executeAction()
    {
        return $this->{$this->action}();

    }//end executeAction()


    protected function returnView($viewmodel, $fullview)
    {
        $className = get_class($this);
        $lastNsPos = strripos($className, '\\\\');
        $className = substr($className, ($lastNsPos + 1));
        $className = substr($className, 0, -10);
        $viewloc = '../App/View/'.$className.'/'.$this->action.'.php';
        if ($fullview === true) {
            include '../App/View/maintemplate.php';
        } else {
            include $viewloc;
        }

    }//end returnView()


}//end class

FightSportsDB\App\Controller\PromotionController


<?php

namespace FightSportsDB\\App\\Controller;

use FightSportsDB\\App\\Model;

class PromotionController extends BaseController
{


    protected function getAll()
    {
        $viewmodel = new Model\\PromotionModel();
        $this->ReturnView($viewmodel->getPromotions(), false);

    }//end getAll()


    protected function getPromotion()
    {
        $viewmodel = new Model\\PromotionModel();
        $this->ReturnView($viewmodel->getPromotionById($this->urlvalues['id']), false);

    }//end getPromotion()


}//end class

FightSportsDB\App\Model\BaseModel


<?php

namespace FightSportsDB\\App\\Model;

abstract class BaseModel
{

    protected $mapper;


    public function __construct()
    {

    }//end __construct()


}//end class

FightSportsDB\App\Model\PromotionModel


<?php

namespace FightSportsDB\\App\\Model;

use FightSportsDB\\App\\DataMapper;

class PromotionModel extends BaseModel
{


    public function __construct()
    {
        $this->mapper = new DataMapper\\PromotionMapper();

    }//end __construct()


    public function getPromotions()
    {
        $promotions = $this->mapper->fetchAll();
        return $promotions;

    }//end getPromotions()


    public function getPromotionByName($name)
    {
        $promotion = $this->mapper->fetchByName($name);
        return $promotion;

    }//end getPromotionByName()


    public function getPromotionById($id)
    {
        $promotion = $this->mapper->fetchById($id);
        return $promotion;

    }//end getPromotionById()


}//end class

FightSportsDB\App\Model\Domain\Promotion


<?php

namespace FightSportsDB\\App\\Model\\Domain;

/**
 * FightSportsDB Promotion Doc Comment
 *
 * PHP version 5
 *
 * @category Application
 * @package  FightSportsDB
 * @author   Jordan Windebank
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @link     http://fightsportsdb.com
 *
 */

/**
 * Class Promotion
 *
 * PHP version 5
 *
 * @category Application
 * @package  FightSportsDB
 * @author   Jordan Windebank
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @link     http://fightsportsdb.com
 */
class Promotion
{

    /**
     * @var int
     */
    public $id;

    /**
     * @var string
     */
    public $name;

    /**
     * @var string
     */
    public $image;

    /**
     * @var string
     */
    public $imageType;


    /**
     * Store the Promotion values on object creation
     *
     * @param int    $promotionId   Promotion ID
     * @param string $promotionName Name of promotion
     * @param string $image         Address of promotion image
     * @param string $imageType     Type of promotion image [Logo|Fanart]
     */
    public function __construct($promotionId, $promotionName, $image, $imageType)
    {
        $this->id        = $promotionId;
        $this->name      = $promotionName;
        $this->image     = '../FightSportsDB/Public/images/'.$image;
        $this->imageType = $imageType;

    }//end __construct()


}//end class

FightSportsDB\App\Model\DataMapper


<?php

namespace FightSportsDB\\App\\DataMapper;

use Doctrine\\DBAL;

abstract class BaseMapper
{

    protected $database;

    public function __construct()
    {
        $appconfig = parse_ini_file('../App/Config/fightsportsdb.cfg');
        $config    = new DBAL\\Configuration();

        $connectionParams = array(
                             'dbname'   => $appconfig['dbname'],
                             'user'     => $appconfig['user'],
                             'password' => $appconfig['password'],
                             'host'     => $appconfig['host'],
                             'driver'   => $appconfig['driver'],
                            );

        $this->database = DBAL\\DriverManager::getConnection($connectionParams, $config);

    }//end __construct()


}//end class

FightSportsDB\App\Model\PromotionMapper


<?php

namespace FightSportsDB\\App\\DataMapper;

use FightSportsDB\\App\\Model\\Domain;

class PromotionMapper extends BaseMapper
{


    public function fetchAll()
    {
        $sql        = "SELECT
	                     p.promotionId AS 'id',
	                     p.promotionName AS 'name',
	                     a.artworkPath As 'image',
	                     aType.artworkType As 'imageType'
	                   FROM
	                   	 Promotions As p
	                   INNER JOIN PromotionsArtwork As pa ON p.promotionId = pa.promotionId
	                   INNER JOIN Artwork As a ON pa.artworkId = a.artworkId
	                   INNER JOIN ArtworkTypes As aType ON pa.artworkTypeId = aType.artworkTypeId";
        $stmt       = $this->database->query($sql);
        $promotions = array();

        while ($row = $stmt->fetch()) {
            $promotion = new Domain\\Promotion($row['id'], $row['name'], $row['image'], $row['imageType']);
            array_push($promotions, $promotion);
        }

        return $promotions;

    }//end fetchAll()


    public function fetchByName($name)
    {
        $sql       = 'SELECT * FROM Promotions WHERE promotionName = '.$name;
        $stmt      = $this->database->query($sql);
        $row       = $stmt->fetch();
        $promotion = new Domain\\Promotion($row['id'], $row['name'], $row['image'], $row['imageType']);

        return $promotion;

    }//end fetchByName()


    public function fetchById($id)
    {
        $sql       = 'SELECT * FROM Promotions WHERE promotionId = '.$id;
        $stmt      = $this->database->query($sql);
        $row       = $stmt->fetch();
        $promotion = new Domain\\Promotion($row['id'], $row['name'], $row['image'], $row['imageType']);

        return $promotion;

    }//end fetchById()


}//end class

Now this is working as I expected it to (though some sections are clearly unfinished), and seems as though it would be fairly easy to build out the rest of my controllers/models in the same way. Is there anything I am doing fundamentally wrong?

My next major stumbling block is my view.

Question 3: How do I build out my view capability to allow me to load multiple sections of data on a single page?

I will end up with individual controllers/models to handle promotions, events, matches & competitors and would want to bring them all in one the main landing page. I have looked at the Composite View pattern but am having trouble finding any material online that is easy to understand how to implement. Should I set up a HomeController that gets data from multiple models (the model) and have a single view to inject it all in to?

Thanks again, this has been an excellent learning exercise so far.

With respect to question 3, your first post indicates that this will be an ajax driven application. So each section of data will generate their own ajax call and get just the information it needs. This will simplify the server side code because you will never need to deal with multiple unrelated data sets.

So the answer to Q3 is: You don’t.

If you’re looking to get suggestions on your code, it might be worth posting a link to your project over at SitePoints new code review forum.

Your Loader object is perhaps a little confusingly named, as it’s actually a factory rather than a loader. I think you might find this example project interesting - it does something similar, with an [URL=“https://github.com/thePHPcc/bankaccount/blob/master/src/application/Factory.php”]application factory that creates controllers and mappers etc.

And what about user’s who have JS disabled?

Users with javascript disabled would not be able to use an ajax driven application.

there’s often very little reason other than developer laziness for building a site that doesn’t work with JS disabled. If you stick to the principle of progressive enhancement you make your site accessible to all users.

I freely admit to being lazy. Why work harder than you have to?

Progressive enhancement works fine for doing things like adding a pop up calendar to an input element. No js mean no popup but the user can still enter a value. Best of all, you can achieve progressive enhancement without any additional effort on the part of the developer.

But adding ajax to the mix changes things completely. Want to load option values for a select element dynamically? Easy with ajax. But wait. No js. How the heck am I supposed to get the options now? Maybe I’ll always send the options or maybe I’ll add some sort of load options button or maybe do some kind of funky js detection. Way to much work.

Us lazy developers would simple write two front ends. One for js and one (if needed) without js.

This is particular true now that full fledged js application frameworks (such as AngularJS) have now reached critical mass. Progressive enhancements for these frameworks is not even an option.

Fair point, I do agree there are situations where non-JS support would be unfeasible. I just think there are way too many sites out there where the developers haven’t even tried.

Thanks ahundiak. I was planning to work through this in stages and the Ajax part was coming later, but may as well tackle this now. The same problem applies in that I have not been able to find a clear explanation on how to allow each section to update itself when the first page loads rather than the user selecting some item. For the user selection I imagine it is straight forward in initiating an Ajax call to a url that will invoke a controller that will then update the specific elements with data from the view?

Thanks again fretburner, lots of good stuff here. The Bank Account example project gives me lots of ideas and I’ll be looking to rewrite a chunk of my code based off this as it seems infinitely cleaner.

As for excluding users without JS; this is a project for myself mostly and will have a very minute user base. I need to head up a project in my organization shortly that will be building an Ajax/HTML5 front end for an internal application and wanted to understand how to approach it before we start so set up this pet-project for that reason. I agree entirely in ensuring a public website is completely accessible. :slight_smile:

Looks like you are making some great progress, I definitely love to see more users here asking questions about OOP, MVC, ORM or architecture design since its good for me to learn and experience too. One thing I notice is that your model is somewhat different from a standard domain model, and you have used the phrase ‘view_model’ for it. Are you sure your approach is not MVVM? In a typical MVC application its rare to see models creating and using mapper objects, and in fact a traditional MVC model should not even be aware of the existence of a mapper(since technically, they belong to different layers of your application). I am curious at whether you have an explanation for what you are doing, its kinda interesting I must say. Or are you following some kind of guideline from an advanced PHP tutorial?

On the other hand, I have one suggestion for your loader class’ createController() method. You have a set of duplicate code that creates an Error Controller, they trigger when a series of if conditions returns false. Duplicate code like this makes your program less organized and less maintainable, one solution to the problem is to create a helper method in your loader class that checks these if conditions and return a boolean true or false value. This way the createController() only needs to run the helper method and checks if it returns true or false, which creates the appropriate controller object or an error controller, heres how it may look like, you may also use try/catch exception here since an invalid controller/action is clearly an error that needs to be handled somehow.


public function createController(){
    return ($this->validateController())?$this->_controller($this->_action, $this->_urlvalues):new Controller\\ErrorController('invalidAction', $this->_urlvalues); 
}

public function validateController(){
    if(class_exists($this->_controller)){
        $parents = class_parents($this->_controller); 
        if(in_array('FightSportsDB\\\\App\\\\Controller\\\\BaseController', $parents) and method_exists($this->_controller, $this->_action))  return TRUE;       
    } 
    return FALSE;
}

At last but not least, I personally disagree with the fact that a site has to check for non-javascript users. In modern web era javascript is pretty much enabled on every client except for the very few who refuse to move on or are way too cautious on potential security issues. I dont think its necessary to compensate for these users, just like there is no need for a modern PHP software to worry about PHP 4 compatibility. Correct me if I was wrong, but I dont consider the number of non-javascript users are so vast that one needs to invest a huge amount of time, energy and possibly money on handling them properly.

There are significant differences between ajax applications and non-ajax applications. For the non-ajax application, the server has the responsibility to put together a complete html page for each request. Lots of state. Lots of template processing, etc. With ajax, much of the application can be shifted to the client. The server only needs to serve up or process bits of json on request. So it’s probably not a bad idea to focus on the ajax side first since using ajax can really change your entire application design.

As you say, implementing individual ajax requests is easy enough. The challenge comes in wiring together all the little bits and pieces necessary to make a smooth application. And this is where the newer javascript frameworks (as opposed to libraries like jquery) come in. You might consider spending some time with AngularJs 1.2 (http://angularjs.org/). The web site has a number of introductory videos as well as a fairly decent tutorial show how a simple ajax application can work. The only bad thing about the tutorial is that the very first page is a bit confusing and talks about installing Node.js and karma for running the unit tests. You don’t need any of that to get started. Just pull the code from github and point your browser to index.html.

Also some nice basic videos at: https://egghead.io/lessons/angularjs-binding

The point is that once you start to understand ajax based applications, many of your server side questions tend to go away. The server really only needs to handle bits of json for the most part. The page loading issues and what not become moot.

Sorry for the late reply, plenty to absorb here.

To be honest I’ve just been fudging this together with bits and pieces I’m finding in articles, tutorials and sample code and then trying to keep it clean. I don’t yet understand the difference between MVC and MVVM but I have controllers so does that make it MVVMC? :lol:

Thanks for the suggestions. I am going to rethink this project based on ahundiak’s comment above but will definitely keep this in mind.

Thank you! I’ve spent the last few days trying to get my head around AngularJS and it looks perfect for what I’m wanting to achieve. I’m going to look at redesigning the server side part of my application to focus purely on the API and start again from the front back in terms of UI and how to make that work with AngularJS.

Next step is to plan and build out the API and will do this using a MVC pattern, which I think I’m clear on how to implement. Will post back in a few days with an update as to how I go.

I really appreciate the ongoing feedback.