Tools Blog Learn Quizzes Smile API Log In / Sign Up
Tools Blog Learn Quizzes Smile API Log In / Sign Up
« Return to the tutorials list
We have updated our privacy policy to let you know that we use cookies to personalise content and ads. We also use cookies to analyse our traffic and we share information about your use of our site and application with our advertising and analytics partners. By using this website or our application you agree to our use of cookies. Learn more about the way this website uses cookies or remove this message.

Develop your own MVC application in PHP

October 14, 2016 Difficulty: 35 / 50 Tweet
railroad-traffic-controller-lights

In this tutorial I will show you how easy it is to create your own MVC application in PHP without using a framework. MVC or model-view-controller is an architectural pattern which is a fancy phrase for 'a method of structuring your application' so that you can write reusable code and maintainable code.

While the model handles data, the controller is responsible for directing the application flow through interacting with the model and sending data to the view. In my view, an MVC is not complete if it can't handle more than one view, possibly even at the same time (Ex: normal request versus Ajax).

In other words ...

Model - "translates" or "models" any structured object in the application to any persistent storage (database, file, whatever).

Controller - Answers one question - What am I doing? The controller does what needs to be done affecting as many "models" as needed ... without necessarily knowing about how the models are being "stored" in the database. The controller would also feed data (coming from the model) to the view, but again, the controller knows nothing about how the view will show that data.
Example: Send an invoice to a user and and mark that as "pending payment".
The model is going to actually mark the invoice, but it is the controller who will tell it to do that.


Example: Display the users invoices
The controller would get a set of invoices from the model and push a data set to the view, regardless of what the view will actually do to that data... show it as an HTML table or as a JSON object.

View - This answers only one question ... what do I show to the user ? How do I render the result ?

File structure

For the purpose of this tiny project you will need to have some basic knowledge of Composer - the PHP package manager and be able to execute basic CLI commands.

Our file structure will be like this:

  • public
    Stores one index.php file - the entry point of our app and various other static assets like CSS, images, etc ... This should be the only folder that is accessible from the web... all other PHP files shouldn't exist in the publicly available virtual host folder.
  • ../app Our main application folder.
  • ../app/bootstrap.php A file that initializes the whole process and can in turn reference various configuration files or can store the configuration in it. Also, it can call a "routes" file or it can store the routes in it. For the purpose of this tutorial we won't have external configuration files or routes, we will put them both in bootstrap.php.
  • ../app/Controllers/* This is where we store our controller files. I am using a naming convention - my files will be formed from [ACTION] + [CONTROLLER]. Like this: HomeController.php, RegistrationController.php, etc...
  • ../app/Models/* This is where we store our model files. I am using the same convention as above, but this time file names relate to objects I use in the app - UsersModel.php, InvoicesModel.php, etc...
  • ../app/Views/* This is where we display stuff. For the purpose of this I only have one file here -> default.php ... which will list whatever I send it from the controller.
  • ../app/vendor/* The default folder for composer included packages. We will include "guzzlehttp/guzzle": "^6.2" ... which we will use to fetch data from remote API endpoints. However, you can just include whatever packages you want/need in your app.

Let's dive into it

Below is our bootstrap.php file. It has the following role(s):

  • Define a few globally available configuration things
  • Auto-load our files
  • Route our app to the appropriate Controller

    
        namespace Codepunker\InvoicesApp;

        define('BASE', __DIR__);
        define('BASE_URI', 'http://InvoicesApp.co');
        define('ASSETS_URI', 'http://InvoicesApp.co');
    
        //load all our classes as we instantiate them following the guidance of the name spaces.
        spl_autoload_register(function($class) {
            require_once __DIR__ . str_replace(['Codepunker\\InvoicesApp', '\\'], ['', '/'], $class) . '.php';
        });
        
        // include the composer autoloader
        require 'vendor/autoload.php';

        $req = $_SERVER['REQUEST_URI'];
        $qs = $_SERVER['QUERY_STRING'];
        
        // this is our pseudo-router ... we don't have a set of classes that interpret requests so we just rely on the plain old super globals

        if(!empty($qs)) {
            $req = substr($req, 0, strpos($req, '?')); // this is to be expanded by you guys...
        }

        switch ($req) {
            case ('/'):
                $buff = new Controllers\HomeController([ 
                    BASE . '/Views/default.php'
                ]);
                break;
            case ('/about'):
                $buff = new Controllers\AboutController([ 
                    BASE . '/Views/default.php'
                ]);
                break;
            default:
                $buff = new Controllers\DefaultController([ 
                    BASE . '/Views/default.php'
                ]);
                break;
        }

        echo $buff->out();        
    

As you've probably noticed we take into account only two routes: '/' and '/about', the third one is just a default which is beyond the scope of this... If you don't want to create it, just kill the script if none of your routes are hit.

Also, from the bootstrap file you can see there's only one view and we pass that to the controller as an array - you can expand on it and add as many view files as you want, depending on how you like to structure your templates.

Create a base controller which will act as a "template" for all other controllers

This base controller I'm proposing is basically a "template" that will be used by the other controllers, thus forcing us to respect a particular standard we've created.

    
    namespace Codepunker\InvoicesApp\Controllers;

    /**
    * Our base controller
    */
    class BaseController
    {
        protected $data;
        private $content;

        public function __construct(array $views)
        {
            $this->loadView($views);
        }

        /**
         * @return array data passed to the view
         */
        protected function getData(): array {
            return $this->data;
        }

        /**
         *  
         * @param  array  $views an array of views to be loaded
         * @return string the compiled view
         */
        protected function loadView(array $views) {
            ob_start();
                $data = $this->getData(); //data needed in the view
                foreach ($views as $k => $view) {
                    include $view;
                }
                $content = ob_get_contents();
            ob_end_clean();
            $this->content = $content;
        }

        /**
         * get's the buffer and returns it
         * @return string the buffer
         */
        public function out() : string {
            return $this->content;
        }
    }
    

