kohana-view
provides separation of view logic and templating for PHP applications. It's designed to be used with the
Kohana framework - but you should be able to use it with most PHP projects with a little work., (*1)
, (*2)
For legacy projects, it can coexist with the standard Kohana View
class, but it is not in any way compatible - each
view needs to use either the stock View
or be updated to work with kohana-view
. In particular, there are significant
differences in how we approach page layout views compared to the stock Kohana Controller_Template
., (*3)
Why you should use it
- Class-based views keep logic out of your controllers, and out of your templates
- Easier to find and update all the display logic in your application
- Easier to customise display logic for modules, configurable sections of applications, etc
- Make the dependencies of each View more obvious and easier to maintain
- Automatically escape all view variables when output as HTML (by default, can be disabled)
- Clean, well-structured code with no global state is easier to test and less at risk of error
- Fully unit tested
Installation
Install with composer: $> composer require ingenerator/kohana-view
, (*4)
Add to your application/bootstrap.php
:, (*5)
Kohana::modules([
'existing' => 'existing/modules/call/here',
'kohana-view' => __DIR__.'/../vendor/ingenerator/kohana-view'
]);
We also recommend using a dependency injection container / service container to manage all the dependencies in your
project. Kohana-view doesn't require one in particular, but comes with configuration for
zeelot/kohana-dependencies. Examples in this readme assume you're
using that container, so if you're using something else (really, don't try and do it all inline in PHP) then fetch
dependencies from your container however required., (*6)
Creating your first view
Each view starts with a class implementing the Ingenerator\KohanaView\ViewModel
interface. You can roll your own,
or extend from Ingenerator\KohanaView\ViewModel\AbstractViewModel
for a base class with some useful common
functionality. View classes can be named anything you like, with or without namespaces, whatever., (*7)
NULL,
];
protected function var_is_morning()
{
$date = new \DateTime;
return ($date->format('H')
<html>
<head><title>Hello =$view->name;?></title></head>
<body>
is_morning): ?>
Good Morning,
Good Afternoon,
=$view->name;?> - notice how I HTML escaped your name?
Never render user-provided content unescaped like this: = raw($view->name);?>. Just do that if you're including
other views or known-safe HTML content.
</body>
</html>
To render this view in a controller response you'd do something like this:, (*8)
<?php
//application/classes/Controller/Welcome.php
class Controller_Welcome extends Controller
{
public function action_index()
{
$view = new View\Hello\WorldView;
$view->display(['name' => $this->request->query('name')]);
$renderer = $this->dependencies->get('kohanaview.renderer.html');
/** @var \Ingenerator\KohanaView\Renderer\HTMLRenderer */
$this->response->body($renderer->render($view));
}
}
Template mapping
All templates are loaded from the /views path in the Cascading File System, using the same rules as the rest of Kohana
to select the appropriate version when a template is present in one or more modules/application directories., (*9)
By default, the template is selected based on the name of the view class. Namespace separators and underscores become
directory separators, CamelCased words become under_scored, and View/ViewModel is stripped from beginning and end. For
example:, (*10)
View Class |
Template File (within /views/ in CFS) |
Helloworld |
helloworld.php |
Hello_World |
hello/world.php |
HelloWorld |
hello_world.php |
View_Hello_World |
hello/world.php |
View\HelloWorldView |
hello_world.php |
If you want to customise this globally you can provide an alternate implementation of the ViewTemplateSelector
class
used by the TemplateManager
., (*11)
Sometimes, however, you just want to customise it for a single view - either because the default mapping isn't ideal for
some reason or because the template depends on the result of some view logic. In that case you can implement the
TemplateSpecifyingViewModel
interface on your ViewModel
and explicitly tell the rendering engine which template
to use., (*12)
Page Layout / Page Content
Kohana-view provides out-of-the-box support for the common case where you have a number of page content views that you
want to render within a (or possibly a chain of) containing page layout view(s). This is similar to Kohana's stock
Controller_Template, except that it supports a recursive rendering model where each view can be contained in another up
to the final overall site template., (*13)
For example, this would allow you to have a set of content-area views, a view that renders any of these content areas
inside a layout with a sidebar and a main area, and a further parent view that renders your overall page header/footer/
etc. As with the old Controller_Template, for AJAX requests the renderer by default only renders the content-area view
and not any of the containing template(s) - this can be customised. This also means you can have your controller extend
any arbitrary base class., (*14)
To use PageLayoutRenderer
you need a minimum of two views - one implementing PageLayoutView
and one implementing
PageContentView
. Note that these interfaces are now deprecated in favour of the more flexible NestedChildView
and
NestedParentView
and will be removed in a future release., (*15)
You may want to extend from the provided AbstractIntermediateLayoutView
and AbstractNestedChildView
though this is
in no way compulsory. Your top-level page view should be an instance of PageLayoutView
., (*16)
sidebar = $sidebar;
}
protected function var_sidebar()
{
return $this->sidebar;
}
}
```
```php
NULL
];
protected function var_page()
{
// If you want to make this available to set things from the view : it's not required
return $this->getUltimatePageView();
}
}
```
```php
<html>
<head><title>=$view->title;?></title></head>
<body>=raw($view->body_html); // Good usecase for rendering unescaped content?></body>
</html>
<?php
//application/views/content_with_sidebar_layout.php
/**
* @var \View\Layout\ContentWithSidebarLayout $view
* @var \Ingenerator\KohanaView\Renderer\HTMLRenderer $renderer
*/
?>
<div class="row">
<div class="sidebar"><?=raw($renderer->render($view->sidebar));?></div>
<div class="content"><?=raw($view->child_html);?></div>
</div>
<?php
//application/views/pages/hello_world.php
/**
* @var \View\Pages\HelloWorldView $view
* @var \Ingenerator\KohanaView\Renderer\HTMLRenderer $renderer
*/
// You can do this here if you want to keep templatey-type stuff together
// Or in your view model at display time if it's a bit more involved
$view->page->setTitle('Hello World');
?>
<h1>Hi <?=$view->name;?></h1>
display(['name' => $this->request->query('name')]);
$renderer = $this->dependencies->get('kohanaview.renderer.page_layout');
/** @var \Ingenerator\KohanaView\Renderer\PageLayoutRenderer $renderer */
$this->response->body($renderer->render($content));
}
}
```
Advanced examples
-----------------
### Default variables
As standard, Views require that the array passed to `AbstractViewModel->display()` contains values for all defined variables.
This is to ensure that the view model is always in the correct state even if it is rendered multiple times (as often happens
with partials and sub-views).
You can define optional view variables by populating the `$default_variables` array in your ViewModel. Note that these
defaults **will be reassigned** to the `$variables` array on every call to `->display()` to ensure that they are always in
expected state.
```php
class View_Something extends AbstractViewModel {
protected $default_variables = [
'title' => 'My page title',
];
protected $variables = [
'caption' => NULL
];
}
print $view->title; // 'My page title'
print $view->caption; // ''
$view->display(['caption' => 'Something', 'title' => 'A title']);
print $view->title; // 'A title'
print $view->caption; // 'Something'
$view->display(['caption' => 'Something else']);
print $view->title; // 'My page title'
print $view->caption; // 'Something else'
```
### Caching variables
Views that extend `AbstractViewModel` expose all the variables in their `$variables` array and also any dynamic
variables provided by `var_variable_name` methods. The `$variables` array takes precedence over dynamic methods which
means you can also use it as a cache for calculated variables that only need to be calculated once for each view
rendering:
```php
''
];
protected function var_user_activity()
{
$activity = [];
foreach ($this->database->loadActivityForUser($this->user_email) as $activity) {
$activity[] = (string) $activity;
}
$this->variables['user_activity'] = $activity;
// Future usage of $view->user_activity will now get the value cached in the variables array without calling
// this method again.
return $activity;
}
}
```
The variables array is cleared with every call to `display`, so values cached in this way will be cleared every time you
provide new view data (eg if rendering a view in a loop).
### Rendering nested views (partials)
The containing view model should expose a reference to the view model for the partial, which might be passed in as a
constructor dependency, created by a dynamic variable method, or injected in some other way.
View models don't have any reference to the renderer, so they cannot render the partial directly - instead this should
happen in the template using the current renderer that is provided as a variable inside the template scope.
For example:
```php
[],
];
public function __construct(View_User_FaceWidget $face_widget)
{
$this->face_widget = $face_widget;
}
protected function var_face_widget()
{
return $this->face_widget;
}
}
```
```php
users as $user):?>
face_widget->display(['user' => $user]);?>
=raw($renderer->render($view)); // Note rendering unescaped HTML ?>
Configuring whether or not templates are compiled
The template engine automatically compiles your source templates to add the auto-escaping functionality. Compiled
templates are cached on disk for future executions. By default, they are cached within the Kohana::$cache
directory
- alongside your autoloader cache etc - which we recommend should be flushed on every deployment., (*17)
The template manager will always compile templates if they don't exist on disk. However, you can also configure it to
compile on every request - useful in development., (*18)
If you are using the default dependency container then these options are configured for you, including setting
recompile_always = (Kohana::$environemnt === Kohana::DEVELOPMENT)
. You can adjust these settings by adding custom
configuration in application/config/kohanaview.php
- see config/kohanaview.php for the
defaults., (*19)
If you are using your own service container you should configure the $options
argument to your CFSTemplateManager
accordingly., (*20)
Credits
This package is heavily inspired by dyron/kohana-view which itself is a fork of
zombor/View-Model but as of version 2.x has been fully rewritten for a cleaner
and more separated structure using a test-first approach. Thanks and credit to @zombor, @dyron, @nanodocumet and
@slacker for their various contributions to the original packages., (*21)
The 2.x version of this package has been sponsored by inGenerator Ltd, (*22)
Contributing
Contributions are very welcome. Please ensure that you follow our coding style, add tests for every change, and
avoid introducing global state or excessive dependencies. For major or API breaking changes please discuss your idea
with us in an issue first so we can work with you to understand the issue and find a way to resolve it that suits
current and future users., (*23)
Bug fixes should branch off from the earliest (>=2.0) version where they are relevant and we will merge them up as
required. New features should branch off from the development branch of the current version., (*24)
Licence
Licensed under the BSD-3-Clause Licence, (*25)