popphp / pop-view
Pop View Component for Pop PHP Framework
Requires
- php: >=8.4.0
- popphp/pop-filter: ^5.0.0
- popphp/pop-utils: ^3.0.0
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^12.5.0
README
Overview
pop-view is the view template component that can be used as the "V" in an MVC stack or
independently as well. It supports using both PHP-file based templates and stream templates.
Within the stream templates, there is basic support for logic and iteration for dynamic
control over the view template.
pop-view is a component of the Pop PHP Framework.
Install
Install pop-view using Composer.
composer require popphp/pop-view
Or, require it in your composer.json file
"require": {
"popphp/pop-view" : "^5.0.0"
}
Quickstart
Consider a phtml template file like this:
<html>
<body>
<h1><?=$title; ?></h1>
</body>
</html>
You can set up a view object and populate data like this:
use Pop\View\View; $view = new View('hello.phtml'); $view->title = 'Hello World!'; echo $view;
View auto-detects the template type from what you pass it: a path ending in .phtml/.php that
exists on disk becomes a file template, and anything else (an .html path, or a raw template string)
becomes a stream template - no need to explicitly wrap it in Template\File/Template\Stream
yourself unless you want more control, such as passing a
cache directory to a stream template. See the
File Template and Stream Template sections below for the
explicit form.
Data can also be passed directly to the constructor instead of being set afterward:
$view = new View('hello.phtml', ['title' => 'Hello World!']);
which will produce:
<html> <body> <h1>Hello World!</h1> </body> </html>
File Template
A file template simply uses PHP variables to deliver the data and content to template to be rendered. With a file template, you have full access to the PHP environment to write any additional code or helper scripts. However, in using this, you must make sure to adhere to the best practices and standards regarding the security of the application.
hello.phtml
<!DOCTYPE html> <html> <head> <title><?=$title; ?></title> </head> <body> <h1><?=$title; ?></h1> <p><?=$content; ?></p> </body> </html>
You can set up the view object like this:
use Pop\View\View; use Pop\View\Template\File; $view = new View(new File('hello.phtml')); $view->title = 'Hello World!'; $view->content = 'This is a test!'; echo $view;
Stream Template
A stream template uses a formatted string placeholder to deliver the data and content to template to be rendered:
hello.html
<!DOCTYPE html> <html> <head> <title>[{title}]</title> </head> <body> <h1>[{title}]</h1> <p>[{content}]</p> </body> </html>
You can set up the view object in a similar way and it will render the exact same as the file template example.
use Pop\View\View; use Pop\View\Template\Stream; $view = new View(new Stream('hello.html')); $view->title = 'Hello World!'; $view->content = 'This is a test!'; echo $view;
A value nested one level down in an array can be substituted directly with [{name[index]}],
without having to flatten the data first:
<p>Welcome, [{user[name]}]!</p>
$view = new View(new Stream('welcome.html'), ['user' => ['name' => 'Nick']]);
Includes
Stream templates support includes to allow you to include other templates within them.
header.html
<!-- header.html //--> <!DOCTYPE html> <html> <head> <title>[{title}]</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <header>This is the header</header>
footer.html
<!-- footer.html //--> <footer>This is the footer</footer> </body> </html>
index.html
<!-- index.html //--> {{@include header.html}} <h1>[{title}]</h1> <p>[{content}]</p> {{@include footer.html}}
You can set up the view object like before:
use Pop\View\View; use Pop\View\Template\Stream; $view = new View(new Stream('index.html')); $view->title = 'Hello World!'; $view->content = 'This is a test!'; echo $view;
Inheritance
Stream templates support inheritance to allow you to extend other templates.
parent.html
<!-- parent.html //--> <!DOCTYPE html> <html> <head> {{header}} <title>[{title}]</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> {{/header}} </head> <body> <h1>[{title}]</h1> [{content}] </body> </html>
child.html
<!-- child.html //--> {{@extends parent.html}} {{header}} {{parent}} <style> body { margin: 0; padding: 0; color: #bbb;} </style> {{/header}}
You can set up the view object like before:
use Pop\View\View; use Pop\View\Template\Stream; $view = new View(new Stream('child.html')); $view->title = 'Hello World!'; $view->content = 'This is a test!'; echo $view;
A parent template isn't limited to a single named block - declare as many {{blockname}}...{{/blockname}}
pairs as you need, and a child can override any subset of them:
<!-- parent.html //--> <head> {{header}} <title>[{title}]</title> {{/header}} </head> <body> {{sidebar}} <aside>Default sidebar</aside> {{/sidebar}} </body>
<!-- child.html //--> {{@extends parent.html}} {{header}} {{parent}} <meta name="child" content="1" /> {{/header}} {{sidebar}} <aside>Child sidebar</aside> {{/sidebar}}
Iteration
Iteration is possible in stream templates when working with arrays and array-like objects.
<!-- index.html //--> <!DOCTYPE html> <html> <head> <title>[{title}]</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> [{items}] <div><strong>[{key}]</strong>: [{value}]</div> [{/items}] </body> </html>
use Pop\View\View; use Pop\View\Template\Stream; $data = [ 'items' => [ 'hello' => 'world', 'foo' => 'bar', 'baz' => 123 ] ]; $view = new View(new Stream('index.html'), $data); echo $view;
A loop over a plain, numerically-indexed list of records works the same way, except each entry's own
fields are addressed directly by name instead of via [{key}]/[{value}]. [{i}] is available in
either form and gives the 1-indexed position within the loop:
[{rows}]
<div class="row-[{i}]">
<h4>[{title}]</h4>
<p>[{content}]</p>
</div>
[{/rows}]
$data = [ 'rows' => [ ['title' => 'First Post', 'content' => 'Some content here.'], ['title' => 'Second Post', 'content' => 'Some more content.'], ] ];
A loop entry whose own value is itself an array, keyed by a non-numeric name, is treated as a nested sub-loop - declare a tag with that same name inside the outer loop's body:
[{items}]
[{pages}]
<p>[{value}]</p>
[{/pages}]
[{/items}]
$data = [ 'items' => [ 'pages' => ['Page One', 'Page Two', 'Page Three'] ] ];
Conditionals
Conditional logic is possible within a stream template as well.
<!-- index.html //--> <!DOCTYPE html> <html> <head> <title>[{title}]</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> [{if(foo)}] <p>The variable 'foo' is set to [{foo}].</p> [{else}] <p>The variable 'foo' is not set.</p> [{/if}] </body> </html>
use Pop\View\View; use Pop\View\Template\Stream; $data = ['foo' => 'bar']; $view = new View(new Stream('index.html'), $data); echo $view;
A nested array value can be checked and printed with the same [{name[index]}] syntax used for
array-index scalars outside of a conditional:
[{if(user[name])}]
<p>Hello, [{user[name]}]!</p>
[{else}]
<p>Hello, guest!</p>
[{/if}]
$data = ['user' => ['name' => 'Nick']];
Conditionals also work inside a loop body, evaluated per-row against that row's own fields:
[{rows}]
<div>
[{if(featured)}]<strong>[{title}] (Featured)</strong>[{else}][{title}][{/if}]
</div>
[{/rows}]
$data = [ 'rows' => [ ['title' => 'First Post', 'featured' => true], ['title' => 'Second Post'], ] ];
Compiled & Cached Templates
By default, a stream template is re-parsed on every render() call. For templates rendered
repeatedly (e.g. on every request), you can opt in to a compiled/cached render path by giving
Template\Stream a writable cache directory - either as the constructor's second argument, or via
setCacheDir():
use Pop\View\View; use Pop\View\Template\Stream; $view = new View(new Stream('index.html', '/path/to/cache/dir'), $data); echo $view;
// or, equivalently: $template = new Stream('index.html'); $template->setCacheDir('/path/to/cache/dir'); $view = new View($template, $data);
The first render compiles the template into plain PHP and writes it to the cache directory, keyed by
a hash of the fully-resolved template content (after @extends/@include/blocks have all been
merged in). Subsequent renders - including across requests, once the compiled file exists on disk -
skip parsing entirely and just include the compiled PHP file. The cache is automatically
invalidated whenever the resolved template content changes, so there's nothing to clear by hand
during normal development.
Every placeholder, loop, and conditional shown above is supported identically on the compiled path - switching a template over to a cache directory is a pure performance opt-in with no change in rendered output.
Template\File templates don't need this: they're already plain PHP, included directly with no
separate parsing step.
Filters
View can run its data through one or more Pop\Filter
filters before rendering, useful for things like sanitizing output. Pass a filter (or an array of
filters) as the third constructor argument, or add them afterward:
use Pop\View\View; use Pop\Filter\Filter; $view = new View('hello.html', [ 'title' => '<b>Hello World</b>', ], new Filter('strip_tags')); echo $view; // <b> tags stripped from every value in the data
$view = new View('hello.html', $data); $view->addFilter(new Filter('strip_tags')); $view->addFilters([ new Filter('htmlentities', [ENT_QUOTES, 'UTF-8']), ]);
A filter can be excluded from specific fields by name, so it only applies to the rest of the data:
$view = new View('hello.html', [ 'title' => '<b>Hello</b>', 'content' => '<b>World</b>', ]); // 'content' is left untouched; only 'title' gets stripped of tags $view->addFilter(new Filter('strip_tags', null, 'content'));
Other available methods: hasFilters(), getFilters(), and clearFilters() to remove all
configured filters.
Working with View Data
Besides passing data through the constructor, View supports a few equivalent ways of getting data
in and out, since it extends Pop\Utils\ArrayObject:
use Pop\View\View; $view = new View('hello.html'); // Property access $view->title = 'Hello World!'; // Array access $view['content'] = 'This is a test!'; // Explicit accessors $view->set('foo', 'bar'); echo $view->get('foo'); // Merge additional data in without disturbing what's already set $view->merge(['content' => 'This is a test!']); // Replace all data at once $view->setData(['title' => 'Hello World!']); // Read everything back $allData = $view->getData();