Next we will create a controller for the first route in bootstrap.php -> "/". Our home controller will look something like this.

    
    namespace Codepunker\InvoicesApp\Controllers;

    use \GuzzleHttp\Client as HttpClient;
    use Codepunker\InvoicesApp\Models\InvoicesModel as InvoicesModel;

    //shows some data on the homepage
    class HomeController extends BaseController
    {
        public function __construct(array $views)
        {
            $this->getHomePageData();
            parent::__construct($views);
        }
    
        //retrieve data from remote endpoint and store it using the model
        private function getHomePageData()
        {
            $client = new HttpClient();
            try {
                $res = $client->request('GET', 'https://www.remoteendpoint.com', [
                    'headers' => [
                        'User-Agent' => 'testing/1.0',
                        'Accept'     => 'application/json',
                    ]
                ]);
                
                $data = json_decode( $res->getBody() );
                $model = new InvoicesModel($data);
                $model->store();
                return $data;
            } catch (Exception $e) {
                echo "Sorry for the inconvenience";
            }
        }
    }
    

Now our view will be just a basic listing.

    
        foreach ($data as $k => $v) :
            //show me...
        endforeach;
    

The model

    
    namespace Codepunker\InvoicesApp\Models;

    //shows some data on the homepage
    class HomeController extends BaseController
    {
        protected $data;
        protected $table_name = "invoices_table";

        function __construct(array $data)
        {
            $this->data = $data;
        }

        public function store()
        {
            // of course in your app you'll have some sort of DB abstraction layer...
            try{
                $db = new \PDO("mysql:host=localhost;dbname=mydb;charset=utf8","username","pw");
                foreach($this->data as $k=>$v) {
                    $stmt = $db->prepare("INSERT INTO {$this->table_name} VALUES (NULL, :param, :anotherparam)");
                    $stmt->bindParam(':param', $v->param);
                    $stmt->bindParam(':anotherparam', $v->anotherparam);
                    $stmt->execute();
                }
            } catch(PDOException  $e ) {
                echo "Error: ".$e;
            }
        }
    }
    

