Provides view support to ICanBoogie

v0.10.0 2022-04-13 21:05 UTC

This package is auto-updated.

Last update: 2024-11-29 13:41:37 UTC


README

Release Code Coverage Downloads

The icanboogie/view package provides the view part of the model-view-controller (MVC) architectural pattern. It extends the features of the icanboogie/routing package—more precisely its controllers—and together with the icanboogie/render package it helps in separating presentation from logic.

Installation

composer require icanboogie/view

Getting started

Before you get started you'll need to define some prototype methods to bind some render components to View instances, and View instances to the Controller instances that use them.

If you use the icanboogie/view package with ICanBoogie, you can simply require the icanboogie/bind-view package and let it deal with bindings.

The following code demonstrates how to bind view prototype property of Controller instances. The binding is defined by the ControllerBindings traits.

<?php

use ICanBoogie\Prototype;
use ICanBoogie\Routing\Controller;
use ICanBoogie\View\View;

use function ICanBoogie\Render\get_renderer;

Prototype::bind([

	Controller::class => [

		'lazy_get_view' => function(Controller $controller) {

			$view = new View($controller, get_renderer());

			new View\AlterEvent($view);

			return $view;

		}

	]

]);

Views and controllers

Views are associated with controllers through the lazy getter view, thus $this->view is all is takes to start a view inside a controller. The view then waits for the Controller::action event, to perform its rendering.

The following example demonstrates how a query of some articles is set as the view content, a title is also added to the view variables:

<?php

use ICanBoogie\Routing\Controller;
use ICanBoogie\Module\ControllerBindings as ModuleBindings;
use ICanBoogie\View\ControllerBindings as ViewBindings;

class ArticlesController extends Controller
{
	use Controller\ActionTrait, ViewBindings, ModuleBindings;

	protected function action_index()
	{
		$this->view->content = $this->model->visible->ordered->limit(10);
		$this->view['title'] = "Last ten articles";
	}
}

Note: The model getter is provided by the icanboogie/module package, and is only available if the route has a module property, which is automatic for routes defined by modules.

The assign() method may be used to assign multiple values to the view with a single call:

<?php

$content = new SignupForm;
$title = "Sign up";
$params = $_POST;

$this->view->assign(compact('content', 'title', 'params'));

Altering the view before it is returned to the controller

The event View::alter of class View\AlterEvent is fired when the instance is created by the view getter. Event hooks may used this event to alter the view before it is returned to the controller.

The following example demonstrates how a view can be altered before it is returned to the controller. If the route has a module property, the "template" directory of the module is added to the template resolver:

<?php

use ICanBoogie\PropertyNotDefined;
use ICanBoogie\View\View;

$app->events->attach(function(View\AlterEvent $event, View $view) use ($app) {

	try
	{
		$module_id = $view->controller->route->module;
	}
	catch (PropertyNotDefined $e)
	{
		// if the property is not defined we just return

		return;
	}

	// adding a template path
	$view->template_resolver->add_path($app->modules[$module_id]->path . 'templates');

	// adding a variable
	$view['log'] = $app->log->messages;

	// altering the layout
	if ($app->is_mobile)
	{
		$view->layout .= '.mobile';
	}

});

Rendering a view

Views are rendered using templates and layouts. Templates render the content of views, while layouts decorate the templates. For instance an "articles/list" template would be used to render a collection of articles, while a "page" layout would be used to decorate that rendered collection with the layout of a website.

The template used to present the content of the view is resolved as follows:

  • From the template property of the view.
  • From the template property of the route.
  • From the template property of the controller.
  • From the name and action of the controller, if the controller has an action property e.g. "articles/show".

The layout used to decorate the template is resolved as follows:

  • From the layout property of the view.
  • From the layout property of the route.
  • From the layout property of the controller.
  • "admin" if the identifier of the route starts with "admin:".
  • "home" if the pattern of the route is "/" and the template exists.
  • "page" if the template exists.
  • "default" otherwise.

Because the template and layout properties are lazily created, you can define them instead of letting View find the right template names. The following example demonstrates how to cancel the template and define "admin" as layout:

<?php

use ICanBoogie\Routing\Controller;
use ICanBoogie\View\ControllerBindings as ViewBindings;

