2024-01-16
PHP 8.0+
6.1.1
When you develop a web application, the rendering of views may be a real challenge. Especially if you just want to use only native PHP syntax and avoid external templating language.
This is exactly the goal of PhpEcho
: providing a pure NATIVE PHP TEMPLATE ENGINE with no other dependencies.
PhpEcho
is really easy to use, it's very close to the native PHP way of rendering HTML/CSS/JS.
It is based on an OOP approach using only one class to get the job done.
As you can imagine, using native PHP syntax, it's fast, really fast. No cache is needed to get top-level performances
No additional parsing, no additional syntax to learn !
If you already have some basic knowledge with PHP, that's enough to use it out of the box.
Basically, you just need to define the path of a view file and pass to the instance a set of key-values pairs that will be available on rendering.
The class will manage :
- file inclusions
- extracting and escaping values from any key-values pairs stored in a
PhpEcho
instance - escaping any value on demand (including recursive array (keys and values))
- returning raw values on demand
- the possibility to write directly plain html code instead of using the file inclusion mechanism
- managing and rendering instance of class that implements the magic function
__toString()
- grant access to the global HTML
<head></head>
from any child block - detecting any infinite loop
You'll also be able to extend the engine features writing your own helpers and let your IDE list all your helpers natively just using PHPDoc syntax.
- Installation
- Configuration
- Parameters
- Principles and overview
- Let's start
- Child blocks
- Manipulating and Access to the head tag
- User values
- Auto escaping vars
- Array of PhpEcho instances
- Using a default view
- Use HEREDEOC for HTML
- Use the auto-generated block's id
- Use the component
ViewBuilder
- Advance use: create your own helpers
- Let's play with helpers
composer require rawsrc/phpecho
To use it out of the box, once you have included the class using include_once
or use any autoloader,
you must tell the engine where is the root directory of the view files (resolved filepath).
Please note that the only admitted directory separator is /
(slash).
<?php
use rawsrc\PhpEcho\PhpEcho;
// eg: from the webroot directory
PhpEcho::setTemplateDirRoot(__DIR__.DIRECTORY_SEPARATOR.'View');
By default, the engine tries to find first the value of any asked key in each local
array of values attached to every PhpEcho
instance; then if not found, it will seek in the
parent blocks until it reaches the root of the tree. The seek is only made at the
first level of the array of vars attached to any PhpEcho
block.
More: User values
By default, the engine is in production mode and does not intercept the infinite loops that may be caused by the mechanism of blocks inclusion. If you want the engine to detect that, just set the option:
<?php
use rawsrc\PhpEcho\PhpEcho;
PhpEcho::setDetectInfiniteLoop(true);
Remember, the detection of infinite loops is time-consuming and grab a part of server resources. This option should be deactivated in production and only used in development mode.
To help you to pilot the rendering, you can store in any PhpEcho
as many parameters
as needed. There are two levels of parameters: local and global.
Please note that the parameters are never escaped.
If a parameter is unknown then you'll have an Exception
.
// for a specific block (local parameter)
$this->setParam('document.isPopup', true);
$is_popup = $this->getParam('document.isPopup'); // true
$has = $this->hasParam('document.isPopup'); // true
$this->unsetParam('document.isPopup');
// for all blocks (global parameter)
PhpEcho::setGlobalParam('document.isPopup', true);
$is_popup = PhpEcho::getGlobalParam('document.isPopup'); // true
$has = PhpEcho::hasGlobalParam('document.isPopup');
PhpEcho::unsetGlobalParam('document.isPopup');;
If you want the parameter's local value first then the global one if not defined
$is_popup = $this->getAnyParam(name: 'document.isPopup', seek_order: 'local');
If you want the parameter's global value first then the local one if not defined
$is_popup = $this->getAnyParam(name: 'document.isPopup', seek_order: 'global');
You can check if a param is defined either in local or global context:
$this->hasAnyParam('document.isPopup'); // order: local then global context
You can set a local and global parameter at once
$this->setAnyParam('document.isPopup', true); // the value is available in both contexts (local and global)
It's also possible to unset a parameter from the local and global context at once:
$this->unsetAnyParam('document.isPopup');
- All values read from a PhpEcho instance are escaped and safe in HTML context
- Inside a view file or inside a helper, the instance of the class
PhpEcho
is always available through$this
- For complex creation, there's a
ViewBuilder
class that comes withPhpEcho
- Parameters stored in any
PhpEcho
instance are NEVER escaped PhpEcho
comes with many pre-built code generators called helpers. They are helpful tools to make your job easier
As a developer, you know that the complexity and size of web apps are growing.
To be able to manage them, you must divide the view into small blocks of code that will be injected
upon rendering. Blocks injected into containers that can be injected into other containers as well and so on.
You must keep in mind the structure of an HTML page, it's a huge tree, PhpEcho
does exactly the same.
It's highly recommended to group the view files (layouts, pages, blocks) into a separated directory.
Usually, the architecture is generic and quite simple:
- a page is based on one layout
- a page contains as many blocks as necessary
- a block can be built on others blocks and so on
Remember: the unit of PhpEcho
is the block.
Others components are usually built with blocks, even layouts and pages are seen as PhpEcho
instances.
Easy, isn't it?
Here is the classical view part of a webapp:
www
|--- Controller
|--- Model
|--- View
| |--- block
| | |--- contact.php
| | |--- err404.php
| | |--- footer.php
| | |--- header.php
| | |--- home.php
| | |--- navbar.php
| | |--- login.php
| | |--- ...
| |--- layout
| | |--- err.php
| | |--- main.php
| | |--- ...
| |--- page
| | |--- about.php
| | |--- cart.php
| | |--- err.php
| | |--- homepage.php
| | |--- login.php
| | |--- ...
|--- bootstrap.php
|--- index.php
<?php
use rawsrc\PhpEcho\PhpEcho;
$block = new PhpEcho();
$block['foo'] = 'abc " < >'; // store a key-value pair inside the instance
// get the escaped value stored in the block, simply ask for it :
$x = $block['foo']; // $x = 'abc " < >'
// escape on demand using a HELPER
$y = $block('hsc', 'any value to escape'); // or
$y = $block->hsc('any value to escape'); // using IDE highlight
// extract the raw value on demand using the helper 'raw()'
$z = $block->raw('foo'); // $z = 'abc " < >'
// the type of value is preserved, are escaped all strings and objects having __toString()
$block['bar'] = new stdClass();
$bar = $block['bar'];
Rendering a homepage using several PhpEcho
blocks separated in many files.
To understand how the files are found, the filepath for each inclusion is prepended with
the view root directory defined using PhpEcho::setTemplateDirRoot()
.
<?php declare(strict_types=1);
use rawsrc\PhpEcho\PhpEcho;
$homepage = new PhpEcho('layout/main.php', [
'header' => new PhpEcho('block/header.php', [
'user' => 'rawsrc',
'navbar' => new PhpEcho('block/navbar.php'),
]),
'body' => new PhpEcho('block/home.php'),
'footer' => new PhpEcho('block/footer.php'),
]);
echo $homepage;
As you can see, you compose the whole page with blocks. Yous should try to keep the blocks
as much as possible independent. In a view context, absolutely every component is an instance of PhpEcho
.
Everything is autowired in the background and automatically escaped by the engine when necessary.
As PhpEcho
is highly flexible, you can even compose any element with others.
We're going to create a simple login form based on the same architecture described just above.
First, we create a layout file in View/layout
called main.php
with some required values:
- a description (string)
- a title (string)
- a
PhpEcho
block in charge of rendering the body part of the page
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ // MAIN LAYOUT ?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="description" content="<?= $this['description'] ?>">
<title><?= $this['title'] ?></title>
</head>
<body>
<?= $this['body'] ?>
</body>
</html>
As every PhpEcho instances are returned as it and transformed into a string when necessary, you can call them directly in your HTML code (as above).
Then, we create a block view in View/block
called login.php
containing the html form:
Please note that $this['url_submit']
and $this['login']
are automatically escaped
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ // LOGIN FORM BLOCK ?>
<p>Please login : </p>
<form method=post action="<?= $this['url_submit'] ?>">
<label>User</label>
<input type="text" name="login" value="<?= $this['login'] ?>"><br>
<label>Password</label>
<input type="password" name="pwd" value=""><br>
<input type="submit" name="submit" value="CONNECT">
</form>
Finally, we create a page page/login.php
based on layout/main.php
and
we inject the body part using a PhpEcho
block block/login.php
.
<?php declare(strict_types=1); // LOGIN PAGE
use rawsrc\PhpEcho\PhpEcho;
echo new PhpEcho('layout/main.php', [
'title' => 'My first use case of PhpEcho',
'description' => 'PhpEcho, PHP template engine, easy to learn and use',
'body' => new PhpEcho('block/login.php', [
'login' => 'rawsrc',
'url_submit' => 'any/path/for/connection',
]),
]);
This is also equivalent:
<?php declare(strict_types=1);
use rawsrc\PhpEcho\PhpEcho;
$page = new PhpEcho('layout/main.php');
$page['title'] = 'My first use case of PhpEcho';
$page['description'] = 'PhpEcho, PHP template engine, easy to learn and use';
$body = new PhpEcho('block/login.php');
$body['login'] = 'rawsrc';
$body['url_submit'] = 'any/path/for/connection';
$page['body'] = $body;
echo $page;
As you can see, PhpEcho
is highly flexible. You can use plenty of ways rendering your HTML/CSS/JS code.
The syntax is always clear, very readable and easy to understand.
To compose a view using many child blocks, there are three ways to declare them:
$this->renderBlock()
: the child block is anonymous in parent's block and unreachable once rendered$this->addBlock()
: the child block has a name and can be reached from the parent context using its name$this->renderByDefault()
: the child block has a name, and if the parent does not provide a specific block with the same name, then the engine will render the default block as specified in the parameter
I repeat, please note, that the whole view must be seen as a huge tree and the blocks are linked all together. You must never declare a totally independent block into another. This is not allowed:
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ ?>
<p>Please login : </p>
<form method=post action="<?= $this['url_submit'] ?>">
<label>User</label>
<input type="text" name="login" value="<?= new rawsrc\PhpEcho\PhpEcho('block/login_input_text.php') ?>"><br>
<label>Password</label>
<input type="password" name="pwd" value=""><br>
<input type="submit" name="submit" value="CONNECT">
</form>
it must be replaced with one of the methods described just above:
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ ?>
<p>Please login : </p>
<form method=post action="<?= $this['url_submit'] ?>">
<label>User</label>
<input type="text" name="login" value="<?= $this->renderBlock('block/login_input_text.php') ?>"><br>
<label>Password</label>
<input type="password" name="pwd" value=""><br>
<input type="submit" name="submit" value="CONNECT">
</form>
This way, you do not cut the tree ;-)
When you code the view part of a website, you will create plenty of small blocks that will be inserted at their right place on rendering.
As everybody knows, the best design is to try to keep the blocks as most independent as possible. Sometimes you will need to add some dependencies
directly in the header of the page. In any instance of PhpEcho
, you have a method named addhead()
which is designed for this purpose.
Now, imagine you're in the depths of the DOM, you need to tell the header to declare a link to your library. In the current block, you have to code:
<?php $this->addHead('<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous">') ?>
or using a helper script
that will secure all your values
<?php $this->addHead('script', [
'src' => "https://code.jquery.com/jquery-3.4.1.min.js",
'integrity' => "sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=",
'crossorigin' => "anonymous"]) ?>
Now in the block that renders the <head></head>
, you just have to code:
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ ?>
<head>
<?= $this->getHead(); ?>
</head>
The engine will compile the <head></head>
data from all the child blocks to render the global header.
The engine is able to retrieve on rendering your values in different ways.
When you ask to render a specific value identified by a key, the engine will try
to seek for it through all the values stored locally, then if not found, it will seek
in the parent blocks climbing the tree to the root (the very first PhpEcho
instance).
This logic is parameterizable, using the method setSeekValueMode(string $mode)
.
By default, the mode is set to parents
.
The different modes are:
current
: the engine will only seek for a key in the current blockparents
: the engine will seek for a key in the current block and then will climb the blocks one after one to the root while not foundroot
: the engine will seek for a key in the current block and if not found in the root only
Please note that the search for the key is only made at the first level of the array of vars.
// e.g.: you have an array of vars in a block
[
'k.abc' => 'v.abc',
'k.def' => [
'k.ghi' => 'v.ghi', // second level
'k.jkl' => 'v.jkl', // second level
],
];
// the visible keys for the block are 'k.abc' and 'k.def' (first level),
// the keys in the sub-array are not automatically readable unless you use a foreach()
// if you ask directly for $block['k.ghi'] the engine will not seek for the value in the sub-array
Here's how it works:
<?php declare(strict_types=1);
use rawsrc\PhpEcho\PhpEcho;
// $page is the root of the tree
$page = new PhpEcho('layout/main.php');
$page['title'] = 'My first PhpEcho project';
$page['description'] = 'PhpEcho, a PHP template engine, easy to learn and use';
$page['login'] = 'rawsrc';
$page['url_submit'] = 'any/path/for/connection';
$body = new PhpEcho('block/login.php'); // login block expects two values (login and url_submit)
$page['body'] = $body; // $body['login'] and $body['url_submit'] are well-defined
// both are sought from the root block: $page
echo $page;
By default, if the key is not found, the engine will throw an Exception
.
You can change that behavior, your can tell the engine to return null
using the method setNullIfNotExists(bool $p)
.
The auto-escaping feature works for keys and values. So please be careful:
<?php
// suppose you have data like that:
$data = ['"name"' => 'rawsrc'];
// now we inject the data into a PhpEcho block
$block = new PhpEcho('dummy_block.php', ['my_data' => $data]);
// inside the block (in HTML context), we have to test the value of the key
// something like that:
?>
<?php foreach ($this['my_data'] as $key => $value) {
// wrong code
if ($key === '"name"') { // this will never be true as the key has been automatically escaped
echo $value; // $value is automatically escaped
}
// correct code
if ($key === '"name"') {
echo $value; // $value is automatically escaped
}
}
Or you can do it manually using the helper raw()
and do not forget to escape the value:
foreach ($this->raw('my_data') as $key => $value) {
if ($key === '"name"') {
echo $this->hsc($value); // $value is manually escaped
}
}
Or you can also create a helper for this purpose that will not escape the keys but only values.
We will see the advanced use of PhpEcho
below.
You can define many strategies for views, especially regarding the level of details (the granularity) of complex layouts and pages. Suppose you render the body part of a page like that:
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ ?>
<body>
<?= $this['body'] ?>
</body>
or like this:
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ ?>
<body>
<?= $this['preloader'] ?>
<?= $this['top_header'] ?>
<?= $this['navbar'] ?>
<?= $this['navbar_mobile'] ?>
<?= $this['body'] ?>
<?= $this['footer'] ?>
<?= $this['copyright'] ?>
</body>
The first code is abstract, and the second is really explicit about what is expected.
When you want to preserve some flexibility using the abstract code, it is possible
to use an array of PhpEcho
instances for a key.
use rawsrc\PhpEcho\PhpEcho;
$page['body'] = [
new PhpEcho('block/preloader.php'),
new PhpEcho('block/top_header.php'),
new PhpEcho('block/navbar.php'),
new PhpEcho('block/navbar_mobile.php'),
new PhpEcho('block/body.php'),
new PhpEcho('block/footer.php'),
new PhpEcho('block/copyright.php'),
];
The blocks are rendered in the order they appear.
Since 6.1.0, the engine is now able to render a recursive array of PhpEcho
blocks.
All are rendered in the order they appear.
You can define a default block view to render for not defined key:
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ ?>
<body>
<?= $this->renderByDefault('preloader', 'block/preloader.php') ?>
<?= $this->renderByDefault('top_header', 'block/top_header.php') ?>
<?= $this->renderByDefault('navbar', 'block/navbar.php') ?>
<?= $this->renderByDefault('navbar_mobile', 'block/navbar_mobile.php') ?>
<?= $this['body'] ?>
<?= $this->renderByDefault('footer', 'block/footer.php') ?>
<?= $this->renderByDefault('copyright', 'block/copyright.php') ?>
</body>
All keys except body
are optional.
It's possible to use directly plain html code instead of file inclusion using HEREDOC. Because of PHP early binding value upon calling, you must be sure that the values are defined before using them in the code.
Remember the layout block, we are going to omit the file block/login.php
and
directly inject the source code into the body part using the HEREDOC notation:
<?php
use rawsrc\PhpEcho\PhpEcho;
$page = new PhpEcho('layout/main.php', [
'title' => 'My first use case of PhpEcho',
'description' => 'PhpEcho, PHP template engine, easy to learn and use',
]);
// CAREFUL: you must define your values before injecting them into a HEREDOC block
$body = new PhpEcho(vars: [
'login' => 'rawsrc',
'url_submit' => 'any/path/for/connection',
]);
// we set directly the plain html code
$body->setCode(<<<html
<p>Please login : </p>
<form method=post action="{$body['url_submit']}>">
<label>User</label>
<input type="text" name="login" value="{$body['login']}"><br>
<label>Password</label>
<input type="password" name="pwd" value=""><br>
<input type="submit" name="submit" value="CONNECT">
</form>
html
);
$page['body'] = $body;
echo $page;
// Note how it's coded, in this use case: `$body` replace `$this`
Every instance of PhpEcho
has an auto-generated id that can be linked to any html tag.
This link will define a closed context that will allow us to work with the current block
without interfering with others.
We'd like to test some new CSS on the block without interfering with other parts of the page.
<?php /** @var rawsrc\PhpEcho\PhpEcho $this */ ?>
<?php $id = $this->getId() ?>
<style>
#<?= $id ?> label {
color: blue;
float: left;
font-weight: bold;
width: 30%;
}
#<?= $id ?> input {
float: right;
}
</style>
<div id="<?= $id ?>">
<p>Please login</p>
<form method="post" action="<?= $this['url_submit'] ?>>">
<label>Login</label>
<input type="text" name="login" value="<?= $this['login'] ?>"><br>
<label>Password</label>
<input type="password" name="pwd" value=""><br>
<input type="submit" name="submit" value="CONNECT">
</form>
</div>
We have now a closed context defined by <div id="<?= $id ?>">
.
In an object oriented design, it's often easier to manipulate the whole view as an object too.
Let's have a look at the example about the login page.
You can now consider this view as a class using ViewBuilder
:
<?php
namespace YourProject\View\Page;
use rawsrc\PhpEcho\PhpEcho;
use rawsrc\PhpEcho\ViewBuilder;
class Login extends ViewBuilder
{
public function build(): PhpEcho
{
$layout = new PhpEcho('layout/main.php');
$layout['description'] = 'dummy.description';
$layout['title'] = 'dummy.title';
$layout['body'] = new PhpEcho('block/login.php', [
'login' => 'rawsrc',
'url_submit' => 'any/path/for/connection',
/**
* Note that the ViewBuilder implements the ArrayAccess interface,
* so you have plenty of ways to pass your values to the view.
*/
]);
return $layout;
}
}
In a controller that must render the login page, you can now code something like that:
<?php declare(strict_types=1);
namespace YourProject\Controller\Login;
use YourProject\View\Page\Login;
class Login
extends YourAbstractController
{
public function invoke(array $url_data = []): void
{
$page = new YourProject\View\Page\Login;
// we pass some values to the page builder
$page['name'] = 'rawsrc';
$page['postal.code'] = 'foo.bar';
// an example of ending the process sought from a framework
$this->task->setResponse(Response::html($page));
}
}
You have the possibility to use your own code generators as simply as a Closure
.
There's a small standard library of helpers that comes with PhpEcho
: stdPhpEchoHelpers.php
Each helper is a Closure
that can produce whatever you want.
Each helper can be linked to an instance of PhpEcho or remain a standalone helper.
If linked to an instance, inside the closure you can use $this
to get access to the caller's execution context.
If standalone, this is just a simple function with parameters.
If a helper needs to get access to the PhpEcho
instance to whom it's linked, you must use
PhpEcho::addBindableHelper()
to declare it otherwise just use PhpEcho::addHelper()
.
If the code generated by the helper is already escaped (to avoid double quote) set the third parameter $result_escaped
to true
This helper compares two scalar values and returns the string " checked "
if they are equal.
$checked = function($p, $ref) use ($is_scalar): string {
return $is_scalar($p) && $is_scalar($ref) && ((string)$p === (string)$ref) ? ' checked ' : '';
};
PhpEcho::addHelper(name: 'checked', helper: $checked, result_escaped: true);
This helper is a standalone closure, there's no need to have access to an instance of PhpEcho.
As everything is escaped by default in PhpEcho, we can consider the word " checked "
is safe
and does not need to be escaped again, this is why, with the helper definition, the third parameter is set to true
.
To call this helper inside your views (two ways):
$this('checked', 'your value', 'ref value'); // based on __invoke
$this->checked('your value', 'ref value'); // based on __call
This helper returns the raw value from the stored key-value pair into each PhpEcho
instance:
$raw = function(string $key) {
/** @var PhpEcho $this */
return $this->getOffsetRawValue($key);
};
PhpEcho::addBindableHelper(name: 'raw', closure: $raw, result_escpaed: true);
As this helper extract data from the stored key-value pairs defined in each instance of PhpEcho,
it needs access to the caller's execution context that's why the helper definition
is created using PhpEcho::addBindableHelper()
.
And as we want to get the value unescaped, we must tell the engine that the returned value by the
closure is already escaped. We know that is not, but this is the goal of that helper.
To call this helper inside your views (two ways):
$this('raw', 'key');
$this->raw('key');
To define a helper, there are two ways:
PhpEcho::addHelper(string $name, Closure $helper, bool $result_escaped = false)
PhpEcho::addBindableHelper(string $name, Closure $helper, bool $result_escaped = false)
When you code a new helper that will be bound to a class instance and needs to use another bound helper,
to be sure the two helpers refer to the same context, you must use this syntax
$existing_helper = $this->bound_helpers['$existing_helper_name'];
inside your code.
Please have a look at the $root_var
helper (how the link to another bound helper $root
is created).
As mentioned above, the standard library stdPhpEchoHelpers.php
contains helpers for data processing and
also for HTML rendering. As helpers are small snippets of code, you can read their source code to understand
easily what they will return.
Examples:
- You need to create a
<input>
tag using the helpervoidTag()
:
$this->voidTag('input', ['type' => 'text', 'name' => 'name', 'required', 'value' => ' < > " <script></script>']);
You do not have to worry about any dangerous character in this tag, all are escaped. Here's the rendered HTML code:
<input type="text" name="name" required value=" < > " <script></script>">
It is also possible to do like that (using the helper attributes()
:
<input <?= $this->attributes(['type' => 'text', 'name' => 'name', 'required', 'value' => ' < > " <script></script>']) ?>>
As you see, there are tons of methods to get the expected result.
Remember the problem with the auto-escape key value? Here's the helper that returns the raw key and the escaped value at once.
<?php
use rawsrc\PhpEcho\PhpEcho;
/**
* Return an array of raw keys and escaped values for HTML
* Careful: keys are not safe in HTML context
*
* @param array $part
* @return array
*/
$hsc_array_values = function(array $part) use (&$hsc_array_values): array {
$hsc = PhpEcho::getHelperBase('hsc');
$to_escape = PhpEcho::getHelperBase('toEscape')
$data = [];
foreach ($part as $k => $v) {
if ($to_escape($v)) {
if (is_array($v)) {
$data[$k] = $hsc_array_values($v);
} else {
$data[$k] = $hsc($v);
}
} else {
$data[$k] = $v;
}
}
return $data;
};
PhpEcho::addBindableHelper('hscArrayValues', $hsc_array_values, true);
rawsrc