Wednesday 11 April 2012

MVC IN PHP

MVC stands for Model View Controller. It is a presentation pattern first described by a Smalltalk programmer called Trygve Reenskaug in late 70s while working at Xerox PARC. Today, over 30 years later, it is most influential presentation pattern used in GUI design and web applications. In this tutorial I am going to describe the MVC pattern and show you a simple implementation in PHP.

About the MVC

MVC has three distinctive roles: Model, View and Controller.
  • Model - represents applications behavior and data. It is also commonly known as the domain. The domain represents the problem you are trying to solve.
  • View - represents the presentation. It can query the model about its state and generate the presentation of the model objects.
  • Controller - represents applications workflow, it processes the request parameters and decides what to do based on them. This, usually, involves manipulating the model and displaying the data in the selected view.
MVC diagram
As I think about the MVC pattern I can see only one negative aspect and several positive aspects. The negative is that the MVC pattern introduces complexity into the project which can be a burden for simple applications, but as your application grows this negative aspect is over weighted by positive aspects. I am going to describe few positive aspects I can think of.
MVC separates the model from the view, that is, the data from its representation. The separation of a model from its representation is one of the basic rules of a good software design. When you separate a model from its representation you can easily add many different representations of the same model. For example, a web application typically has HTML representation, used by web browsers, and JSON representation used by JavaScript AJAX clients.
Distinctive MVC roles allows us to better distribute manpower on a big project. For example, domain and database experts can work on a model while web designers work on a view. Because each part is developed by specialized developers, an application can be of better quality. This also affects development time, projects can be build quicker because specialized developers can work simultaneously, each in their area of expertise, without affecting other developers work (to much).
Maintenance and upgrades of a complex application are easier because a developer can look at the application as a series of "modules" each consisting of Model, View and the Controller part. A developer can modify one "module" and does not need to worry that the changes he introduced will affect other parts of the system (I believe that this is called separation of concerns). Also, when adding new functionality to the application, a developer can simply create a new "module".

Example application

For this tutorial I created simple contacts manager which supports basic CRUD operations. User can add a new contact, delete it, display a contact detail and display list of all contacts. I will refer to these operations as actions. Therefore, this application has four different actions: add contact, delete contact, show contact and list contacts. You can see the result of the list contacts action in the screenshot below:
Contacts manager example application

The impementation

You can download complete source code of the application from here.
The implementation consists of the index.php file and several files placed in the model, view and controller directories:
MVC tutorial project layout