class ArticlesController extends Controller
{
	use Controller\ActionTrait;
	use ViewBindings;

	// …

	protected function action_index()
	{
		$this->view->content = $this->model->visible->ordered->limit(10);
		$this->view->template = null;
		$this->view->layout = "admin";
	}

	// …
}

The templates and layouts are usually specified as names e.g. "page" or "articles/show", and not by path e.g. "/path/to/my/template.phtml". A template resolver and an engine collection are used to resolve these names into pathname, and the engine collection is used to render the templates with the appropriate engines. The reason for this is that templates are usually defined as a hierarchy in your application, and using this hierarchy they can be replaced to better suit your application.

For instance, the framework ICanBoogie decorates the default template resolver to add additional features, and also to add the application directories to the template resolver.

Please take a look at the icanboogie/render package for more details about template resolvers and engine collections.

Providing a cached result

The event View::render:before of class View\BeforeRenderEvent is fired before a view is rendered. Event hooks may use this event to provide a cached result and save the cost of rendering.

The following example demonstrates how an event hook may provide a cached result of a previously rendered view. Because the JSON of a view instance includes its template, layout, and variables, its hash is perfect as cache key:

<?php

use ICanBoogie\View\View;

/* @var $storage \ICanBoogie\Storage\Storage */

$app->events->attach(function(View\BeforeRenderEvent $event, View $view) use ($storage) {

	$hash = hash('sha256', json_encode($view));
	$result = $storage->retrieve($hash);

	if ($result !== null)
	{
		$event->result = $result;

		return;
	}

	$event->result = $result = $view->render();
	$storage->store($hash, $result);
	$event->stop();

});

Rendering JSON and stuff

Views are often used to render HTML, but they can also render JSON, XML and other nice things, and it's rather simple since all you have to do is alter the Response instance of your controller according to what you are rendering. This is not really a View feature, but its something to remember.

The following example demonstrates how the response is altered to suit the JSON response:

<?php

// templates/json.php

/* @var $content mixed */

echo json_encode($content);
<?php

	// …

	protected function action_any_json()
	{
		$this->view->content = $this->model->one;
		$this->view->template = 'json';
		$this->response->content_type = "application/json";
	}

	// …

Cancelling a view

A view can be cancelled when you need to return a different result or when you want to cancel its rendering. Views are automatically cancelled when the controller they are attached to returns a result. A view can also be cancelled by setting the view property of its controller to null.

The following example demonstrates how views can be cancelled using these methods:

<?php

use ICanBoogie\Routing\Controller;
use ICanBoogie\View\ControllerBindings as ViewBindings;
use ICanBoogie\Module\ControllerBindings as ModuleBindings;

class ArticlesController extends Controller
{
	use Controller\ActionTrait, ViewBindings, ModuleBindings;

	protected function action_any_index()
	{
		$this->view->content = $this->model->visible->ordered->limit(10);
		$this->view['title'] = "Last ten articles";
	}

	protected function action_any_json()
	{
		$this->action_any_index();
		$this->response->content_type = "application/json";
		// The view is cancelled to return JSON text
		return json_encode($this->view->content);
	}

	protected function action_head_index()
	{
		$this->action_any_index();
		// The view is cancelled although no result is returned
		$this->view = null;
	}
}

Rendering a partial

The partial() method renders a partial using the view's renderer:

Remember that the view is included in the variables passed to the template.

<?php

$view->partial('articles/overview', [ 'article' => $article ]);

Prototype methods

The following prototypes method are used. The ControllerBindings trait may be used to help hinting code.

  • ICanBoogie\Routing\Controller::lazy_get_view: Returns the View instance associated with the controller and also starts the view magic.

Events

  • ICanBoogie\View\View::alter of class View\AlterEvent: fired when the instance is created by the Controller::view getter. Event hooks may use this event to alter the view before it is returned to the controller.

  • ICanBoogie\View\View::render:before of class View\BeforeRenderEvent: fired before the view is rendered. Event hooks may use this event to provide a cached result.

Continuous Integration

The project is continuously tested by GitHub actions.

Tests Static Analysis Code Style

Code of Conduct

This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you're expected to uphold this code.

Contributing

See CONTRIBUTING for details.