Finally, MVC Explained Well.
Introduction
If your goal is loose coupling and the separation of concerns--and you believe in the Single Responsibility Principle--then the Controller
should not pass data directly to the View
. No.
Generally speaking, despite the initial learning curve for MVC, most people can understand, and agree, that the Controller
is the first step and the View
is the last step.
The Front Controller and Controller
All things being equal, the generic purpose of the Controller
(see the Command Pattern), not the Front Controller
, is to manage the relationship between the Model
and the View
. The Front Controller
will send user input to the correct command--usually a method in a child instance of Controller
.
From here, inside a method / function / command of the child Controller
instance, is where the user input data must be sent to a method of the Model
so that the state of the application can be managed.
Fat Versus Skinny Controllers
Debates often occur about fat versus skinny Controllers
, especially when the issue of checking to see if a user is logged-in is considered. The simple resolution to this quandary is to consider user status as part of the application state.
Hence, the logic (or objects) for checking if a user is logged in should reside / be injected high up in the Model
inheritance hierarchy. Thus, all child models will have the ability to detect a user's authentication status and respond accordingly.
Continuing on, if the first step (Controller
) must directly communicate with the last step (View
), then the relationship is tightly coupled and adulterated, which makes the View
dependent on the Controller ((Controller-View) + Model) and mixes concerns.
Single Responsibility Principle
The Single Responsibility Principle attempts to give a class one job, and thus only one reason to change. If you have to change the Controller
because (1) something about the View
has changed, then going strictly by the book, your Controller
has too many responsibilities.
Stable View Interfaces
Assuming a stable 'View" interface, changes in the Controller
(the first step), or changes in the View
(the last step), should be invisible to each other. The interface of the View
should remain stable throughout the lifetime of the application: view.render()
or $view->render()
, or something to that effect. The method should expect an input argument from the Model
.
Quick History Lesson
The Smalltalk programming language (the original language MVC was implemented in) passed objects between object, not arrays or maps / associative arrays between objects. Thus, one should think carefully about what kind of data structure you want the View
(the presentation logic) to work with.
In many ways, it can be more elegant to pass an object wrapped around an array / map to another object. Why? Any non-view related logic can reside with the object, keeping the View
only concerned with presentation logic / templating, not data conditioning logic (such as escaping data as HTML entities when necessary).
Hey, What About the Model?
A method of a child instance of Model
should return data to an interfacing method of a View
. Thus, by virtue of the Liskov Substitution and Dependency Inversion Principles, you should be able to inject any View
object with the correct interface into a Controller
and expect it to work just fine (according to defined behavior).
Polymorphism in the House
Thus, if you need a View
to respond with HTML, XML, JSON, YAML, plain text, or even just HTTP headers, it will work because the implementation is hidden behind the interface / method of the child View
. In effect, you would be using a Strategy pattern (for your views) and polymorphism to get different behaviors from the same View
method call (view.render()
or $view->render()
).
Bottom Line
In effect, to establish the correct coupling relationships, separate concerns, and mind the Single Responsibility principle, one must inject (composition) a Model
object and a View
object into a Controller
object.
Once a child Controller
instance has Model
and View
instances as properties, it can define methods / commands that accept user data from the Front Controller
.
The child Model
instance will typically (1) establish user authentication status (via inheritance), and (2) validate user input data (encoding checks, filtering / sanitizing, validating).
When the Model
method finishes its work, it returns all necessary data to a method of the View
(render(), or something similar). All of this is occurring inside a method of the Controller
.
Each command / method of the child Controller
serves as a top level main line program for each application request.
Bonus: The Observer Pattern
What people forget is that the original MVC architecture was coded in Smalltalk, and was designed for GUI software running on a single computer. Server-side MVC implementations are not structured the same as a desktop application might be.
In an ideal server-side world, a change in the server-side Model
's application state would automatically update the application / DOM on the client-side, and vice versa. HA!
The Observer Pattern was designed to allow objects to automatically update one another when certain actions take place by registering with one another. Unfortunately, it is not possible to register client-side View
objects with server-side Model
objects.
Instead, under normal circumstances, the best that we can do from the server-side is to send new state data (XML, JSON, YAML, text, ...), typically to client-side JavaScript. Hopefully, the original, incoming HTTP request came from an XMLHttpRequest object (an asynchronous HTTP request) so that an entire HTML file (plus any images, CSS, blah, blah, blah ...) does not need to be loaded synchronously.
What about ReSTful APIs?
If you are making a ReSTful API, you only ever send application state data (frequently, JSON or XML). The only question is how you should format and structure the data (typically determined by the incoming HTTP Accept header).
Double Bonus: HTTP Headers
Yes, a very thorough server-side, MVC implementation would be ReSTful and send the appropriate HTTP header and status code before sending state data back to the client.
The original MVC implementation in Smalltalk did not have to consider this at all!
Additionally, HTTP caching should be accounted for.
In short, the first way to create a successful, ReSTful MVC architecture is to solve how you will send the appropriate HTTP headers and HTTP status code back to the client.
If you have not done that work, you are omitting an important aspect of the fundamental architecture of the World Wide Web.
Conclusion
If your Controller
is passing data to the View
, then you are missing the middle step, an intentionally defined and separate Model
. If a Controller
is passing data to a View
, this does not describe a true MVC architecture. That would be a Controller-View (CV), which is doable, except for violating purist ideals about loose coupling, separation of concerns, single responsibility, substitution, and dependency inversion.
In short, an application that merges the Controller
and Model
(Codel? Montroller?), then sends some kind of data structure to the View
(V), is a software engineering regression. Some might call that an anti-pattern.
Side Issue
==========
(Another issue about Controllers
that interest me is that because some content on a website will be public (no login required), but other content will be private, there's an opportunity for efficiency if Controller
is sub-classed into PublicController and PrivateController. Then, sub-class these classes accordingly based on 1) The public website, or 2) Content that requires you to be logged in.
Again, the original MVC architecture did not have to deal with the concept of public and private content.
By making these fundamental subclasses, the FrontController can act in a more secure way by denying access to content that requires one to be logged in first. If the instance of the requested Controller
is on the "private" list and the requestor is not logged in, then the FrontController should use "common sense" and deny, or redirect.
This check will occur anyway in a flatter Controller
hierarchy, but it will have to wait until the desired Model
is instantiated, which in my opinion is inefficient. Why should someone who is not logged in be able to instantiate a Model
that they cannot use!! Just to have it do a log-in check, and then reject/redirect the request? Dumb. Straight dumb.
It's really dumb when you consider how many objects might have to be instantiated and injected into an instance of a Model
, just to in the first few lines of execution determine that NOPE, you're not logged in. ;-)
In my opinion, FrontControllers (the router/dispatcher stuff) should have the option to deny, or redirect to other controllers, without having to instantiate a Model
to do it.
Update:
I have received some interesting comments about this answer. A small, practical illustration should make my discourse more clear. This is a bare bones example in PHP because it's easy.
Let's assume the user's address bar shows the following in their browser / user-agent because they are filling out a form.
https://www.example.com/contact
When they hit the send button, the user-agent or XmlHttpRequest object, is instructed to go here:
https://www.example.com/contact/send
abstract class Model
{
protected Authenticator $authenticator;
protected function __construct(Authenticator $authenticator) {
$this->authenticator = $authenticator;
}
protected function isLoggedIn(): bool {
return $this->authenticator->isAuthenticated();
}
}
class ContactModel extends Model
{
private Validator $validator; // Throws exceptions
private Notifier $notifier; // Sends a type of message, throws exceptions
private Message $message; // A specific message, less user input, throws exceptions
public function __construct(Authenticator $authenticator, Validator $validator, Notifier $notifier, Message $message) {
parent::__construct($authenticator);
$this->validator = $validator;
$this->notifier = $notifier;
$this->message = $message;
}
public function sendEmail(Request $request): array {
try {
$cleanData = $this->validator->test($request);
$message = $this->message->format($cleanData);
$this->notifier->send($message);
return ['emailSent' => true];
} catch(Exception $e) {
return ['emailSent' => false];
}
}
}
abstract class View
{
protected function __construct() {
}
abstract public function render():
}
abstract class HttpView extends View
{
private Response $response;
protected function __construct(Response $response) {
$this->response = $response;
}
public function render(array $headers) {
$this->response->addHeaders($headers);
$this->response->sendHeaders();
}
protected function setMessage(string $message) {
$this->response->setBody($message);
}
protected function reply() {
$this->response->sendBody();
}
}
abstract class JsonView extends HttpView
{
public function __construct(Response $response) {
parent::__construct(Response $response);
}
public function render(array $data) {
$this->setMessage(json_encode($data, JSON_THROW_ON_ERROR));
parent::render($data['headers']);
$this->reply();
}
}
abstract class XmlView extends HttpView
{
private Template $template;
public function __construct(Response $response, Template $template) {
parent::__construct(Response $response);
}
public function render(array $data) {
$this->setMessage($this->template->merge($data['body']));
parent::render($data['headers']);
$this->reply();
}
}
abstract class HtmlView extends HttpView
{
private Template $template;
public function __construct(Response $response, Template $template) {
parent::__construct(Response $response);
$this->template = $template;
}
public function render(array $data) {
$this->setMessage($this->template->merge($data['body']));
parent::render($data['headers']);
$this->reply();
}
}
/* Which "Contact" view gets instantiated depends on the incoming
HTTP Accept header! One View for any particular media type.
Although, one might not need more than JSON in many cases.
*/
class ContactJsonView extends JsonView
{
public function __construct(Response $response) {
parent::__construct($response);
}
}
class ContactXmlView extends XmlView
{
public function __construct(Response $response, Template $template) {
parent::__construct($response, $template);
}
}
class ContactHtmlView extends HtmlView
{
public function __construct(Response $response, Template $template) {
parent::__construct($response, $template);
}
}
// ***********************************
abstract class Controller
{
protected $model;
protected $view;
public function __construct(Model $model, View $view) {
$this->model = $model;
$this->view = $view;
}
}
/*
Controllers of this side of the family require authentication before
instantiating an instance of a Model or View.
*/
abstract class PrivateController extends Controller
{
protected Authenticator $authenticator;
public function __construct(Model $model, View $view, Authenicator $authenticator) {
parent::__construct($model, $view);
$this->authenticator = $authenticator;
}
}
/*
Controllers that are publicly accessible. No authentication required.
*/
abstract class PublicController extends Controller
{
public function __construct(Model $model, View $view) {
parent::__construct($model, $view);
}
}
class ContactController extends PublicController
{
public function __construct(Model $model, View $view) {
parent::__construct($model, $view);
}
// Assume a well organized HTTP Request object is being utilized.
public function send(Request $request) {
$data = $this->model->sendEmail($request);
$this->view->render($data); // The Model is updating the View
/* Note, if I had made the Model a property of the View, it
would go against the idea of dependency injection and making
code that can be easily tested. By only passing data to the
View, I can test the view without having to instantiate the Model.
In this case, $data is the minimum amount of data required
to be sent back to the user-agent to update the state of the
application (Model) on the client side. It's up to the client
to update it's View based on the contents of $data.
E-mail will not be part of the View. It's more of a side effect in this case.
*/
}
}
// Meanwhile, in the HALL OF JUSTICE!! (index.php)
// -------------------------------------------------------------
try {
require 'Autolaoder.php';
$errorHandler = new ErrorHandler();
$validator = new HttpRequestValidator();
$validator->test(); // Testing the header info, not the true user info yet
$request = new HttpRequest($validator->getCleanData());
$response = new HttpResponse(); // Must be populated with data
$model = new ContactModel(new Authenticator(), new ContactValidator(), new EmailNotifier(), new ContactMessage());
$view;
$acceptHeader = $request->getHeader('Accept');
if ($acceptHeader === 'application/json') {
$view = new ContactJsonView($response);
} elsif ($acceptHeader === 'application/xml') {
$view = new ContactXmlView($response, new ContactXmlTemplate());
} else {
$view = new ContactHtmlView($response, new ContactHtmlTemplate());
}
$controller = new ContractController($model, view);
$controller->send($request);
} catch(Throwable $e) {
// Run error sequence.
}
Final Summary
In a PHP MVC application, URL rewriting will occur (Apache, or other) first so that there can be a single point of entry (index.php) into the app.
The code here at the end is, in part, what would really be going on in the index.php
before instantiating a FrontController
, and inside FrontController itself.
Note that I have made a very, very oversimplified solution here at the end. The FrontController should be able to DYNAMICALLY determine the correct Controller from the request URL https://www.example.com/contact/send, by using 'contact' (or anything validated in that position) as the Controller
, and 'send' (or anything validated in that position) as the action / command / menthod.
A more modular solution would have a a Router class to examine the request URL, break it down, verify the route is valid with for the app, and then find the correct controller, limited by a correctly corresponding HTTP request method (GET, POST, PUT, PATCH, DELETE, HEAD, ... whatever). One must not only ensure that the Controller
exists, but that also the method on the Controller
exists, too!
If a truly ReSTful MVC implementation is desired, then the View
must be able to be dynamically determined as well. This is feasible because if the HTTP Accept
header has been sent by the client, it should indicate the representation of the data it wants in reply. Can you have a default View
? Can you ever know for certain that you will always get asked for JSON or HTML? No. It's up to you to support the media types that your clients are allowed to receive, and that you want to provide.
If the client asks for a media-type that you do not support, throw an exception and respond with an error (and correct error code). I have seen some very creative error pages! ;-)
All in all, this is why dependency injection containers are very useful.
Keeping track of class dependencies, and use of the new
keyword, can happen in just one place if you use a dependency injection container.
You should have an autoloader. You should have error handling. You should have input validation (encoding, sanitization, validation). You should have a sessions solution (not implemented here). You should have a modular FrontController
, and use of the Relfection API can be useful in determining if Classes and methods exists. PHP has built in methods for such things, but use what's best for your use case.
Controller
is sending data to aView
, do you have a separateModel
?