The index.php script is central access point, all requests go through it.
The controller is defined in the controller/ContactsController.php file.
Application views are defined in the view directory: contact-form.php is responsible for displaying "Add new contact" form to the user, contact.php is responsible for displaying contact details, contacts.php is responsible for displaying the contacts list and error.php is responsible for diplaying errors.
The model is defined in the model directory. It consist of three parts: ContactsGateway is a table data gateway to the database table I'll show you later, the ContactsService object defines the model API that is used by the controller. ValidationException is an exception thrown from the model and catched by the controller in case of any validation errors. Using the ValidationException the model can alert the controller about validation errors and the controller can pass them to the view so they can be displayed.
The model
Before I explain source code, you must know something about the model. The model has a single entity - Contact which is persisted in the contacts table. The Contact has no behaviour so I used SQL table structure to define it:
?
1
2
3
4
5
6
7
8
CREATE TABLE `contacts` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(128) NOT NULL,
    `phone` varchar(64) DEFAULT NULL,
    `email` varchar(255) DEFAULT NULL,
    `address` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`)
)
There is no class in the model that represents the Contact entity, instead I used standard PHP objects automatically created from the database record (I know, it is not very OO but it was quick).
What can we do with that incredible model? Here is the public interface of the ContactsService class where you can see all the features of the model:
?
1
2
3
4
5
6
7
8
9
10
11
12
class ContactsService {
     
    public function getAllContacts($order) ...
     
    public function getContact($id) ...
     
    // throws ValidationException if $name is not set
    public function createNewContact( $name, $phone, $email, $address ) ...
     
    public function deleteContact( $id ) ...
     
}
Please take a look at the source code in the ContactsService.php file to see implementation details. I will not talk about implementation of these methods in this tutorial because they are simple and easy to understand.
ContactsService object does not work with the database directly, instead it uses the ContactsGateway object which in return issues queries to the database. This technique is called Table Data Gateway.
The source
First let's look at the index.php source:
?
1
2
3
4
5
6
7
<?php
require_once 'controller/ContactsController.php';
 
$controller = new ContactsController();
$controller->handleRequest();
 
?>
This script has simple role, it instantiates the controller object and hands it control over the application via the handleRequest method.
All requests must go through the index.php script because MVC requires that all requests are handled by the controller which is called from the index.php script. Web MVC applications usually redirects all requests to go through the index.php which can be done in server configuration.
Let's look at the handleRequest method of the controller.
?
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
class ContactsController {
 
...
 
     public function handleRequest() {
        $op = isset($_GET['op'])?$_GET['op']:NULL;
        try {
            if ( !$op || $op == 'list' ) {
                $this->listContacts();
            } elseif ( $op == 'new' ) {
                $this->saveContact();
            } elseif ( $op == 'delete' ) {
                $this->deleteContact();
            } elseif ( $op == 'show' ) {
                $this->showContact();
            } else {
                $this->showError("Page not found", "Page for operation ".$op." was not found!");
            }
        } catch ( Exception $e ) {
            $this->showError("Application error", $e->getMessage());
        }
    }
 
...
 
}
The handleRequest method acts as a dispatcher for the actions. The method decides which action should it invoke based on a value of the HTTP GET "op" parameter and invokes the method that implements the action. If any exception is thrown from the action methods the handleRequest method catches them and prints the error message.
Now, let's look at the action methods:
?
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
53
54
55
56
57
58
59
60
61
62
63
64
class ContactsController {
 
...
 
    public function listContacts() {
        $orderby = isset($_GET['orderby'])?$_GET['orderby']:NULL;
        $contacts = $this->contactsService->getAllContacts($orderby);
        include 'view/contacts.php';
    }
     
    public function deleteContact() {
        $id = isset($_GET['id'])?$_GET['id']:NULL;
        if ( !$id ) {
            throw new Exception('Internal error.');
        }
         
        $this->contactsService->deleteContact($id);
         
        $this->redirect('index.php');
    }
     
    public function showContact() {
        $id = isset($_GET['id'])?$_GET['id']:NULL;
        if ( !$id ) {
            throw new Exception('Internal error.');
        }
        $contact = $this->contactsService->getContact($id);
         
        include_once 'view/contact.php';
    }
 
    public function saveContact() {
        
        $title = 'Add new contact';
         
        $name = '';
        $phone = '';
        $email = '';
        $address = '';
        
        $errors = array();
         
        if ( isset($_POST['form-submitted']) ) {
             
            $name       = isset($_POST['name']) ?   $_POST['name']  :NULL;
            $phone      = isset($_POST['phone'])?   $_POST['phone'] :NULL;
            $email      = isset($_POST['email'])?   $_POST['email'] :NULL;
            $address    = isset($_POST['address'])? $_POST['address']:NULL;
             
            try {
                $this->contactsService->createNewContact($name, $phone, $email, $address);
                $this->redirect('index.php');
                return;
            } catch (ValidationException $e) {
                $errors = $e->getErrors();
            }
        }
         
        include 'view/contact-form.php';
    }
 
...
 
}
First, we have the listContacts method which has a simple workflow, it reads a parameter required for sorting the contacts, gets sorted contacts from the model, stores it in the contacts variable and, finally, includes the view.
Second, we have the deleteContact method which reads an id of the contact and tells the model to delete it. Finally, it redirects the user back to the index.php script which in return invokes the list contacts action. The delete contact action can not work without the id parameter, so, the method will throw an exception if the id of a contact is not set.
Third method is the showContact method which is similar to the deleteContact method so I will not waste space explaining it.
Finally, the saveContact method displays the "Add new contact" form, or, processes the data passed from the form if it was submitted. If any ValidationException occurred in the model, errors are collected from the exception and passed to the view. Ideally the view should display it (and indeed it does, as you will see soon).
Now let's look at some views, first I want to show you the contacts.php view which is straightforward:
?
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
...
   
     <div><a href="index.php?op=new">Add new contact</a></div>
        <table class="contacts" border="0" cellpadding="0" cellspacing="0">
            <thead>
                <tr>
                    <th><a href="?orderby=name">Name</a></th>
                    <th><a href="?orderby=phone">Phone</a></th>
                    <th><a href="?orderby=email">Email</a></th>
                    <th><a href="?orderby=address">Address</a></th>
                    <th>&nbsp;</th>
                </tr>
            </thead>
            <tbody>
            <?php foreach ($contacts as $contact): ?>
                <tr>
                    <td><a href="index.php?op=show&id=<?php print $contact->id; ?>"><?php print htmlentities($contact->name); ?></a></td>
                    <td><?php print htmlentities($contact->phone); ?></td>
                    <td><?php print htmlentities($contact->email); ?></td>
                    <td><?php print htmlentities($contact->address); ?></td>
                    <td><a href="index.php?op=delete&id=<?php print $contact->id; ?>">delete</a></td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
 
...
The contacts.php view, which is used by the list contacts action, requires the contacts variable to be filled with contact objects. The variable is filled in the listContacts method and passed to the view using the PHP scoping rules and then the view uses the data from it to display the contacts as an HTML table.
Scripts like the contacts.php script are called templates. With a template developer defines a fixed structure that is filled with variable values ​ ​from the application (at run-time) and then presented to the user. Templates contain only presentation logic, because of this, they are understandable by non-programmers (designers, ...) which makes them quite usefull and frequently used in web applications.
Now let's take a look at more complex view, the contact-form.php view:
?
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
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>
        <?php print htmlentities($title) ?>
        </title>
    </head>
    <body>
        <?php
        if ( $errors ) {
            print '<ul class="errors">';
            foreach ( $errors as $field => $error ) {
                print '<li>'.htmlentities($error).'</li>';
            }
            print '</ul>';
        }
        ?>
        <form method="POST" action="">
            <label for="name">Name:</label><br/>
            <input type="text" name="name" value="<?php print htmlentities($name) ?>"/>
            <br/>
             
            <label for="phone">Phone:</label><br/>
            <input type="text" name="phone" value="<?php print htmlentities($phone) ?>"/>
            <br/>
            <label for="email">Email:</label><br/>
            <input type="text" name="email" value="<?php print htmlentities($email) ?>" />
            <br/>
            <label for="address">Adress:</label><br/>
            <textarea name="address"><?php print htmlentities($address) ?></textarea>
            <br/>
            <input type="hidden" name="form-submitted" value="1" />
            <input type="submit" value="Submit" />
        </form>
         
    </body>
</html>
This view, also an template, is used by the add contact action and it displays the "Add new contact" form to the user. All variables required by the template are filled in the saveContact action method including the errors variable which, if set, is used by the template to display errors that occurred in the model while trying to create a new contact so the user can fix invalid data causing them.
This template shows us usefulness of controller-view separation, because, without it we would put controller logic from the saveContact method into the contact-form.php script. I say script because if a template contains any controller logic in it, it is no more a template. 

No comments:

Post a Comment