Immutable MVC: MVC In PHP 2019 Edition (Part 1)
MVC Revisited
The most popular articles on this website are about MVC, the oldest of which is now nearing ten years old. A lot has changed in that time, and my own experience has also grown. My previous MVC series kind of fizzled out because I wanted to go back and tweak the examples in the earlier articles.
PHP has changed quite a lot since I wrote those articles. I'm going to start fresh and give some cleaner, complete and more modern code examples.
Nine years have passed since I wrote the first MVC article here and I have a few observations about it looking back:
- The PHP ecosystem hasn't changed. People still call the "controller as a mediator" approach MVC. If anything the situation is worse because the popular frameworks are now so embedded trying to do anything different just feels wrong.
- The points I made are still valid. Giving the view access to the model has significant benefits which I won't reiterate here. Take a look at my previous article if you're interested.
- The code examples I gave aren't clear or complete enough. I spent more time focusing on concepts than giving code examples. I'm going to rectify that here.
In addition to writing some more complete examples, it's time for an update with some newer PHP features and programming trends like immutability. I'm going to show you how to write a completely immutable MVC structure. Then demonstrate reusable controllers and a routing system.
Other than making use of new PHP features such as return type hinting, the two main changes I've made to my coding style are immutability and favouring arguments over constructors with related properties.
Hello World
The first thing I'm going to do is provide a demonstration of a completely immutable MVC triad.
Let's take my previous Hello World example and convert it to become immutable:
class Model {
public $text;
public function __construct() {
$this->text = 'Hello world!';
}
}
class View {
private $model;
public function __construct(Model $model) {
$this->model = $model;
}
public function output() {
return '<a href="mvc.php?action=textclicked">' . $this->model->text . '</a>';
}
}
class Controller {
private $model;
public function __construct(Model $model) {
$this->model = $model;
}
public function textClicked() {
$this->model->text = 'Text Updated';
}
}
$model = new Model();
//It is important that the controller and the view share the model
$controller = new Controller($model);
$view = new View($model);
if (isset($_GET['action'])) $controller->{$_GET['action']}();
echo $view->output();
This version of the architecture relies on the model being mutable. Both the controller and the view share a reference to the same model
instance. The controller updates the model and the view reads the model's state to produce the output.
The Model
In this example, the model is mutable but the other components are not. Firstly, let's make the model immutable:
class Model {
private $text;
public function __construct($text = 'Hello World') {
$this->text = $text;
}
public function getText() {
return $this->text;
}
public function setText($text) {
return new Model($text);
}
}
Now there is no way to change the model's state once it's been instantiated. Calling the setText
method creates a new instance. Any place that the original instance is referenced will not see a change in the state of the model. The controller and view will need to be updated to use this updated model class.
Previously, the model and controller shared a model instance. The controller would amend the model's state and the view would read the state of the model instance. Now that the model is immutable, we cannot rely on the view and controller sharing the same model instance. Instead, the controller action will return a model instance which will then be passed to the view.
The Controller
Instead of updating the state in an existing, mutable, model instance, the controller will return a new model instance after making its changes:
class Controller {
public function textClicked(Model $model): Model {
return $model->setText('Text Clicked');
}
}
Rather than relying on constructors and properties, I've opted to use arguments to pass in the model which is going to be updated. The controller action is now given an immutable model instance and returns a new instance with an changes that are required.
It should be noted that this isn't part of MVC, using either constructor arguments with related properties or arguments to the controller action have the same effect. But by avoiding constructors it is no longer necessary to keep the model as a property and the controller is significantly simplified. Less code doing the same job is always a good thing.
The View
The View needs only a minor tweak to call the method rather than reading the public property:
class View {
public function output(Model $model) {
return '<a href="mvc.php?action=textclicked">' . $model->getText() . '</a>';
}
}
The advantage of using arguments here is that the same View
instance can be used to render multiple models. It's no longer necessary to instantiate a view object for each model that is rendered.
Putting it all together
The final piece of the puzzle is the code which instantiates all the components. There are no longer any constructor arguments but the correct method arguments need to be applied at each stage:
$model = new Model();
$controller = new Controller();
$view = new View();
if (isset($_GET['action'])) {
$model = $controller->{$_GET['action']}($model);
}
echo $view->output($model);
In this immutable version, the model is passed into the controller action and an updated model instance returned. The model instance is then passed into the view's output method.
Immutable MVC Complete Example
The complete code looks like this:
class Model {
private $text;
public function __construct($text = 'Hello World') {
$this->text = $text;
}
public function getText() {
return $this->text;
}
public function setText($text) {
return new Model($text);
}
}
class View {
public function output(Model $model) {
return '<a href="mvc.php?action=textclicked">' . $model->getText() . '</a>';
}
}
class Controller {
public function textClicked(Model $model): Model {
return $model->setText('Text Clicked');
}
}
$model = new Model();
$controller = new Controller();
$view = new View();
if (isset($_GET['action'])) {
$model = $controller->{$_GET['action']}($model);
}
echo $view->output($model);
Conclusion
This immutable implementation of MVC has some advantages over the mutable version:
- State is better managed so that the application doesn't suffer from action at a distance where changing an object in one location (in the controller) then causes changes in a seemingly unrelated component (the view)
- There is less state overall. There are no longer references to the different objects in multiple locations. The controller and view no longer have a reference to the model, they are given an instance to work with at the moment they need it, not before
What next?
Now that we have an immutable MVC structure to build on, I'm going to use this to build a more functional application. The first thing I'll do is demonstrate a more complete example with multiple controllers and actions that do some basic database manipulation.