5
\$\begingroup\$

I've seen at least two recent PHP questions that would do well from a Model-View-Controller ('MVC' from here-on-out in this question) setup. Now, me, being the horrible person I am, I wanted to build one because it's been a while since I've done any actual PHP work, and as much trash as I have been known to talk about it it's still a very popular and, to be fair, very easy language to use.

So, I've built an MVC structure in it. It's very basic, but it does exactly what an MVC application should. Separation of view from code.

It all starts with the .htaccess file, this file basically allows the use of /Controller/Action/Id?QueryString structured URL's, so I feel it's a good starting place:

RewriteEngine on
# Rewrite /Controller as /Controller/Index
RewriteRule ^([a-zA-Z0-9]+)/?$ index.php?Controller=$1&Action=Index [L,QSA]
# Rerwite /Controller/Action(/Id?Querystring)
RewriteRule ^([a-zA-Z0-9]+)/([a-zA-Z0-9]+)/?(/([a-zA-Z0-9]+)/?)?$ index.php?Controller=$1&Action=$2&Id=$4 [L,QSA]

This is pretty self-explanatory, and it should make sense.

The next step is directing the user to index.php. This is where magic starts happening, and this is where we start making actual MVC decisions:

<?php
    function LoadDirectory($dir) {
        foreach (scandir($dir) as $file) {
            if ($file !== "." && $file !== "..") {
                require_once("{$dir}/{$file}");
            }
        }
    }

    LoadDirectory("Include");
    LoadDirectory("Controllers");
    LoadDirectory("Models");

    $controllerName = $_GET["Controller"];
    $action = $_GET["Action"];

    $id = "";
    if (isset($_GET["Id"])) {
        $id = $_GET["Id"];
    }

    // Forced 'Controller' name suffix, configured in Constants
    $controllerClassName = $controllerName . "Controller";
    if (Constants::REQUIRE_CONTROLLER_SUFFIX === FALSE
        && class_exists($controllerClassName) === FALSE) {
        $controllerClassName = $controllerName;
    }

    // If this one fails then we could not find a valid Controller for our
    // request, this means we have an error and must quit.
    // TODO: Pretty this up a bit.
    if (class_exists($controllerClassName) === FALSE) {
        die("The controller '{$controllerName}' does not exist.");
    }

    $controller = new $controllerClassName();

    // TODO: Pretty this up a bit.
    if (method_exists($controller, $action) === FALSE) {
        die("The controller '{$controllerClassName}' does not have an action '{$action}'.");
    }

    $requestType = $_SERVER['REQUEST_METHOD'];

    if ($requestType === "POST") {
        $methodModelMethod = $action . "Model";

        $methodModelName = $controller->$methodModelMethod();

        $methodModel = new $methodModelName();

        foreach (get_object_vars($methodModel) as $var => $value) {
            $methodModel->$var = $_POST[$var];
        }

        echo $controller->$action($methodModel);
    } else if ($requestType === "GET") {
        echo $controller->$action($id);
    } else {
        die("HTTP request type '{$requestType}' is unsupported.");
    }
?>

Nothing here needs to be modified to use the MVC system, this is the landing so that index.php can take all the work and send it away.

This next controller is an example, because I want to demonstrate how easy this MVC system is to use:

<?php
    class HomeController extends BaseController {
        private $items = array("Cat", "Dog", "Rat", "Fox", "Horse");

        public function Name() {
            return "Home";
        }

        public function Index() {
            $model = new HomeIndexViewModel();
            $model->Title = "Test";
            $model->Message = "Some awesome message";
            $model->Items = $this->items;
            return $this->View("Index", $model);
        }

        public function Detail($id) {
            $item = $this->items[$id];
            $model = new HomeIndexViewModel();
            $model->Title = "View Item";
            $model->Message = "Viewing an item";
            $model->Items = array($item);
            return $this->View("Index", $model);
        }

        public function UpdateModel() { return "UpdateModel"; }
        public function Update($postModel) {
            $item = $this->items[$postModel->Id];
            $model = new HomeIndexViewModel();
            $model->Title = "Updated Item" . $postModel->Id;
            $model->Message = "Updated an item";
            $model->Items = array($item);
            return $this->View("Index", $model);
        }
    }
?>

Of course, this begs the question, what is BaseController? Well, that's where the View magic happens:

<?php
    abstract class BaseController {
        abstract public function Name();

        public function View($action, $model) {
            $controllerName = $this->Name();

            $view = file_get_contents("Views/{$controllerName}/{$action}." . Constants::VIEW_EXTENSION);
            $view = $this->PopulateView($view, $model);

            $layoutModel = new LayoutModel();
            $layoutModel->Title = $model->Title;
            $layoutModel->Controller = $controllerName;
            $layoutModel->Action = $action;
            $layoutModel->View = $view;
            $layout = file_get_contents("Views/" . Constants::LAYOUT_VIEW . "." . Constants::VIEW_EXTENSION);
            $finalHtml = $this->PopulateView($layout, $layoutModel);

            return $finalHtml;
        }

        public function PopulateView($view, $model) {
            $start = strpos($view, "<?php");

            while ($start !== FALSE) {
                $end = strpos($view, "?>", $start + 6);
                $phpCode = substr($view, $start + 6, $end - $start - 6);

                $result = CleanEval::RunCode($phpCode, $model);

                $view = substr_replace($view, $result, $start, $end - $start + 2);
                $start = strpos($view, "<?php", $start + 6);
            }

            return $view;
        }
    }
