-
Notifications
You must be signed in to change notification settings - Fork 1
[!!] This is a dump of the old README. It contains information discussed elsewhere in this guide too.
As I hope we all know by now views make up part of the MVC triad that Kohana and other frameworks adhere to. Logic separation is important to keeping our applications sane. When I say sane I mean concise, focused and extensible.
So we have logic separation in our Kohana applications but
a recent surge of interest in the Kostache module
hints that developers need more separation than is
currently provided by Kohana_View.
Let's take a simple example of a CMS page Controller using the standard Kohana_View class.
<?php
class Controller_Page extends Controller {
public function action_view()
{
$slug = $this->request->param('slug');
$model = Model::factory('Page', array('slug' => $slug));
$view = View::factory('page', array(
'title' => $model->title,
'navigation' => array(
array(
'url' => Route::url('page', array('slug' => 'home')),
'label' => 'Home',
),
array(
'url' => Route::url('page', array('slug' => 'about')),
'label' => 'About',
),
),
'content' => $model->content,
));
$this->response->body($view);
}
}A simple example where we set 3 values to the view. Of
course I could have passed in the model and then referenced
$model->title and $model->content from within the
template.
This is all good and proper until you start adding more data to the array. What if you want to define your JavaScript and CSS files? Add pagination?
You could keep your controllers lighter by using HMVC to
move your navigation building to another action. You could
move the navigation definition into the template itself.
You might even describe your navigation as a model and pass
in both Model_Page and Model_Navigation to your
template.
None of these solutions are appropriate or considered in my opinion. Too many HMVC calls across your application prove for an unneccessary overhead. Moving navigation into the template couples the navigation and template, what happens if you want to reuse the navigation on another action? You could move the navigation array into a method in your controller, but it's really not worth convoluting all these various areas when this stuff is clearly related to the view!
There are two requirements for providing safe view structured logic:
- Remove complex logic from templates completely
- Use an object to describe view logic.
I cannot be bothered to explain MVVM right now any anyway
wikipedia does a pretty good job. The long and
short of it is, you can place your view specific logic into
ViewModel classes.
You could put the ViewModel principle directly into your
application today without any module! Take this example:
<?php
class View_Page {
protected $_slug;
protected $_page;
public function __construct($slug)
{
$this->_slug = $slug;
}
protected function page()
{
if ($this->_page === NULL)
{
$this->_page = Model::factory('Page', array('slug' => $this->_slug));
}
return $this->_page;
}
public function title()
{
return $this->page()->title;
}
public function navigation()
{
return array(
array(
'url' => Route::url('page', array('slug' => 'home')),
'label' => 'Home',
),
array(
'url' => Route::url('page', array('slug' => 'about')),
'label' => 'About',
),
);
}
public function content()
{
return $this->page()->content;
}
}The class above defines a basic CMS page using 3 public
methods ::title(), ::navigation() and ::content().
It provides a lazy loading mechanism to Model_Page and
that's about it! The template is brief too:
<!doctype html>
<html>
<head>
<title><?= $view->title() ?></title>
</head>
<body>
<h1><?= $view->title() ?></h1>
<ul>
<? foreach($view->navigation() as $_link): ?>
<li>
<a href="<?= $_link['url'] ?>"><?= $_link['label'] ?></a>
</li>
<? endforeach; ?>
</ul>
<?= $view->content() ?>
</body>
</html>So you ask,
How do you actually get this working in my applications?
Easily, within your Controller_Page:
<?php
class Controller_Page extends Controller {
public function action_view()
{
$slug = $this->request->param('slug');
$view = new View('page', array('view' => new View_Page($slug)));
$this->response->body($view);
}
}Look how slim that action is! And you can do this today without requiring anything else. You now have:
- A skinny action
- A clean template
- View logic defined as a class
Now you realise the power of the ViewModel is already in your hands, so why use Beautiful View?
Well firstly I would like to mention you may not need it. You can just use a ViewModel as shown above. This module was built to serve my own needs:
- Even cleaner Controller actions
- Use Mustache template rendering, because:
- Removes all logic from templates
- Auto escapes all data by default
- Looks prettier than PHP
- Render ViewModel data as JSON
- Not break existing Kohana_View functionality
If you have similar requirements then maybe Beautiful View is for you. Let me introduce you to her:
<?php
class Controller_Page extends Controller {
public function action_view()
{
$slug = $this->request->param('slug');
$view = new View('page', new View_Page($slug));
$this->response->body($view);
}
}Oh wow, so you saved 17 extra characters... big deal!
Well that is not all we have done my friend, we also added a line to our bootstrap:
<?php
Template::$default_class = 'Template_Mustache';<?php
class View_Page extends ViewModel {
// ...
}We have set our default Template class to use Mustache and
our View_Page now extends ViewModel.
Hmmm, interesting?! What does this mean for the template?
<!doctype html>
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<ul>
{{#navigation}}
<li>
<a href="{{url}}">{{label}}</a>
</li>
{{/navigation}}
</ul>
{{{content}}}
</body>
</html>Woah cool! It's just like using
Kostacheor vanilla Mustache.
Indeed it is. You can also swap out the template in the Controller like so:
<?php
class Controller_Page extends Controller {
public function action_view()
{
$slug = $this->request->param('slug');
$template = 'page';
if ($this->request->is_ajax())
{
$template = new Template_JSON($template);
}
$view = new View($template, new View_Page($slug));
$this->response->body($view);
}
}If the first parameter is a string path then it will be
encapsulated into a Template object.
Template::$default_class will be used in this case.
You can then override this setting when calling
View::render() by passing a Template object as the
first parameter.
If the parameter you pass View::__construct() is a
Template object then this will be used unless it is
overridden when calling View::render().
As you can see in my example above if the request is AJAX
Template_Json is used. Otherwise the Template_Mustache
will be used due to Template::$default_class being set
in our bootstrap.
Have a look near the bottom of this README for my ideas on how Template_JSON works.
A path to a template must always be set. It is not assumed
from the ViewModel class name, nor set by any property
in that class. This could change if someone persuades me.
Although Beautiful View does not extend upon Kohana_View it
does steal it's namespace of View. My reasons behind this
are:
- I would be happy for Kohana to incorporate this into the core and therefore it is being developed as a replacement from the outset.
- Want all my current PHP templates to benefit from
improvements via
Template_PHP. -
Viewmakes more sense thanKostache.
You can read more into side effects by reading
Beautiful_View and Template_PHP. However here are a few
listed for clarity:
-
View::__construct()andView::factory()can take aTemplateobject as their first parameter as well as a string path. -
View::__construct()andView::factory()can take aViewModelobject as their first parameter as well as an array of data. -
View::__construct()andView::factory()will initialise aViewModelinstance if you pass an array as the second parameter to store the data in. -
View::__construct()andView::factory()will initialise aTemplate_PHPinstance if you pass an array as the second parameter. -
View::bind(),View::set()and their magic counterparts will attach data to theViewModel. Ideally you would set data directly to theViewModelinstance however for performance reasons. -
View::set_filename()now callsTemplate::set_filename(). -
View::render()now callsTemplate::render(). -
View::bind_global()is dropped completely because I do not care for these or your application if you use them. -
View::render()can now accept aTemplateobject as well as a string path.
The default templating for Beautiful View, it runs in the same vein as Kohana_View's template rendering.
If you pass Beautiful_View an array of data then this will
be placed into a vanilla ViewModel. You may also use a
vanilla ViewModel directly. Either way when rendering a
the view data using Template_PHP the ViewModel's
properties will be exposed as variables like Kohana_View.
Using any other object that extends ViewModel the only
variable exposed to the view is the ViewModel inside the
$view variable.
What follows is an example that hopefully spells out the
difference between passing in a class that extends
ViewModel and passing in an array.
<h1><?= $view->title() ?></h1><h1><?= $title ?></h1><?php
class View_Example extends ViewModel {
public function title()
{
return 'View_Example Title';
}
}
echo new View(
new Template_PHP('example/viewmodel'),
new View_Example);
echo new View(
new Template_PHP('example/array'),
array('title' => 'Array Title'));So you see, the templates do differ! This is your warning.
The reason for this is to maintain backwards compatability
with the old View API whilst your new templates can benefit
from accessing the object with $view.
As with Kohana_View your PHP templates need to be placed
in the views folder. You can extend Template_PHP
yourself and overwrite the ::$_dir property to change
this to the more sensible templates folder.
For rendering your mustaches of course :{) It works in a
similar way to that of zombor's Kostache. In fact if you
understand how Mustache.php works you will find that
Template_Mustache is a very simple wrapper.
Mustache templates should be placed in the templates
directory.
It's easier for those of you who use Kostache to just let you know the differences:
- You cannot set partials on the ViewModel.
- The template path is set when you initialise
Template_Mustacheand does not reference the name of yourViewModel. - You cannot set the template path from your
ViewModel. - There is no equivelent to
Kostache_Layoutcurrently.
So why differentiate? Beautiful_View is about the complete
separation of a template from a ViewModel. They should not
really have any idea about each other. This means that
either piece can be interchanged, this flexibility is
awesome especially when flipping between Template_PHP and
Template_JSON.
Partials have to be set directly to the template using
Template_Mustache::partial(). This is because partials
are specific to a template not a ViewModel.
I believe this separation is key although I think that I
will need to come up to some Kostache_Layout equivelent.
Perhaps something like:
<?php
$template = new Template_Mustache(
'post',
array('layout' => 'shared/layout'));
echo new View($template, new View_Post);Whereby the second param of Template_Mustache can be an
array of options. This way partials could be set via the
constructor too. Let me know what you think.
As modern web application developers we tend to use a lot of JSON as a means for communicating via XHR. I really want an easy way to either render a normal HTTP request using Mustache or if the request is XHR then to send the only the ViewModel data as JSON.
Now I could just call all public methods and place these into a JSON object but that doesn't give you the control that you might need to call only a certain amount of data.
This flexibility could be provided by some kind of JSON
template file. To this end I have come up with a simple
solution. You do actually write JSON templates. The top
level properties of the JSON object are used to look up
your ViewModel's public methods and properties. The
values are used as defaults if the public method or
property does not exist.
Here is a JSON template as I imagine it:
{
"title" : "Unknown Title",
"navigation" : [],
"content" : ""
}There you have it! Then your ViewModel would be used to populate these values if set. I think it could be awesome!