PHP: Understanding MVC Basics
Model-View-Control (MVC) is a software architecture pattern, originally formulated in the late 1970. It is built on the basis of separation of concerns – keeping the presentation of data separate from the methods that interact with the data.
It is widely used in web applications across multiple languages, from javascript to Ruby, PHP, ASP.Net, and others, enabling rapid development and deployment. In theory, a well-developed MVC system should allow front-end and back-end development teams to work on the same system without interfering, sharing or editing files either party is working on.
What Is MVC Structure
It’s important to note that there is no one way of implementing an MVC design. Typically however an MVC framework contains the main components alongside additional components, which may interact with external sources, and provide information to the main components, normally via the controller.
- Model:Manages data, business logic and rules of the application, independent of the view. Used to store and manipulate state, typically in a database and/or local storage, which is retrieved according to commands from the controller.
- View: Outputs information gathered by the controller, possibly via interaction with the model. It may be a single view, or amalgamation of views.
- Controller: Accepts input from the user and converts it to usable commands for the model, and information for the view
Additional components may include: Router, Request, and Response objects, for parsing user requests, dealing with user request data, and outputting view data respectively. One of the prevalent doctrines of an MVC framework is ‘Slim Controller, thick Model’, where the controller only contains what is required to access the required data from the model, and present to the view. The grunt work – business logic, data access & manipulation etc, is done within the model.
MVC in PHP architecture
The MVC pattern is a widely used programming technique in PHP web application development. It is at the heart of all the well known PHP frameworks, on which are built many Web Applications, and SaaS sites. Frameworks such as Laravel, Symphony, Yii, Zend, and CodeIgniter are notable examples.
A Basic MVC Framework
We’re going to go through the principles of MVC by developing a lightweight framework. We won’t be going into databases, however where and when this interaction could take place will be shown. As mentioned, there is no fixed structure for MVC, however there is always a similar filestructure with an index.php and folders for the controllers, models and views. Our basic file structure is:
|-index.php |-loader.php |-router.php |-front.php |-controller.php |-model.php |-.htaccess |- controller | - Home | - Blog |- model | - Blog |-View | - HomeIndex | - HomeExtra | - BlogIndex
The first point of contact between any Web Application and the user is via the browser url. To know which controller to access and which controller method to call – with or without arguments, we’ll need to set up the url structure. In this case we’ll use one that may be familiar to users of the OpenCart e-commerce platform:
http://url/?route=page/action&path=x e.g. http://url/?route=blog/index&path=2 or http://url/?route=page/action&path=x
The responsibility for this is tasked to additional components: Router and Front Controller. The router parses the url, and then passes responsibility for selecting the correct controller and controller method to the front controller.
Router
The router uses the set rules to parse the url and construct the required information for the router.
/** * Router * @access public final * @package MVC */ final class Router < /** * Route settings * * @var string $class * @access protected */ protected $route = array( 'class' =>'', 'method' => '', 'args' => '' ); /** * Class constructor * * @param string $route * @param array $args * @throws Exception */ public function __construct() < // get url parts: ?route=controller/action&path=arg1_arg2 $route = filter_input( INPUT_GET, 'route', FILTER_SANITIZE_STRING ); $path = filter_input( INPUT_GET, 'path', FILTER_SANITIZE_STRING ); // deconstruct route $route_parts = ( empty ( $route ) ) ? '' : explode( '/', $route ); // set controller, actions & args $page = ( empty( $route ) ) ? 'home' : trim( $route_parts[0] ); $action = ( empty( $route ) || !isset( $route_parts[1] ) ) ? 'index' : trim( $route_parts[1] ); $path = ( empty( $path ) && isset( $route_parts[2] ) ) ? trim( $route_parts[2] ) : $path; if ( is_file( APP_PATH . '/Controller/' . ucfirst( $page ) . '.php' ) ) < $this->class = $page; > else < throw new Exception( 'Error: Invalid Controller [' . ucfirst( $page ) . ']' ); >// Set method, args & view $this->method = $action; $this->args = trim( $path ); > /** * Set a class variable * * @param string $name * @param mixed $value * @access public */ public function __set( $name, $value ) < if ( array_key_exists( $name, $this->route ) ) < $this->route[$name] = $value; > > /** * Retrieve value by key * * @param string $name * @return mixed * @access public */ public function __get( $name ) < return ( array_key_exists( $name, $this->route ) ) ? $this->route[$name] : null; > >
Breaking it down, the first task is to get the route and path from the url. We use the PHP filter function for this, which adds a bit of sanitization to the input. We then break down the route into its component parts which are then translated into page & action, with fallback defaults.
We then check that the controller is valid before setting the class variables. In production MVC’s it’s probably better to revert to the default controller and index method.
Note that we use the __set & __get functions to set the main ‘class’, ‘method’, and ‘args’ variables. These magic methods are commonly used in PHP objects to store variables accessed externally.
Front Controller
The main grunt work of the MVC entry is done by the front controller. This takes the router variables, selects the required controller, and calls the requested controller method. For example if the url is ?route=home/extra then the controller is set to HomeController and the controller method ‘extra’. If no route argument is passed the controller defaults to HomeController and index method. If no action is passed e.g.?route=blog then the method defaults to index. If no path argument is set then the args are defaulted to empty. In our case we have used the Singleton pattern to create an instance of the front controller object.
/** * Front Controller * * @package MVC */ final class Front < /** * Holder for class instance * * @var object $instance * @access private static */ private static $instance; /** * Runtime error message * * @var string $error * @access private */ private $error; /** * Returns the instance of Front * * @return object */ public static function getInstance() < // test for instance object if (null === static::$instance) < static::$instance = new static(); >return static::$instance; > /** * Prevent creating a new instance * * @access protected */ protected function __construct() <> /** * Prevent cloning of the instance * * @return void * @access private */ private function __clone() <> /** * Prevent unserializing * * @return void * @access private */ private function __wakeup() <> /** * Run the controller * * @param string $action * @throws Core Exception * @access public */ public function run( Router $route ) < // execute action if ( $this->execute( $route ) === FALSE ) < throw new Exception ( $this->error ); > // ok, done. exit(0); > /** * Execute the controller method * * @param string $route * @access private */ private function execute( Router $route ) < // set paths & names $file = APP_PATH . '/Controller/' . ucfirst( $route->class ) . '.php'; $class = ucfirst( preg_replace('/[^a-zA-Z0-9]/', '', $route->class ) ) . 'Controller'; $args = ( empty( $route->args ) ) ? array() : explode( '|', $route->args ); // test for file if ( file_exists( $file ) ) < // include controller include_once( $file ); // create controller instance $controller = new $class( $route ); // check controller method is viable if ( is_callable( array( $controller, $route->method ) ) ) < call_user_func_array( array( $controller, $route->method ), $args ); > else < $this->error = 'Error: Bad Controller Method'; return FALSE; > > else < $this->error = 'Error: Controller File Not Found'; return FALSE; > return TRUE; > >
Again breaking it down, the front controller receives the Router object and uses it to set the controller file and class name. It then validates these, checking the controller file is accessible and the controller class method is callable. It creates the instance of the controller and calls the controller method with arguments. Here we throw an exception if something is wrong. A production MVC may be a bit more user friendly in how it handles issues.
Controller
The controller defines a common set of functionality to determine how it interacts with the model(s) and render the view. According to the principles of DRY these are placed into an abstract parent controller class of which all other controller classes are children. It requires that a common index function is defined by all children.
/** * Controller Class * * @package MVC */ abstract class Controller < /** * Controller name * * @var string * @access protected */ protected $name; /** * Controller view * * @var string * @access protected */ protected $view; /** * Holder for variable array * @access protected * @var array $viewData */ protected $viewData = array(); /** * Class constructor * * @param $router Router * @access public */ public function __construct( Router $route ) < $this->name = ucfirst( $route->class ); $this->view = ucfirst( $route->class ) . ucfirst( $route->method ); > /** * Set view data * * @param string $name * @param mixed $value * @access public */ public function __set( $name, $value ) < $this->viewData[$name] = $value; > /** * Get view data * * @param string $name * @return mixed * @access public */ public function __get( $name ) < return ( array_key_exists( $name, $this->viewData ) ) ? $this->viewData[$name] : null; > /** * Display View * * @access protected */ protected function displayView() < // set view file path $view = APP_PATH . '/View/' . $this->view . '.php'; // test for file if ( !file_exists( $view ) ) < throw new Exception( 'Error: Invalid View ' . ucfirst( $this->name ) ); > // extract data variables, skip collisions if ( !empty( $this->viewData ) ) < extract( $this->viewData, EXTR_SKIP ); > // Include view include $view; > /** * Load Models * * @param string $model * @return object * @access protected */ protected function load( $model ) < $model_file = APP_PATH . '/Model/' . ucfirst( $model ) . '.php'; // test valid model if ( is_file( $model_file ) ) < include_once( $model_file ); $class = ucfirst( $model ) . 'Model'; return new $class; >else < throw new Exception( 'Error: Invalid Model:' . ucfirst( $model ) ); >> // Core function abstract public function index(); >
When instantiated by the front controller the controller sets up the view file name. The controller method also sets up connections to models and sends the data to the view. Neither are strictly required. For example:
/** * BlogController * * @package MVC */ class BlogController extends Controller < /** * Core function */ public function index() < // load blog model $blog_model = $this->load( 'Blog' ); // set data $this->title = 'This is the Blog View'; $this->blog_title = $blog_model->getTitle(); $this->hello = 'Hello World!'; // Render view $this->displayView(); > >
Here the load method retrieves an instance of the BlogModel and uses this to set a data variable ‘blog_title’. Again using __get and __set hide the creation and retrieval of the data. The controller method then renders the associated view which displays the data. This is the typical data flow for an MVC framework.
Model
Again all models are children of a parent model. In this case it’s a bit sparse, however in a more detailed MVC this could be used to set database access or links to required external libraries.
/** * Abstract Model * * @access public abstract * @package MVC */ abstract class Model <>
/** * Blog Model */ class BlogModel extends Model < /** * Get Blog Title */ public function getTitle() < return 'This is the Blog Title'; >>
The BlogModel example shows the method used to get the blog title. Production MVC’s would here define the sql and access the database object to retrieve the data.
View
The view is an html template with placeholders for PHP variables which is rendered and sent to the browser. Many frameworks use a templating engine which attempt to simplify the processing of the variables, however PHP has it’s own templating system via its alternative syntax which is more than suitable for purpose. For example, here the blogindex view.
StartUp
So far we’ve been through the process flow of an MVC framework. How is this initiated? Normally this is through an index.php file, which loads required files and bootstraps the framework. In our case this is:
// The name of THIS file define( 'BASE', pathinfo( __FILE__, PATHINFO_BASENAME ) ); // Path to the root folder define( 'ABS_PATH', str_replace( BASE, '', __FILE__ ) ); // Path to the system folders define( 'APP_PATH', ABS_PATH . str_replace( '\\', '/', 'app' ) ); define( 'LIB_PATH', ABS_PATH . str_replace( '\\', '/', 'lib' ) ); // Include Class Loader require_once( 'Loader.php' ); // test run try < // Parse and construct route $router = new Router(); // Set up front controller & run $controller = Front::getInstance()->run( $router ); > catch ( Exception $e ) < print "[" . $e->getMessage() . "]
"; >
First we set up the required paths and include the loader. As detailed below this includes files & classes as required. The next steps are to initialise the Router, which parses the url. This is then passed to the Fron Controller run method which processes the information and calls the requested runctionality. All of the code is encapsulated in the MVC framework and processed via a single entry point. This is the heart of the MVC pattern.
Loader
The loader is not strictly required, but here it’s included as an example. It lazy loads the required top level files by registering a loader function. A production framework may extend this to all framework files.
/** * Loader * * Loads required mvc class if allowed and keeps a record of what is already loaded. * Requires the Base directory to be defined and that each class file is named after the className. * * @package MVC */ class Loader < /** * A list of Class files already located * @var array $located * @access protected static */ protected static $located = array(); /** * Load a new class / interface into memory * @param string The name of the class, case SenSItivE * @param boolean $ext Defaults to FALSE Extended path or ABS path * @access public static */ public static function Load( $className ) < // test if pre-loaded if ( in_array( $className, self::$located ) || class_exists( $className, FALSE ) || interface_exists( $className, FALSE ) ) < return; >// get path & file require_once ABS_PATH . '/' . str_replace( '_', '/', $className ) . '.php'; // store file paths self::$located[] = $className; > > // Register autoloader spl_autoload_register( 'Loader::Load');
Fat Models, Thin controller
Hopefully this gives a good insight into the MVC pattern and how PHP frameworks use them. One principle to remember is ‘fat models, thin controller’. Essentially this means that responsibility for processing data should be in the models, and that the controller should be as lightweight as possible, and only be responsible for receiving, requesting and rendering data.
This is a fairly ‘classical’ approach to MVC with PHP where the controller acts as a ‘guardian’ to the model and view. Other approaches to MVC and PHP have the model and view as connecting entities.
I’ll add this to github asap.