?>

Well this introduces our CleanEval class, and Constants really stands out here. Let's look at both of those:

The CleanEval class does some dirty things, and I'm not proud that it does them, but I think it does them well enough that, while we lack other options now, this will suffice. I built the $actCode the way I have to isolate the $code from the environment mostly. It can still access all the classes and such loaded, but at the very least it has no unwanted local variables, and won't leave any unwanted local variables in scope:

<?php
    class CleanEval {
        public static function RunCode($code, $model) {
            $actCode = "
            \$html = new Html();

            \$cleanFunction = function(\$Model, &\$Html) {
                " . $code . "
            };

            \$result = \$cleanFunction(\$model, \$html);
            return array(\$html->GetBuffer());";

            return eval($actCode);
        }
    }
?>

And our Constants:

<?php
    class Constants {
        // Set this to the base directory
        public const BASE_URL = "/Php7Mvc";

        // If set to TRUE then URL's will be: /Controller/Action/Id?QueryString,
        // If set to FALSE then URL's will be:
        // /index.php?Controller=controller&Action=action&Id=id&QueryString
        public const USE_CLEAN_URLS = TRUE;

        // If set to TRUE then controllers will require the Controller suffix,
        // If set to FALSE then we'll try to find the class with the suffix
        // first, but then will allow any class with the controller name.
        public const REQUIRE_CONTROLLER_SUFFIX = TRUE;

        // Set this to the base layout view (defaults to 'Layout'), do not
        // append the VIEW_EXTENSION.
        public const LAYOUT_VIEW = "Layout";

        // Set this to the extension to use for 'View' files (defaults to php)
        public const VIEW_EXTENSION = "php";
    }
?>

Then we have a LayoutModel:

<?php
    class LayoutModel extends BaseModel {
        public $Controller;
        public $View;
    }
?>

Now we need to know what BaseModel is:

<?php
    class BaseModel {
        public $Title;
    }
?>

Not much to those.

We also see HomeIndexViewModel (another usage example):

<?php
    class HomeIndexViewModel extends BaseModel {
        public $Message;
        public $Items;
    }
?>

And UpdateModel (yet another usage model):

<?php
    class UpdateModel {
        public $Id;
    }
?>

The final core class of the framework is the Html class, which allows easy rendering:

<?php
    class Html {
        private $buffer;

        public function Render($string) {
            $this->buffer .= $string;
        }

        public function AbsoluteUri($relativePath) {
            return Constants::BASE_URL . "/" . $relativePath;
        }

        public function StyleSheet($path) {
            return "<link rel=\"stylesheet\" href=\"" . $this->AbsoluteUri($path) . "\" />";
        }

        public function BuildLink($name, $controller, $action, $id = "", $queryString = "", $classes = "") {
            $url = "";

            if (Constants::USE_CLEAN_URLS === TRUE) {
                $url = Constants::BASE_URL . "/{$controller}/{$action}";

                if (strlen($id) > 0) {
                    $url .= "/{$id}";
                }

                if (strlen($queryString) > 0) {
                    $url .= "?{$queryString}";
                }
            } else {
                $url = Constants::BASE_URL . "/index.php?Controller={$controller}&Action={$action}";

                if (strlen($id) > 0) {
                    $url .= "&Id={$id}";
                }

                if (strlen($queryString) > 0) {
                    $url .= "&{$queryString}";
                }
            }

            return "<a href=\"{$url}\" class=\"{$classes}\">{$name}</a>";
        }

        public function GetBuffer() {
            return $this->buffer;
        }

        public function ClearBuffer() {
            $this->buffer = "";
        }
    }
?>

All that's left are the views, the Layout view is part of the framework:

<html>
    <head>
        <title><?php $Html->Render($Model->Action . " - " . $Model->Controller); ?></title>

        <?php $Html->Render($Html->StyleSheet("Content/Site.css")); ?>
    </head>
    <body>
        <?php $Html->Render($Model->View); ?>
    </body>
</html>

The Index view is not:

<div class="test">
    <h1>Page Title</h1>
    <?php $Html->Render($Model->Title); ?>
</div>
<div class="test">
    <h1>Message</h1>
    <?php $Html->Render($Model->Message); ?>