Hope this tutorial will help demystify the MVC architectural pattern and will allow you to better understand how PHP frameworks are built. Thanks for reading through.

Here's an older version of the same article I've written in 2014... no composer, no name spaces but basically the same concept.

This part of the article (everything below) is old. Be careful! It might contain outdated information.

In this tutorial I will show you how easy it is to create your own MVC application in PHP without using a framework. MVC or model-view-controller is an architectural pattern which is a fancy phrase for 'a method of structuring your code'. While the model handles data and application logic, the controller is responsible for directing the entire application flow by interpreting input and sending directives to the model or the view.

The code listed in this tutorial is not meant to be used in a production environment. It is only meant to show you how an MVC application can be structured. I have commented almost each line of code so that you can understand what's going on. The final result is a "Hello World" like application that displays a message to the user to which I have also added a basic dynamic function - A form is submitted and success / failure messages are displayed depending on what is sent by the user.

The file structure

  
    ..
    application_folder
      classes // holds additional needed classes
        request.class.php // a class to get and set request values
        session.class.php // a class to get and set session values
        ... other classes that you might need // not in the scope of this tutorial
      templates //holds the html
      index.php //starts the application
      boot.php //serves as a controller
      model.php //serves as a model
      view.php //serves as a view
    ?>
  

The Launcher

Your index.php file will serve as the starting point for any request. It will initialize the application by loading the controller.

Replace session name and the 'your_app_name' constant with whatever values you want. Change 'your_app_name' in all the files. It is used to restrict direct access to any file other than index.php.

  
    <?php 
    if(session_id() == '') //initialize the session if not already set
    {
      session_name("YOUR_SESSION_NAME");
      session_start();
    }

    define("your_app_name", ""); //app name
    define('APP_BASE', dirname(__FILE__)); //server base folder for your app

    // the base url for your app
    $base = 'http://' . $_SERVER['HTTP_HOST'];
    $base .= rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\');
    define('APP_URL', $base);

    //load the controller
    require_once(APP_BASE . '/boot.php');

    //load any other class that's needed by simply initializing it with new 
    //... no need to require / include files every time
    spl_autoload_register(function ($class) {
      include 'classes/' . $class . '.class.php';
    });

    //init
    $app = new Boot();
  

The Controller

  
    <?php
    defined("your_app_name") or die; // don't allow access to this file directly

    /*
    *  The main application controller
    *  1. If an action comes in the request it will call the method with the same name from the model and then redirect
    *  2. If no action comes it will load the view which in turn will render the page
    */
    class Boot
    {
      public $action;
      public $page;

      public function __construct()
      {
        $this->page = (Request::get('page')) ?: "default";
        $this->action = Request::get('action');

        /*
        * action is something we use to define wether we're executing 
        * a task followed by a redirect or we're just displaying the requested page
        */
        if($this->action)
        {
          $method = $this->action;
          require_once APP_BASE . '/model.php';
          $model = new Model();
          if(method_exists($model, $method))
          {
            /*
            * The model returns an array with three values 
            *   0 => true/false
            *   1 => "a message"
            *   2 => "a page where to redirect a user after the processing is done"
            */
            $result = $model->$method();
            if($result[0]!==true) /*Success*/
              Session::set('error', $result[1]);
            else /*Fail*/
              Session::set('message', $result[1]);

            header('Location: ' . $result[2]); //redirect
            die;
          }
          die('Not allowed'); //kill the script if the submitted action is not a method in the model
        }
        else
        {
          ob_start();
            echo $this->renderPage();
          ob_get_contents();
          ob_end_flush();
        }
      }
      
      protected function renderPage()
      {
        if($this->page)
        {
          require_once(APP_BASE."/view.php");
          $view = new View();
          return $view->loadHTML($this->page);
        }
        else
          die;
      }
    }
  

