Developers

Views

Most plugins return data, not markup, and never build a view. Reach for one when the text you are producing is longer than the logic that produces it: a plugin that adds a tab to a group page, a panel to a member profile, or — like plg_bookings_notify — the body of an email. The reason is not tidiness. A layout in a file can be overridden by a template or a super group; a string built inside the class cannot.

Where layouts live

app/plugins/bookings/notify/
    views/
        email/tmpl/message.php
core/plugins/groups/forum/
    views/
        sections/tmpl/display.php
        categories/tmpl/display.php
        categories/tmpl/edit.php
        threads/tmpl/display.php

Under views there is one directory per view name, and inside it a tmpl directory holding one file per layout. Both names are yours to choose; default is the layout used when none is given. The tmpl directory is not optional for a plugin view — Hubzero\Plugin\View searches {plugin}/views/{name}/tmpl and the template override path, and nowhere else.

Creating a view

$this->view($layout, $name) on the plugin returns a Hubzero\Plugin\View configured for this plugin. Note the argument order: layout first, name second. So plg_bookings_notify reaches views/email/tmpl/message.php with

$body = $this->view('message', 'email')
	->set('reservation', $reservation)
	->loadTemplate();

and plgGroupsForum renders views/sections/tmpl/display.php with

$this->view = $this->view('display', 'sections');

Either argument may be omitted. $this->view() gives the default layout of a view named after the plugin itself, so a plugin called notify with a single screen can keep it at views/notify/tmpl/default.php.

Constructing the view class directly works too, and is what the helper does underneath:

$view = new \Hubzero\Plugin\View(array(
	'folder'  => 'bookings',
	'element' => 'notify',
	'name'    => 'email',
	'layout'  => 'message'
));

folder is the plugin group, element is the plugin, name is the view directory, and layout is the file inside tmpl.

Passing data and rendering

set() assigns a variable and returns the view, so calls chain, and loadTemplate() renders the layout and returns the markup as a string:

		return $this->view
			->set('recvEmailOptionID', $recvEmailOptionID)
			->set('recvEmailOptionValue', $recvEmailOptionValue)
			->set('option', $this->option)
			->set('group', $this->group)
			->set('filters', $filters)
			->set('config', $this->params)
			->set('forum', $this->forum)
			->set('sections', $sections)
			->set('categories', $categories)
			->set('edit', $edit)
			->loadTemplate();

Return that string from your event handler. Use loadTemplate(), not display(): display() echoes, which puts the output wherever the buffer happens to be rather than in the response the component asked for.

Assigning to properties works as well as set()$view->group = $group; — and reads the same in the layout.

Writing a layout

<?php
// No direct access
defined('_HZEXEC_') or die();
?>
<?php
$who = User::getInstance($this->reservation->get('created_by'));
echo Lang::txt('PLG_BOOKINGS_NOTIFY_BOOKED_BY', $who->get('name'));
?>

<?php echo $this->reservation->instrument->get('title'); ?>
<?php echo $this->reservation->get('starts'); ?>

Inside the layout, $this is the view. Every variable you set() is a property. $this->escape() is available and should be used for anything that came from the database or the request and is going into HTML; the plain-text mail body above is the case where it is wrong to use it. Layouts run in the global namespace, so Lang, Route, User, and the rest need no imports.

Template overrides

Hubzero\Plugin\View adds a fallback search path so a template can replace any layout:

{template}/html/plg_{group}_{element}/{view name}/{layout}.php

For the forum's sections view under the kimera template that is core/templates/kimera/html/plg_groups_forum/sections/display.php. Note there is no tmpl directory in the override path.

The override path is searched before the plugin's own file, which is what makes it an override — the search paths are a stack and the override is pushed on last. See Overrides.

Sub-views

view() exists on the view class too, with the same layout-first argument order, so a layout can render a partial without repeating the folder and element:

<?php
$this->view('_entry')
     ->set('entry', $entry)
     ->display();
?>

That looks for views/{current view name}/tmpl/_entry.php. Pass a second argument to reach a different view directory. Use display() here, because you do want the output echoed into the enclosing layout; use loadTemplate() if you need the string.

Helpers

Calling an undefined method on a plugin view makes it look for a helper: first a file helpers/{method}.php under the plugin directory, then a class named Plugins\{Group}\{Element}\Helpers\{Method}, falling back to the older Plugin{Group}{Element}Helper{Method}. If the class is invokable it is bound to the view and called. This is how plugins share a formatting routine between several layouts without a global function.

The namespaced form is the one to write. The Plugin…Helper… form is inherited naming kept for the plugins that still use it, and a new helper should not add to them.

Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.