Alexander Steshenko | On Software Engineering

Model-View-Controller for BasicCrm

Ideally, in a project where you choose to apply DDD, MVC is just a way to deliver the Model to end-users of your app. Nowadays the look-and-feel matters a lot in most of the applications. Even so, MVC is just a wrapper for the Model, a layer between the model and website visitors.

Zend Framework

I chose Zend Framework as main helper for building the MVC layer. I’m not going to use Zend\Application component because there is already ServiceLocator which covers most of the functionality of Zend\Application that is needed.

Three things are needed to get it working:

  1. Add the public directory (web server’s document root) and the entry point for the web application facade.
  2. Configure and run Zend\Controller\Front in the new entry point.
  3. Create a controller and a view.

Entry point

The entry point is index.php file while .htaccess routes all requests to index.php so the framework takes care of them. Put both to the /public directory and point the web server’s document root right there.

Configuration of the server depends on the server software used, in my case I just configured the Apache’s virtual hosts like this:

<VirtualHost 127.0.0.1:80>
    DocumentRoot "D:\www/basiccrm.local/public/"
    ServerName basiccrm.local
    …
</VirtualHost>

This is what I’ve got so far in the app directory:

The .htaccess file utilizing mod_rewrite Apache module is as simple as this:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]

The entry point, index.php file should essentially do the same as our entry point for unit tests suite, i.e.

“Tests suite’s entry point as any entry point of our app needs to define APPLICATION_PATH constant, setup include path and a simple autoloader so models can be found and loaded.”

and run the web application. Here is how it looks:

/**
 * MVC application entry point
 */

define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));

set_include_path(implode(PATH_SEPARATOR, array(
    APPLICATION_PATH . '/models',
    get_include_path()))
);

// Registering the autoloader
require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance()->setFallbackAutoloader(true);

// Running the front controller
$frontController = ServiceLocator::getFrontController();
$frontController->dispatch();

Configuring Zend_Controller_Front

I’ll create getFrontController function in ServiceLocator which configures and returns an instance of Zend_Controller_Front. I need to pass the path to the directory where the controllers will be located:

public static function getFrontController()
{
    if (self::$frontController === null) {
        self::$frontController = Zend_Controller_Front::getInstance()
            ->setControllerDirectory(APPLICATION_PATH . '/controllers');
    }

    return self::$frontController;
}

Creating a controller and a view

The default action that is triggered when a visitor opens the main page of the site in Zend Framework is “indexAction” in “IndexController” the view script for which must be located in /application/views/scripts/index/index.phtml.

Let’s create them:

indexAction can be empty for now

class IndexController extends Zend_Controller_Action
{
    public function indexAction()
    {
    }
}

I put some “Lorem Ipsum” into the view script: https://github.com/lcf/BasicCRM/blob/master/application/views/scripts/index/index.phtml

To check whether we did everything correctly let’s open the site:

Design

I downloaded a free html template with styles and images, from here: http://www.mollio.org/. What is often needed is to have the Two Step View (http://martinfowler.com/eaaCatalog/twoStepView.html) - a common layout for all pages of the web site. Zend Framework has Zend\Layout for that. To use Zend\Layout I have to register a plugin with Zend_Controller_Front and add an action helper to the stack of helpers. We can do it by calling one static method of Zend_Layout:

public static function getFrontController()
{
    if (self::$frontController === null) {
        self::$frontController = Zend_Controller_Front::getInstance()
            ->setControllerDirectory(APPLICATION_PATH . '/controllers');
        Zend_Layout::startMvc(
            array('layoutPath' => APPLICATION_PATH . '/views/layouts', 'layout' => 'index'));
    }

    return self::$frontController;
}

the main layout file /application/views/layouts/index.phtml:  https://github.com/lcf/BasicCRM/blob/master/application/views/layouts/index.phtml

Once you’ve put the styles and images into the Document Root (/public directory) you’ll see the new version of the site’s index page:

Register a company

To register a company, a html form is needed that will let us enter our company data, choose the subscription plan and so on. A controller to process that data and pass it to the model is already implemented. What’s left is error handling.

Errors handling

Let’s start with the ErrorController and errorAction. Those are default controller/action for processing errors in Zend Framework. Whenever an exception is thrown and not caught during the dispatch process this controller will be responsible for telling the user what happened.

The main idea of the ErrorController is about letting users know the exact errors in case the exception is a DomainException. In all other non-domain specific cases it will just show “Application error” (for instance if our DB goes down for some reason).

Here is the code that does it:

class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');
        if ($errors->exception instanceof DomainException) {
            $message = $errors->exception->getMessage();
            if (!$message) {
                $message = 'Unknown error';
            }
        } else {
            $this->getResponse()->setHttpResponseCode(500);
            $message = 'Application error';
        }
        break;
        $this->view->assign('message', $message);
    }
}

The view script simply shows the message /application/views/scripts/error/error.phtml:

<h2>An error occurred</h2>
<p class="error">
    <?= $this->message ?>
</p>
<p>
    <a href="javascript:history.go(-1);">Go back</a>
    <a href="/">Home page</a>
</p>

This way we’re safe and can be sure that users will always know what they did wrong, even if a particular controller itself does not process exceptional situations at all.

The form

To get it working, I created CompanyController with empty registerAction and the view script accordingly: /applications/views/scripts/company/register.phtml. The form goes to the view https://github.com/lcf/BasicCRM/blob/master/application/views/scripts/company/register.phtml. The form will be accessible with the /company/register query string.

To let people get to registering companies from any page of the web site, I added a link to the left menu in the layout file and here is how the page with the form looks like:

Processing the form

The data that users enter into the form must be passed to the CompanyService::registerCompany() function. Let’s add the code that does it to the CompanyController, registerAction

class CompanyController extends Zend_Controller_Action
{
    public function registerAction()
    {
        if ($this->_request->isPost()) {
            $companyService = ServiceLocator::getCompanyService();
            $companyService->registerCompany(
                $this->_getParam('subscription-plan'),
                $this->_getParam('name'),
                $this->_getParam('admin-name'),
                $this->_getParam('admin-email'),
                $this->_getParam('password'),
                $this->_getParam('confirm-password')
            );
            $this->_redirect('/company/register-success');
        }
    }

    public function registerSuccessAction()
    {
        // just view here
    }
}

After successful registration it redirects us to the registerSuccessAction which simply shows a “Thank you” message.

If any error happens with the data we entered or during the registration process the error controller will take control and show what is exactly wrong:

Conclusion

I published the results here http://basiccrm.lcf.name. Also, check out the github repository for all the new code : https://github.com/lcf/BasicCRM


Comments and feedback are much appreciated. See contact details here