The View

  
    <?php
    defined("your_app_name") or die; // don't allow access to this file directly
    class View
    {
      public function loadHTML($page)
      {
        if($page=="default")
        {
          $template = APP_BASE . '/templates/template.php';
          require_once APP_BASE . '/model.php';
          $model    = new Model();
          $this->theMessage = $model->displayMessage();
          if(file_exists($template) && is_readable($template))
            require $template;
          else
            die;
        }
        else
        {
          $template = APP_BASE . '/templates/'.$page.'.php'; //other pages / templates
          if(file_exists($template) && is_readable($template))
            require $template;
          else
            die;
        }
      }
    }
  

The Model

  
    <?php
    defined("your_app_name") or die; // don't allow access to this file directly
    class Model
    {
      public function parseGreeting() //called from the controller
      {
        $greeting = Request::get('greeting');
        if(in_array($greeting, array('Hello', 'Bye')))
          return array(true, 'Greeting allowed', APP_URL);
        else
          return array(false, 'Greeting not allowed', APP_URL);
      }

      public function displayMessage() //called from the view
      {
        $theMessage = 'I am a message';
        return $theMessage;
      }
    }
  

The Template

  
    <?php defined("your_app_name") or die; // don't allow access to this file directly ?>
    <!DOCTYPE html>
    <html>
    <head>
      <title>A demo page for the CodePunker MVC</title>
    </head>
    <body>
      <h1>
        <?php echo $this->theMessage; ?> that is loaded by the View Class, which calls a method in the model and then injects the result into the template.
      </h1>
      <?php 
          if(Session::get('error'))
          {
            echo '<h2 style="background:red; color:white">'.Session::get('error').'</h2>';
            Session::clear('error');
          }
      ?>
      <?php 
          if(Session::get('message'))
          {
            echo '<h2 style="background:green; color:white">'.Session::get('message').'</h2>';
            Session::clear('message');
          }
      ?>
      <form method="POST">
        <label>You are allowed to type 'Hello' or 'Bye' in the text box below:</label><br>
        <input name="greeting"><br>
        <input type="hidden" name="action" value="parseGreeting">
        <input type="submit" name="submit" value="Submit">
      </form> 
    </body>
    </html>
  

The Helper Classes for Session and Request

Below is the code used to get/set values from the Request and Session. However the two classes listed below ARE ONLY written for demonstration purposes. DON'T just copy/paste the code below, use it only as a starting point for you app.

  
    //Session.class.php
    <?php
    defined("your_app_name") or die; // don't allow access to this file directly

    class Session
    {
      static function set($key, $value)
      {
        $_SESSION[$key] = $value;
      }

      static function get($key)
      {
        if(!empty($_SESSION[$key]))
          return $_SESSION[$key];
        else
          return false;
      }

      static function clear($key)
      {
        if(isset($_SESSION[$key]))
          unset($_SESSION[$key]);

          return true;
      }
    }

    //Request.class.php
    <?php
    defined("your_app_name") or die; // don't allow access to this file directly

    class Request
    {
      static function get($key)
      {
        if(!empty($_REQUEST[$key]))
          return $_REQUEST[$key];
        else
          return false;
      }
    }
  
comments powered by Disqus

Better Docs For A Better Web - Mozilla Developer Network

Alerts

2017-02-16 - The exif_process_IFD_in_JPEG function in ext/exif/exif.c in PHP before 5.5.35, 5.6.x before 5.6.21, and 7.x before 7.0.6 does not validate IFD sizes, which allows remote attackers to cause a denial of service (out-of-bounds read) or possibly have unspecified other impact via crafted header data. Read more ...
2017-02-16 - The bcpowmod function in ext/bcmath/bcmath.c in PHP before 5.5.35, 5.6.x before 5.6.21, and 7.x before 7.0.6 accepts a negative integer for the scale argument, which allows remote attackers to cause a denial of service or possibly have unspecified other impact via a crafted call. Read more ...

See All Entries...