Ok
I’ll use the first part of their example, a list of posts.
Firstly, here’s a domain model which provides very generic accessors to the domain state. I won’t supply the full PDO backend but you can see what it’s doing from the model API.
class Posts {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function findByField($field, $value, $sortOrder) {
//Pseudocode, ignore that this won't work...
return $this->pdo->query("SELECT * FROM posts where $field = $value ORDER BY $sortOrder");
}
public function findAll($sortOrder) {
//Pseudocode, ignore that this won't work...
return $this->pdo->query("SELECT * FROM posts ORDER BY $sortOrder");
}
public function findById($id) {
//Pseudocode, ignore that this won't work...
return $this->pdo->query("SELECT * FROM posts where id = $id");
}
}
This is going to be reused throughout the application anywhere that posts are required.
Next, here’s a generic View which contains the logic that is common across any type of list:
class ListView {
private $model;
private $template;
public function __construct(ListViewModel $model, Template $template) {
$this->model = $model;
}
public function output() {
$this->template->assign('items', $this->model->getRecords());
return $this->template->output();
}
}
And an interface to provide an API for the getRecords method:
interface ListViewModel {
public function getRecords();
}
Now we’re ready to link the domain model to the GUI via a Model (MVC model, ViewModel, Application Model. Whatever terminology you want to use). The important distinction is that this holds application state rather than domain state. Since we’re listing every single post, the application state is that “All posts have been selected”. Not a great example. but I’ll come to that later:
class EveryPostList implements ListViewModel {
private $posts;
public function __construct(Posts $posts) {
$this->posts = $posts;
}
public function getRecords() {
return $this->posts->findAll('date desc');
}
}
Since there is zero user interaction in a simple list there is no controller!
Now we just wire everything up:
$template = new Template('postList.tpl');
$posts = new Posts($pdo);
$model = new EveryPostList($posts);
$view = new ListView($model, $template);
echo $view->output();
Advantage 1: There is no controller. Controller code is unnecessary by definition when there is no user interaction.
Advantage 2: Because the controller isn’t acting as an entry point and template selection is done by the router (or elsewhere within the front controller) it’s easy to use a different template and I don’t need to create another controller, I just wire everything up differently.
Advantage 3: Everything is reusable. Let’s say I only want posts by a specific user. I add a new Application model and just wire everything differently.
class UserPostList implements ListViewModel {
private $posts;
private $userId;
public function __construct(Posts $posts, $userId) {
$this->posts = $posts;
$this->userId = $userId;
}
public function getRecords() {
return $this->posts->findByField('userId', $this->userId, 'date DESC');
}
}
I don’t need to add a new controller just to achieve this, I just wire all the existing components differently:
$template = new Template('postList.tpl');
$posts = new Posts($pdo);
$model = new UserPostList($posts, 123);
$view = new ListView($model, $template);
echo $view->output();
Everything has been reused but I can wire this exact code with a different template, a different model, a view which paginates the list, etc.
Anywhere I want a list of posts I can use this View and template combination along with the relevant application model that bridges the gap to the domain reducing repeated code dramatically.
Advantage 4: Because the controller isn’t doing View Selection, nesting views works with ease. Let’s say I want to compare the posts of two users side by side. I already have the ability to get a list of all posts by a specific user, so that code can easily be reused:
class NestedView {
private $views = array();
private $template;
public function __construct(Template $template) {
$this->template = $template;
}
public function addView($name, View $view) {
$this->views[$name] = $view;
}
public function output() {
$this->template->assign('children', $this->views);
return $this->template->output();
}
}
And it just gets wired up:
$posts = new Posts($pdo);
$view = new NestedView(new Template('compareTwoUsers.tpl');;
$view->addView('user1', new ListView(new UserPostList($posts, 123), new Template('postList.tpl')));
$view->addView('user2', new ListView(new UserPostList($posts, 321), new Template('postList.tpl')));
echo $view->output();
The template would then do something like:
<div style="width: 50%; float: left">
<h2>First user's posts:</h2>
<?php echo $this->children['user1']->output(); ?>
</div>
<div style="width: 50%; float: left">
<h2>Second user's posts:</h2>
<?php echo $this->children['user2']->output(); ?>
</div>
This is for demonstration, In reality I have a helper function in the template which builds the nested views. I basically supply a route such as $this->getView(“/usersposts/123”) and it uses the same router as the main application to build the MVC triad and return the view.
Advantage 5: Controllers become agnostic of everything else.
It’s not unreasonable that in the example above I’d want to show the posts of a specific user rather than hardcoding it at the wiring stage! In fact, it’s almost certain. This is information that’s needed from the user and as such is where the controller is genuinely needed.
Let’s take our “Posts by a specific user” ViewModel from earlier, and amend it slightly:
class UserPostList implements ListViewModel {
private $posts;
private $userId;
public function __construct(Posts $posts) {
$this->posts = $posts;
}
public function setUser($id) {
$this->userId = $id;
}
public function getRecords() {
return $this->posts->findByField('userId', $userId, 'date DESC');
}
}
I’ve made the userId mutable. It’s the controllers job to set this state based on user input:
class UserPostController {
private $userPostList;
public function __construct(UserPostList $userPostList) {
$this->userPostList = $userPostList;
}
public function setUser($id) {
$this->userPostList->setUser($id);
}
}
Imagine the router is routing /user/postlist/123 to $userPostController->setUser(123)
Trivial stuff, but it’s powerful. The viewModel doesn’t care where/how it’s being used. The view doesn’t need a controller in order to exist and all the classes in the system really are this simplistic.
Ok, that’s simple. Let’s go back to the nested view. I’ll have a route such as /user/postcompare/123/321 to compare the posts of user 123 with user 312. I simply create a controller to allow for it:
class UserPostCompareController {
private $userPostList1;
private $userPostList2;
public function __construct(UserPostList $userPostList1, UserPostList $userPostList2) {
$this->userPostList1 = $userPostList1;
$this->userPostList2 = $userPostList2;
}
public function setUsers($user1, $user2) {
$this->userPostList1->setUser($user1);
$this->userPostList2->setUser($user2);
}
}
The two instances of UserPostList would be the same as the ones passed to the nested view:
$posts = new Posts($pdo);
$view = new NestedView(new Template('compareTwoUsers.tpl');;
$postList1 = new ListView(new UserPostList($posts), new Template('postList.tpl'));
$postList2 = new ListView(new UserPostList($posts), new Template('postList.tpl'));
$view->addView('user1', $postList1);
$view->addView('user2', $postList2);
$controller = new UserPostCompareController($postList1, $postList2);
//The dispatcher would do this
$controller->setUsers($_GET['arg1'], $_GET['arg2']);
echo $view->output();
And here, everything is reusable. Essentially any view can pull in any view with ease because Views don’t have their logic tied up in controllers.
Advantage 6: Actually this is more of a side-effect of better separation of concerns, but it’s still an advantage: Controllers, views and models don’t need to extend from weighty base classes. Controller, view and ViewModel code in my framework really is that sparse. You program the bare minimum around interfaces.
Going back to the original user list, I can add a ViewModel and a Controller to deal with any set of criteria, e.g. searching the posts
class SearchablePostsList implements ListViewModel {
private $posts;
private $criteria;
public function __construct(Posts $posts) {
$this->posts = $posts;
}
public function setCriteria($criteria) {
$this->criteria = $critera;
}
public function getRecords() {
//Query the model based on the criteria
$return $this->posts->find('......');
}
}
class SearchablePostsController {
private $model;
public function __construct(SearchablePostsList $model) {
$this->model = $model;
}
public function main() {
//Do something with posts and set:
$this->model->setCriteria('...whatever!');
}
}
Any part of the system can be reused, I can reuse these viewmodels with different controller (perhaps one that uses GET and one that uses POST), I can reuse the controllers with different views. I can substitute the model entirely. For example. I might make a domain model which extends Posts and always filters out posts by a specific user unless you’re logged in as an admin (All domain logic) and pass it in as $posts when I wire everything up.
I’m afraid I don’t have time to go any further this evening but feel free to ask any specific questions! Hopefully this will help shed some light on this approach.
I’d like to see how you’d achieve similar in Symfony without ugly hacks such as invoking a controller when there hasn’t been any user input, or ugly inheritance trees.