In this case, the view pulls from the model rather than having data pushed into it.
We can turn that around: When is flexiblity a bad thing?
Consider a basic blog. E.g. this one from: http://tutorial.symblog.co.uk/docs/extending-the-model-blog-comments.html#the-controller
class PageController extends Controller
{
public function indexAction()
{
$em = $this->getDoctrine()
->getEntityManager();
$blogs = $em->createQueryBuilder()
->select('b')
->from('BloggerBlogBundle:Blog', 'b')
->addOrderBy('b.created', 'DESC')
->getQuery()
->getResult();
return $this->render('BloggerBlogBundle:Page:index.html.twig', array(
'blogs' => $blogs
));
}
// ..
}
There are a multitude of flexibilty (and by extension reusability) issues here.
1) There is no real user-action here.
This is displaying a static set of data from the model. This could be rewritten without the controller entirely if it was set up like this:
new View('BloggerBlogBundle:Page:index.html.twig', new PageModel);
and the view called a method on the model:
foreach ($this->model->getPages()) {
//...
}
and the model instead had the application state
class PageModel {
public function getPages() {
$em = $this->getDoctrine()
->getEntityManager();
return $em->createQueryBuilder()
->select('b')
->from('BloggerBlogBundle:Blog', 'b')
->addOrderBy('b.created', 'DESC')
->getQuery()
->getResult();
}
}
there is zero need to even instantiate the controller
which brings us neatly to…
2) The application state is stored in the controller rather than the model
This causes a problem when I want to reuse the data selection elsewhere in the application, anywhere I want to display news articles I need what is basically the same controller copied/pasted. Consider some search/sort Options.
class PageController extends Controller
{
public function indexAction()
{
$em = $this->getDoctrine()
->getEntityManager();
$blogs = $em->createQueryBuilder()
->select('b')
->from('BloggerBlogBundle:Blog', 'b')
->addOrderBy('b.created', $_POST['sort'])
->addWhere('b.title LIKE ' . $_POST['searchCriteria'])
->getQuery()
->getResult();
return $this->render('BloggerBlogBundle:Page:index.html.twig', array(
'blogs' => $blogs
));
}
// ..
}
Instead, if this was in the model:
class PageModel {
public $sort;
public $where;
public function getPages() {
$em = $this->getDoctrine()
->getEntityManager();
return $em->createQueryBuilder()
->select('b')
->from('BloggerBlogBundle:Blog', 'b')
->addOrderBy('b.created', 'DESC')
->getQuery()
->getResult();
}
}
then the controller would use the model and not be tied to the underlying data source or the view:
class PageController {
private $model;
public function __construct(PageModel $model) {
$this->model = $model;
}
public function indexAction() {
$this->model->sort = $_POST['sort'];
$this->model->where = 'b.title LIKE ' . $_POST['searchCriteria'];
}
}
This alone has the advantage of decoupling the user input from the model. I can now use this controller with a model that’s pulling data from another source.
Where this becomes particularly useful is where you have repeated functionality. Consider tabular data or forms. With tabular data there is a lot of repeated functionality:
- Change the page
- Sort by a given column
- Change the number of results per page
By separating out the application state from the model state it allows the removal of this repetition by having a reusable controller that can take a model that follows a specific interface
interface TabularData {
public function getData();
public function setSort($field, $dir);
public function setSearch($field, $value);
public function setPage();
public function setRecordsPerPage();
}
class TabularDataController {
private $model;
public function __construct(TabularData $model) {
$this->model = $model;
}
public function sort() {
$this->model->setSort($_POST['sortField'], $_POST['sortDir']);
}
public function page($num) {
$this->model->setPage($num);
}
//...
}
Then I can write any number of models which use the interface:
class PageModel implements TabularData {
private $sortField = 'name';
private $sortDir = 'desc';
private $searchField;
privvate $searchValue;
private $page = 1;
private $
public function getData() {
$em = $this->getDoctrine()
->getEntityManager();
$result = $em->createQueryBuilder()
->select('b')
->from('BloggerBlogBundle:Blog', 'b')
->addOrderBy($this->sortField, $this->sortDir);
if ($this->searchField) $result->addWhere($this->searchField . ' LIKE ' . $this->searchValue);
return $result->getQuery()->getResult();
}
public function setSort($field, $dir) {
$this->sortField = $field;
$this->sortDir = $dir;
}
public function setSearch($field, $value) {
$this->searchField = $field;
$this->searchValue = $value;
}
public function setPage($num) {
$this->page = $num;
}
}
It’s then possible to add models for users, pages, cagegories or anything else that may have tabular data by providing the model to the relevant view:
$model = new PageModel;
$controller = new TabularDataController($model);
$view = new PageView($model);
The view then calls
foreach ($this->model->getData()) {
}
Taking it a step further, if the model had a getHeadings() method, the same view could be reused for any kind of tabular data.
3) The view is hardcoded in the controller.
When the view is hardcoded in the controller it makes it difficult to substitute without repeated code. By selecting the view outside the controller it’s easy to do:
$model = new PageModel;
$controller = new TabularDataController($model);
$view = new PageRssView($model);
$model = new PageModel;
$controller = new TabularDataController($model);
$view = new PageJsonView($model);
With this implementation
- I can now use a different controller with the same view (e.g. one that used GET rather than POST, or fetched the search options from somewhere else like a user’s preferences)
- I can substitute the model so data is retrieved from a different data source
- I can easily substitute the view
All of this becomes difficult when the controller is doing all the work, you either end up with if/elses rather than polymorphism or you end up with repeated code