</div>
<div class="test">
    <h1>Items</h1>
    <?php
        foreach ($Model->Items as $item) {
            $Html->Render('<div class="test">' . $item . '</div>');
        }
    ?>

    <?php $Html->Render($Html->BuildLink("Test", "Home", "Detail", 1)); ?>    

    <form method="POST" action="/Php7Mvc/Home/Update">
        <input type="text" value="0" name="Id" />
        <input type="submit" value="Update" />
    </form>
</div>

Currently working on improving the form building, but it's still pretty easy.

So, realistically, it's not a lot of work to make this happen. If you don't use the .htaccess version (which requires mod_rewrite), you can set USE_CLEAN_URLS in Constants to FALSE, and then links and URL's will generate from the Html class without the /Controller/Action/Id?QueryString bit, and will be a regular URL (/index.php?Controller=controller&Action=action&Id=id&QueryString).

You can comment on everything except the quoted blocks, those are just examples of usage.

\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

I would consider moving your routing definition logic out of .htaccess and into your PHP code. I think you will find it much easier over the long haul to maintain your logic solely in PHP vs. being split between PHP and Apache.

This could leave you with a simple front controller redirect in Apache to index.php, with PHP then inspecting the request for routing.


You may consider autoloading your model/view/controller classes rather than loading everything for every request. Consider PSR-4 autloading standards (there are example implementation available as well here https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md

This would have you also introducing namespacing in your classes, something you are not currently doing.


You should consider building a class to represent the request, so you don't need to be arbitrarily doing things like determining URI in your HTML class. You would instead inject the Request dependency to any code (like router, controller, view, etc. that need it).

Combining this with the thought of having a PHP Routing class, you could be simplifying your main index page down to something like this (shown without error handling):

// require your application bootstrapping file
// here you specify config, autoloading, session handling, error/exception handling, etc.
require(...);

// set up request object based on request
$request = new Request();

// perhaps load route configuration from a file somewhere
Router::loadConfig();

// route to controller based on Request
Router::route($request);

That is really all you need in your index file.

You then have the router:

  • determine the appropriate controller based on URI and HTTP action (GET, POST, PUT, DELETE, HEAD, etc.);
  • instantiate the appropriate controller;
  • pass the request and execution control to the controller.

So in Router class the key methods may look like:

public static route(Request $request) {
    $controller = Router::getController($request);
    $controller->execute();
}

protected static getController(Request $request) {
    // here the resolve controller method could either return an appropriate controller
    // or specify an error controller if no route is found
    $controllerName = Router::resolveController($request);
    return new {$controllerName}($request);
}

protected static resolveController(Request $request) {
    // maybe Request class has method to determine the requested resource
    // from URI
    $resource = $request->getRequestedResource();
    $httpAction = $request->getHttpAction();

    // not shown - logic here to determine controller name based on resource
    // named in URI and HTTP action as compared against route configuration
    return $controllerName;
}

The main thing here is that you are driving towards a granular approach to handling routing, controller instantiation, etc. with each method doing one specific thing, rather than an intermixing of responsibilities as is done in your current index file.


I don't think you have clean separation between your controllers, models, and views. Your controller for example should focus solely on marshalling the appropriate data model to be rendered and then passing that data to the view for rendering. Right now, your controller is actually populating that data into the view. And doing really inappropriate things like evaling view related code.

Extending my example from above, the key controller methods may look like:

public function execute() {
    // get data model based on action which the controller can determine
    // based on the fact it has stored the request
    $data = $this->executeAction();
    // instantiate a view object, handing it the request and model info
    $view = $this->getView();

    // pass data from model and execution off to view object.
    $view->render($data);
}

protected function executeAction() {
    // the class inheriting the base controller would typically
    // specify the model to be worked with so we can instantiate it.
    $model = $this->getModel();
    // now, let's get the method name based on the info from request
    // let's assume you can get URI from request
    $uri = $this->request->getUri();
    // and that the inheriting controller can resolve the URI
    // into an appropriate method name to call on model
    $method = $this->resolveMethod($uri);

    // now call the model, along with request (which still holds query string
    // post data, etc. that may be needed by the model).
    return $model->{$method}($this->request);
}

protected function getView() {
    // not shown
    // determine view based on request settings (i.e. do we need to return HTML,
    //  JSON, XML, etc.)
    // the inheriting controller class would then specify the specific
    // view class to be initialized
    return new {$view}();
}

Why the need for eval() here? You should be able to design this away.


Once you get to the point of calling render() operation on the view, you may consider building a Response object. That way, regardless of the type of view to be delivered, you could always have the contract of having a structured response object that contains headers to be sent as well and content itself (regardless of format) and would, in essence be the last thing called (i.e. $response->send()). Right now, I don't really see any consideration in your framework for how you handle response headers. I would think this would be a base requirement for any framework.


Your layout/templating model seems very cumbersome and doesn't deliver a clear separation of format (i.e. HTML) from code, nor does it seem to consider component-based layouts (i.e. components for header, footer, or other common elements that can be re-used across different views). There are a number of templating solutions out there in PHP, so I am not sure that rolling your own makes much sense.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.