diff --git a/PhpEcho.php b/PhpEcho.php
index fe64ece..b1da68d 100644
--- a/PhpEcho.php
+++ b/PhpEcho.php
@@ -7,13 +7,17 @@
use Closure;
use InvalidArgumentException;
+use function array_intersect;
use function array_key_exists;
use function array_push;
+use function array_reduce;
use function array_shift;
+use function array_walk_recursive;
use function bin2hex;
use function chr;
use function count;
use function implode;
+use function in_array;
use function is_array;
use function is_file;
use function is_string;
@@ -26,6 +30,7 @@
use function str_shuffle;
use function substr;
+use const COUNT_RECURSIVE;
use const DIRECTORY_SEPARATOR;
/**
@@ -55,7 +60,7 @@
* SOFTWARE.
*
* PhpEcho HELPERS
- * @method mixed raw(string $key) Return the raw value from a PhpEcho block
+ * @method mixed raw(string $key) // Return the raw value from a PhpEcho block
* @method bool isScalar(mixed $p)
* @method mixed keyUp(array|string $keys, bool $strict_match) // Climb the tree of PhpEcho instances while keys match
* @method mixed rootVar(array|string $keys) // Extract the value from the top level PhpEcho block (the root)
@@ -80,6 +85,8 @@ class PhpEcho
private string $id = '';
private array $vars = [];
private array $params = [];
+ /** @var array */
+ private array $children = [];
private array $head = [];
private string $head_token = '';
/**
@@ -91,10 +98,6 @@ class PhpEcho
* @var array [helper's id => bound closure]
*/
private array $bound_helpers = [];
- /**
- * Indicates if the current instance contains in its vars other PhpEcho instance(s)
- */
- private bool $has_children = false;
private PhpEcho $parent;
private static string $template_dir_root = '';
/**
@@ -115,7 +118,8 @@ class PhpEcho
private static array $used_tokens = [];
private static array $global_params = [];
private static bool $std_helpers_injected = false;
- private static bool $return_null_if_not_exist = false;
+ private static bool $opt_return_null_if_not_exist = false;
+ private static string $opt_seek_value_mode = 'parents'; // parents | root
//region MAGIC METHODS
/**
@@ -143,7 +147,7 @@ public function __construct(string $file = '', array $vars = [], string $id = ''
}
/**
- * This function call a helper defined elsewhere or dynamically
+ * This function calls a helper defined elsewhere or dynamically
* Auto-escape if necessary
*
* @throws InvalidArgumentException
@@ -186,9 +190,12 @@ public function __toString(): string
}
}
+ /**
+ * @throws BadMethodCallException
+ */
public function __clone(): void
{
- unset($this->parent);
+ throw new BadMethodCallException('cloning.a.phpecho.instance.is.not.permitted');
}
//endregion
@@ -363,14 +370,30 @@ public function unsetAnyParam(string $name): void
}
//endregion PARAMETERS
+ //region OPTIONS
/**
* If a key is not defined then return null instead of throwing an Exception
*/
public static function setNullIfNotExists(bool $p): void
{
- self::$return_null_if_not_exist = $p;
+ self::$opt_return_null_if_not_exist = $p;
}
+ /**
+ * 3 modes
+ * @param string $mode among current|parents|root
+ * @throws InvalidArgumentException
+ */
+ public static function setSeekValueMode(string $mode): void
+ {
+ if (in_array($mode, ['current', 'parents', 'root'])) {
+ self::$opt_seek_value_mode = $mode;
+ } else {
+ throw new InvalidArgumentException("unknown.seek.mode.value.{$mode}");
+ }
+ }
+ //endregion OPTIONS
+
/**
* Generate a unique execution id based on random_bytes()
* Always start with a letter
@@ -385,8 +408,10 @@ public function generateId(): void
*/
public function setVars(array $vars): void
{
- $this->has_children = false;
if ($vars === []) {
+ foreach ($this->children as $v) {
+ $this->unsetChild($v);
+ }
$this->vars = [];
} else {
foreach ($vars as $k => $v) {
@@ -396,11 +421,18 @@ public function setVars(array $vars): void
}
/**
- * Values available for the whole tree of blocks
+ * Local values only
+ */
+ public function getVars(): array
+ {
+ return $this->vars;
+ }
+
+ /**
+ * Values are stored ine the root of the tree
*/
public function injectVars(array $vars): void
{
- /** @var PhpEcho $root */
$root = $this('root');
foreach ($vars as $k => $v) {
$root->offsetSet($k, $v);
@@ -413,6 +445,34 @@ public function offsetExists(mixed $offset): bool
return array_key_exists($offset, $this->vars);
}
+ public function offsetSet(mixed $offset, mixed $value): void
+ {
+ $for_array = function(array $p) use (&$for_array): array {
+ $data = [];
+ foreach ($p as $k => $v) {
+ if ($v instanceof PhpEcho) {
+ $this->addChild($v);
+ $data[$k] = $v;
+ } elseif (is_array($v)) {
+ $data[$k] = $for_array($v);
+ } else {
+ $data[$k] = $v;
+ }
+ }
+
+ return $data;
+ };
+
+ if ($value instanceof self) {
+ $this->addChild($value);
+ $this->vars[$offset] = $value;
+ } elseif (is_array($value)) {
+ $this->vars[$offset] = $for_array($value);
+ } else {
+ $this->vars[$offset] = $value;
+ }
+ }
+
/**
* The returned value is escaped
*
@@ -449,19 +509,57 @@ public function offsetGet(mixed $offset): mixed
return $data;
};
+ $recursive_array_of_blocks = function(array $p) use (&$recursive_array_of_blocks, $get_escaped): string {
+ $data = [];
+ foreach ($p as $k => $z) {
+ if (is_array($z) && ($z !== [])) {
+ $data[$get_escaped($k)] = $recursive_array_of_blocks($z);
+ } else {
+ $data[$get_escaped($k)] = (string)$z;
+ }
+ }
+
+ $str = '';
+ array_reduce($data, function($str, $v) { $str .= $v; return $str; }, '');
+
+ return $str;
+ };
+
if ($v === null) {
return null;
} elseif (is_array($v)) {
if ($this->isArrayOfPhpEchoBlocks($v)) {
- return implode('', $for_array($v));
+ // simple array of PhpEcho blocks (one level)
+ if (count($v) === count($v, COUNT_RECURSIVE)) {
+ return implode('', $v);
+ } else {
+ // recursive array of PhpEcho blocks
+ return $recursive_array_of_blocks($v);
+ }
} else {
return $for_array($v);
}
- } else{
+ } else {
return $get_escaped($v);
}
}
+ /**
+ * Only local value
+ */
+ public function offsetUnset(mixed $offset): void
+ {
+ if (array_key_exists($offset, $this->vars)) {
+ if ($this->vars[$offset] instanceof self) {
+ $this->unsetChild($offset);
+ }
+ unset($this->vars[$offset]);
+ } else {
+ throw new InvalidArgumentException("unknown.offset.{$offset}");
+ }
+ }
+ //endregion ARRAY ACCESS
+
/**
* @throws InvalidArgumentException only if $return_null_if_not_exist is set to false
*/
@@ -469,64 +567,110 @@ private function getOffsetRawValue(mixed $offset): mixed
{
if (array_key_exists($offset, $this->vars)) {
return $this->vars[$offset];
- }
-
- $block = $this;
- while (true) {
- if (isset($block->parent)) {
- if (array_key_exists($offset, $block->parent->vars)) {
- return $block->parent->vars[$offset];
- } else {
- $block = $block->parent;
+ } elseif (self::$opt_seek_value_mode === 'parents') {
+ $block = $this;
+ while (isset($block->parent)) {
+ if (array_key_exists($offset, $block->vars)) {
+ return $block->vars[$offset];
+ }
+ $block = $block->parent;
+ }
+ if (array_key_exists($offset, $block->vars)) {
+ return $block->vars[$offset];
+ }
+ } elseif (self::$opt_seek_value_mode === 'root') {
+ $root = $this('root');
+ if ($root !== $this) {
+ if (array_key_exists($offset, $root->vars)) {
+ return $root->vars[$offset];
}
- } else {
- return self::$return_null_if_not_exist ? null : throw new InvalidArgumentException("unknown.offset.{$offset}");
}
}
+
+ return self::$opt_return_null_if_not_exist ? null : throw new InvalidArgumentException("unknown.offset.{$offset}");
}
- public function offsetSet(mixed $offset, mixed $value): void
+ /**
+ * @return array
+ */
+ private function getParentsId(): array
{
- $block = function(PhpEcho $p): self {
- $this->has_children = true;
- $p->parent = $this;
+ $data = [];
+ $block = $this;
+ while (isset($block->parent) && ($block->parent !== $this)) {
+ $data[] = $block->parent->id;
+ $block = $block->parent;
+ }
- return $p;
- };
+ return $data;
+ }
- $for_array = function(array $p) use (&$for_array, $block): array {
- $data = [];
- foreach ($p as $k => $v) {
- if ($v instanceof PhpEcho) {
- $data[$k] = $block($v);
- } elseif (is_array($v)) {
- $data[$k] = $for_array($v);
- } else {
- $data[$k] = $v;
- }
+ /**
+ * @return array
+ */
+ private function getParentsFilepath(): array
+ {
+ $data = [];
+ $block = $this;
+ while (isset($block->parent) && ($block->parent !== $this)) {
+ if ($block->parent->file !== '') {
+ $data[] = $block->parent->file;
}
+ $block = $block->parent;
+ }
- return $data;
- };
+ return $data;
+ }
- if ($value instanceof self) {
- $this->vars[$offset] = $block($value);
- } elseif (is_array($value)) {
- $this->vars[$offset] = $for_array($value);
- } else {
- $this->vars[$offset] = $value;
+ /**
+ * @return array
+ */
+ private function getChildrenId(): array
+ {
+ $data = [];
+ foreach ($this->children as $v) {
+ $data[] = $v->id;
+ array_push($data, ...$v->getChildrenId());
}
+
+ return $data;
}
- public function offsetUnset(mixed $offset): void
+ /**
+ * @throws InvalidArgumentException
+ */
+ private function addChild(PhpEcho $p): void
{
- if (array_key_exists($offset, $this->vars)) {
- unset($this->vars[$offset]);
- } else {
- throw new InvalidArgumentException("unknown.offset.{$offset}");
+ $this->children[] = $p;
+ $p->parent = $this;
+ $this->detectInfiniteLoop();
+ }
+
+ private function unsetChild(mixed $offset): void
+ {
+ /** @var PhpEcho $block */
+ $block = $this->vars[$offset];
+ unset($this->children[$offset]);
+ $block->parent = $block; // the block is now orphan
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ private function detectInfiniteLoop(): void
+ {
+ if ($this->file !== '') {
+ if (in_array($this->file, $this->getParentsFilepath(), true)) {
+ throw new InvalidArgumentException('infinite.loop.detected');
+ }
+ }
+
+ // the current block and its childs must not refer one of their parents id
+ $ids = [$this->id, ...$this->getChildrenId()];
+ if (array_intersect($ids, $this->getParentsId()) !== []) {
+ throw new InvalidArgumentException('infinite.loop.detected');
}
}
- //endregion ARRAY ACCESS
//region HELPER ZONE
public static function getToken(int $length = 16): string
@@ -671,13 +815,16 @@ private function bindHelpersTo(object $p): void
private function isArrayOfPhpEchoBlocks(mixed $p): bool
{
if (is_array($p) && ($p !== [])) {
- foreach ($p as $v) {
- if ( ! ($v instanceof self)) {
- return false;
+ $status = true;
+ array_walk_recursive($p, function($v) use (&$status) {
+ if ($status) {
+ if ( ! $v instanceof PhpEcho) {
+ $status = false;
+ }
}
- }
+ });
- return true;
+ return $status;
} else {
return false;
}
@@ -732,7 +879,7 @@ public function setCode(string $code): void
*/
public function hasChildren(): bool
{
- return $this->has_children;
+ return $this->children !== [];
}
/**
diff --git a/README.md b/README.md
index 1b05255..4613778 100644
--- a/README.md
+++ b/README.md
@@ -1,760 +1,9 @@
# **PhpEcho**
-`2023-08-13` `PHP 8.0+` `6.0.1`
+`2023-09-24` `PHP 8.0+` `6.1.0`
## **A native PHP template engine : One class to rule them all**
## **IS ONLY FOR PHP 8 AND ABOVE**
-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 very simple 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 the locally stored key-values pairs
-* escaping any value on demand
-* returning raw values (when you know what you're doing)
-* the possibility to write directly plain html code instead of using the file inclusion mechanism
-* escaping recursively keys and values in any array
-* managing and rendering instance of class that implements the magic function `__toString()`
-* let you access to the global HTML `` from any child block
-* let you create your own helpers
-* let your IDE list all your helpers natively just using PHPDoc syntax (see the PHPDoc of the class)
-
-**INSTALLATION**
-```bash
-composer require rawsrc/phpecho
-```
-
-**What you must know to use it**
-1. All values read from a PhpEcho instance are escaped and safe in HTML context.
-2. Parameters stored in any PhpEcho instance are **NEVER** escaped
-3. Inside an external view file, the instance of the class PhpEcho is always available through `$this`.
-4. The only admitted directory separator is / (slash)
-
-**SHORT EXAMPLE**
-```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 a helper
-$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'];
-```
-
-## **Views in web applications and PhpEcho overview**
-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 others containers as well and so on.
-
-It's highly recommended grouping 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.
-In the sunny world of PhpEcho, a layout or a page are also seen as blocks.
-
-In the bootstrap of your webapp, you just have to tell `PhpEcho` where is the root directory:
-Example:
-```txt
-www
- |--- Controller
- |--- Model
- |--- View
- | |--- Template01
- | | |--- 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
-```
-Before v.5.1.1, in your `bootstrap.php` file, you must inject the standard helpers and set up the main template directory.
-Since v.5.1.1, standard helpers are now injected once automatically, you still have to set up the main template directory.
-```php
-
-use rawsrc\PhpEcho\PhpEcho;
-
-// PhpEcho::injectStandardHelpers(); // before v.5.1.1
-PhpEcho::setTemplateDirRoot(__DIR__.DIRECTORY_SEPARATOR.'View'.DIRECTORY_SEPARATOR.'Template01');
-```
-Then you will code for example the homepage `page/homepage.php` based on `layout/main.php` like that:
-```php
- 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 your 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. It's up to you to decide.
-
-## **Defining and using your own code snippets as helpers**
-You have the possibility to use your own code generator as simply as a `Closure`.
-There's a small standard library of helpers that comes with PhpEcho : `stdPhpEchoHelpers.php`
-
-**About helpers:**
-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()` 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`
-For example, have a look at the helper that returns the HTML attribute `checked`:
-This helper compares two values and if they are equal return the string `" checked "`
-```php
-$checked = function($p, $ref) use ($is_scalar): string {
- return $is_scalar($p) && $is_scalar($ref) && ((string)$p === (string)$ref) ? ' checked ' : '';
-};
-PhpEcho::addHelper('checked', $checked, 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 code (2 ways) :
-* `$this('checked', 'your value', 'ref value'); // based on __invoke`
-* `$this->checked('your value', 'ref value'); // based on __call`
-
-Now, have a look at the helper that returns the raw value from the stored key-value pair `raw`:
-```php
-$raw = function(string $key) {
- /** @var PhpEcho $this */
- return $this->getOffsetRawValue($key);
-};
-PhpEcho::addBindableHelper('raw', $raw, 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 goal of that helper.
-* `$this('raw', 'key');`
-* `$this->raw('key');`
-
-To define a helper, there are 2 ways:
-* `PhpEcho::addHelper(string $name, Closure $helper, bool $result_escaped = false)`
-* `PhpEcho::addBindableHelper(string $name, Closure $helper, bool $result_escaped = false)`
-
-When you write 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).
-
-## **Simple example**
-
-We're going to create a simple login form based ont the same architecture described just above.
-
-1. First, we create a layout file in `View/Template01/layout` called `main.php`
-Do not forget that all values returned are safe in HTML context.
-In the layout, some values are required:
-* a description (string)
-* a title (string)
-* a PhpEcho block in charge of rendering the body part of the page
-```php
-
-
-
-
-
-
- = $this['title'] ?>
-
-
-= $this['body'] ?>
-
-
-```
-As every PhpEcho instances are returned as it and transformed into a string when it's necessary, you can call them directly in your HTML code (as above).
-Then, we create a block view in `View/Template01/block` called `login.php` containing the html form:
-Please note that `$this['url_submit']` and `$this['login']` are automatically escaped
-```php
-
-
Please login :
-
-```
-Finally, we create a page `page/login.php` based on `layout/main.php`
-and inject the body `block/login.php`. All are sought from the template directory root:
-```php
- '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
-renderBlock()`: the rendered block is anonymous in the page and unreachable once rendered
-* `$this->addBlock()`: the rendered block has a name and can be reached from the parent context using its name
-* `$this->renderByDefault()`: the rendered block has a name and if the parent does not provide a specific block for,
-then the engine will render the default block as specified in the parameters
-
-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 for example:
-```php
-
-
Please login :
-
-```
-it should be replaced with one of the methods described just above:
-```php
-
-
Please login :
-
-```
-This way, you do not cut the tree ;-)
-
-**AUTO WIRING VARS**
-
-Coming from versions prior to 6, this change may slightly break your existing code.
-Before, the engine used to copy the vars from the parent block to its child only when the child had
-no vars defined at all. Now, you must provide the values expected to be rendered by the current block,
-there's no more copy.
-
-As many developers, I usually store all the needed values in the root and then I used to create my blocks without
-defining specific values, so the child blocks got a copy of the parent values and so on.
-
-This is exactly the new approach.
-
-Now if the value is not defined/found in the current block, then the engine will automatically seek for it in the root of the tree.
-You can store all the values needed by the child blocks in the root and let the engine pick them up on rendering.
-```php
- '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:
-?>
- $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"') { // ($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:
-```php
-foreach ($this->raw('my_data') as $key => $value) {
- if ($key === '"name"') {
- echo $this->hsc($value); // $value is manually escaped
- }
-}
-```
-or you can create a helper for this purpose that will not escape the keys but only values:
-
-```php
-use rawsrc\PhpEcho\PhpEcho;
-
-$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);
-```
-
-## **Array of PhpEcho blocks**
-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
-
-
-= $this['body'] ?>
-
-```
-or like this:
-```php
-
-
-= $this['preloader'] ?>
-= $this['top_header'] ?>
-= $this['navbar'] ?>
-= $this['navbar_mobile'] ?>
-= $this['body'] ?>
-= $this['footer'] ?>
-= $this['copyright'] ?>
-
-```
-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, since v4 it is possible to use an array of `PhpEcho` blocks for a key.
-```php
-
-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. You can omit one or many, swap them. You are free to render the code as you need.
-
-## **Render default view if not defined**
-Since v4, it's possible to define a default block view to render:
-
-```php
-
-
-= $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') ?>
-
-```
-All keys except `body` are optional.
-
-## **Use HEREDOC instead of file inclusion**
-
-It's possible to use directly plain html code instead of file inclusion.
-Because of PHP early binding value upon calling you must be sure that the values are defined before using them in the code.
-
-We are going to omit the file `block login.php` and inject directly the source code into the layout:
-Remember, the layout:
-```php
-
-
-
-
-
-
- = $this['title'] ?>
-
-
-= $this['body'] ?>
-
-
-```
-Remember the login form:
-```php
-
-
Please login :
-
-```
-Let's swap the login form file:
-```php
- 'My first use case of PhpEcho',
- 'description' => 'PhpEcho, PHP template engine, easy to learn and use',
-]);
-
-// here we define the needed values inside the plain html code before injecting them
-$body = new PhpEcho(vars: [
- 'login' => 'rawsrc',
- 'url_submit' => 'any/path/for/connection',
-]);
-
-// we set directly the plain html code
-$body->setCode(<<Please login :
-
-html
- );
-$page['body'] = $body;
-
-echo $page;
-// Note how it's coded, in this use case : `$body` replace `$this`
-```
-
-## **Use id**
-
-It's possible now to define automatically a closed context in the rendered view by using a html tag's id
-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.
-
-How to use it: we will update the `block login.php` file to see how to use this feature.
-For example, we'd like to test some new CSS on the block without changing the rendering of other parts of the page.
-```php
-
-getId() ?>
-
-
-
Please login
-
-
-```
-See how it is possible to use the PhpEcho's id in the HTML context: we have now a closed context defined by `
`, that will let us to lead
-our css tests without interfering with others parts of HTML. It's also possible to use it for any javascript code related to the current instance of PhpEcho.
-
-## **Parameters**
-
-There's two level of parameters: local and global contexts
-Please note that the parameters are never escaped.
-If a parameter is unknown then you'll have an `Exception`
-```php
-// for a specific block
-$this->setParam('document.isPopup', true);
-$is_popup = $this->getParam('document.isPopup'); // true
-$has = $this->hasParam('document.isPopup'); // true
-$this->unsetParam('document.isPopup');
-```
-```php
-// for all blocks
-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
-```php
-$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
-```php
-$is_popup = $this->getAnyParam(name: 'document.isPopup', seek_order: 'global');
-```
-You can check if a param is defined either in local or global context:
-```php
-$this->hasAnyParam('document.isPopup'); // seek in the current block first then in the global context
-```
-You can set a local and global parameter at once
-```php
-$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:
-```php
-$this->unsetAnyParam('document.isPopup');
-```
-
-## **Using the component `ViewBuilder`**
-For complex view, it's often easier to manipulate the whole view as an object.
-Let's have a look at the example about the login page.
-You can now consider this view as a class using `ViewBuilder`.
-We're going to reuse the whole code in a different way:
-
-```php
-namespace YourProject\View\Page;
-
-use rawsrc\PhpEcho\PhpEcho;
-use rawsrc\PhpEcho\ViewBuilder;
-
-class Login extends ViewBuilder
-{
- public function build(): PhpEcho
- {
- // here you can build the page as you want
- $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 array access interface
- * So you have plenty of ways to pass your values to the view,
- * eg: passing values from the current ViewBuilder to the block view:
- * 'abc' => $this['name'],
- * 'def' => $this['postal.code'],
- *
- * 'abc' and 'def' are keys to be used in the block/login.php and
- * 'name' and 'postal.code' are keys from the current ViewBuilder
- * (see below)
- */
- ]);
-
- return $layout;
- }
-}
-````
-In a controller that must render the login page, you can now code something like that:
-```php
-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));
- }
-}
-```
-Much more easy with that addon.
-
-## **Let's play with helpers**
-As mentioned above, there's some new helpers that have been added to the standard helpers library `stdPhpEchoHelpers.php`.
-These helpers will help you to render any HTML code and/or interact with any PhpEcho instance.
-By default, everything in PhpEcho is escaped, so this is also true for the HTML code generated by the helpers.
-
-As helpers are small snippets of code, you can read their source code to understand easily what they will return.
-The helpers are also documented.
-
-Examples:
-* You need to create a `` tag
-```php
-$this->voidTag('input', ['type' => 'text', 'name' => 'name', 'required', 'value' => ' < > " ']);
-```
-You do not have to worry about any dangerous character in this tag, all are escaped. Here's the rendered HTML code:
-```html
-
-```
-It is also possible to do like this:
-```php
-attributes(['type' => 'text', 'name' => 'name', 'required', 'value' => ' < > " ']) ?>>
-```
-As you see, there are tons of methods to get the expected result.
-It's highly recommended creating and using your own helpers.
-
-
-About some helpers:
-
-**Access the root**
-
-The very first PhpEcho instance is a special object that is available for any child PhpEcho instance.
-
-You have direct access to it using the helper `root` or `$this->root()`. This helper return the top-level instance of a tree of PhpEcho classes.
-
-**Accessing a value stored in the root**
-
-As any other PhpEcho instance, you can store inside any value and retrieve it from any child block using the helper `rootVar` or
-the corresponding method : `rootVar()`. Now, you can define some global values and interact with them from any child block.
-These values behave like any standard value and are of course escaped when necessary.
-
-**Climbing the tree of blocks**
-
-The last is `keyUp` with the corresponding method `keyUp()`.
-From a given list of keys (string or array, string: the delimiter for each key is space), the engine will start to climb the tree
-of blocks while the key is found, and will return the value corresponding to the last key or throw an exception if not found.
-With the parameter `$strict_match`, it is possible to tell the engine to continue to climb if the current key is still not found.
-```php
-// imagine you have a tree of PhpEcho blocks corresponding to a part of the DOM
-$block1['abc'] = 'rawsrc';
-$block1['form1'] = $block2;
- $block2['def'] = 'github';
- $block2['tab'] = $block3;
- $block3['tab_footer'] = $block4;
-
-// now from the $block4 you want to read the value for the key 'abc'
-// with $strict_match === true, you must define the whole path to the block
-$x = $this->keyUp('tab abc', true);
-// this is equivalent
-$x = $this->keyUp('def abc', true);
-
-// with $strict_match === false, you know there's a parent block having the key
-// but you don't know the path to get it out
-$x = $this->keyUp('abc', false);
-```
-
-## **Accessing the top `` from any child PhpEcho block**
-
-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 keep your blocks in the most independent way from the others. Sometimes you will need to add some dependencies
-directly in the header of the page. This is also possible using PhpEcho as your main template engine.
-
-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're coding a block and need to tell the header to declare a link to your library.
-In the current block, you will do:
-```php
-addHead('']);
+```
+You do not have to worry about any dangerous character in this tag, all are escaped. Here's the rendered HTML code:
+```html
+
+```
+It is also possible to do like that (using the helper `attributes()`:
+```php
+attributes(['type' => 'text', 'name' => 'name', 'required', 'value' => ' < > " ']) ?>>
+```
+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
+ $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**
\ No newline at end of file
diff --git a/README_fr.md b/README_fr.md
new file mode 100644
index 0000000..bcd46c0
--- /dev/null
+++ b/README_fr.md
@@ -0,0 +1,798 @@
+# **PhpEcho**
+
+`2023-09-24` `PHP 8.0+` `6.1.0`
+
+## **Un moteur de rendu en PHP natif : Une classe pour les dominer tous**
+## **UNIQUEMENT POUR PHP VERSION 8 ET SUPÉRIEURE**
+
+Quand vous codez une application web, le rendu des vues peut être un véritable défi surtout si vous souhaitez
+n'utiliser que du PHP natif et éviter de faire appel à un langage externe de génération de code.
+
+C'est l'unique objectif de `PhpEcho` : fournir un moteur de rendu en PHP natif sans aucune autre dépendance.
+
+`PhpEcho` est très simple à utiliser, il colle parfaitement à la syntaxe utilisée par PHP pour le rendu du HTML/CSS/JS.
+Il est basé sur une approche objet n'utilisant qu'une seule et unique classe pour accomplir la tâche.
+Comme vous pouvez vous en douter, l'utilisation du PHP natif offre des performances inégalées.
+Besoin d'aucun cache pour avoir des performances stratosphériques.
+Pas de parsage additionnel, pas de nouvelle syntaxe à apprendre !
+Si vous avez déjà quelques bases en PHP, c'est amplement suffisant pour l'utiliser.
+
+Pour résumer, vous n'avez qu'à pointer vers un fichier de vue et passer à l'instance un tableau clé-valeur disponible au rendu.
+
+La classe gère :
+* l'inclusion de fichiers
+* l'extraction et l'échappement des valeurs stockées dans l'instance courante
+* l'échappement de n'importe quelle valeur sur demande (y compris clé-valeur des tableaux multidimensionnels)
+* le rendu brut et sans échappement d'une valeur sur demande
+* la possibilité d'écrire directement du code HTML au lieu de passer par des inclusions de fichier
+* la gestion et le rendu de toutes les instances de classe implémentant la fonction magique `__toString()`
+* l'accès à la balise globale `` de n'importe quel bloc enfant
+* détection d'inclusion infinie
+
+Vous serez également en mesure d'étendre les fonctionnalités du moteur en créant vos propres assistants
+tout en laissant votre EDI les lister rien qu'en utilisant la syntaxe PHPDoc.
+
+1. [Installation](#installation)
+2. [Configuration](#configuration)
+ 1. [Répertoire racine de toutes les vues](#répertoire-racine-de-toutes-les-vues)
+ 2. [recherche des valeurs](#recherche-des-valeurs)
+3. [Paramètres](#paramètres)
+4. [Principes and généralités](#principes-et-généralités)
+5. [Démarrage](#démarrage)
+ 1. [Exemple rapide](#exemple-rapide)
+ 2. [Codage standard](#codage-standard)
+ 3. [Contexte HTML](#contexte-html)
+ 1. [Mise en page - Layout](#mise-en-page---layout)
+ 2. [Formulaire](#formulaire)
+ 3. [Page](#page)
+6. [Blocs enfants](#blocs-enfants)
+7. [Accès à la balise HEAD](#accès-à-la-balise-head)
+8. [Valeurs utilisateurs](#valeurs-utilisateur)
+ 1. [Recherche de clés](#recherche-de-clés)
+ 2. [Clé non trouvée](#clé-non-trouvée)
+9. [Échappement automatique des valeurs](#échappement-automatique-des-valeurs)
+10. [Tableau d'instances de PhpEcho](#tableau-dinstances-de-phpecho)
+11. [Utilisation d'une vue par défaut](#utilisation-dune-vue-par-défaut)
+12. [HTML au format HEREDOC](#html-au-format-heredoc)
+13. [Utilisation de l'id de bloc auto-généré](#utilisation-de-lid-de-bloc-auto-généré)
+14. [Utilisation du composant `ViewBuilder](#utilisation-du-composant-viewbuilder)
+15. [Utilisation avancée : création de ses propres assistants](#utilisation-avancée-création-de-ses-propres-assistants)
+ 1. [Assistants](#assistants)
+ 2. [Étude : l'assistant autonome `$checked`](#étude--lassistant-autonome-checked)
+ 3. [Étude : l'assistant lié `$raw`](#étude--lassistant-lié-raw)
+ 4. [Création d'un assistant et liaison complexe](#création-dun-assistant-et-liaison-complexe)
+16. [Voyons quelques assistants](#voyons-quelques-assistants)
+
+## **INSTALLATION**
+```bash
+composer require rawsrc/phpecho
+```
+
+## **CONFIGURATION**
+### **RÉPERTOIRE RACINE DE TOUTES LES VUES**
+Pour l'utiliser, une fois que vous avez déclaré la classe en utilisant `include_once` ou
+n'importe quel autoloader, vous devez indiquer au moteur avant toute chose le répertoire
+racine de toutes les vues (chemin résolu).
+Veuillez noter que le seul séparateur de répertoire autorisé est `/` (slash).
+```php
+
+Notez bien que les paramètres ne sont jamais échappés.
+
+Si le paramètre est inexistant, le moteur déclenchera une `Exception`.
+```php
+// pour un bloc spécifique (paramètre local)
+$this->setParam('document.isPopup', true);
+$is_popup = $this->getParam('document.isPopup'); // true
+$has = $this->hasParam('document.isPopup'); // true
+$this->unsetParam('document.isPopup');
+```
+```php
+// pour tous les blocs (paramètre global)
+PhpEcho::setGlobalParam('document.isPopup', true);
+$is_popup = PhpEcho::getGlobalParam('document.isPopup'); // true
+$has = PhpEcho::hasGlobalParam('document.isPopup');
+PhpEcho::unsetGlobalParam('document.isPopup');;
+```
+
+Si vous souhaitez la valeur du paramètre local en premier et ensuite global si inexistant :
+```php
+$is_popup = $this->getAnyParam(name: 'document.isPopup', seek_order: 'local');
+```
+Si vous souhaitez la valeur du paramètre global en premier et ensuite local si inexistant :
+```php
+$is_popup = $this->getAnyParam(name: 'document.isPopup', seek_order: 'global');
+```
+Pour vérifier l'existence d'un paramètre dans les deux contextes :
+```php
+$this->hasAnyParam('document.isPopup'); // contexte local puis global
+```
+Définition d'un paramètre dans les deux contextes simultanément :
+```php
+$this->setAnyParam('document.isPopup', true); // the value is available in both contexts (local and global)
+```
+Suppression d'un paramètre dans les deux contextes simultanément :
+```php
+$this->unsetAnyParam('document.isPopup');
+```
+
+## **PRINCIPES ET GÉNÉRALITÉS**
+
+1. Toutes les valeurs extraites d'une instance de `PhpEcho` sont échappées et sûres dans un contexte HTML
+2. Dans un fichier vue ou dans un assistant de code, l'instance courante de `PhpEcho` est accessible via `$this`
+3. Pour des vues complexes, la classe `ViewBuilder` est fournie avec le moteur
+4. `PhpEcho` est fourni avec plusieurs générateurs de code appelés assistants pour vous rendre la vie meilleure.
+
+En tant que développeur, vous savez que la complexité et la taille des applications web vont croissants.
+Pour y arriver, vous devez diviser les vues en petits blocs qui composeront un rendu plus complexe.
+Avec `PhpEcho`, les blocs s'injectent et se lient les uns aux autres et composent des blocs plus vastes réutilisables.
+
+Vous devez bien comprendre la structure d'une page HTML, c'est un arbre gigantesque. `PhpEcho` suit exactement la même approche.
+
+Il est fortement recommandé de garder les fichiers de rendu dans un répertoire séparé (gabarit, pages, blocs).
+Habituellement, l'architecture est générique et assez simple :
+- une page est basée sur un gabarit,
+- une page contient autant de blocs que nécessaire,
+- un bloc peut être composé d'autres blocs et ainsi de suite.
+
+Notez : l'unité de travail de `PhpEcho` est le bloc. Les autres composants sont
+construits sur des blocs et sont eux-mêmes vus comme des blocs.
+
+Simple, n'est-ce pas ?
+
+## **DÉMARRAGE**
+
+Voici la partie classique de la section vue d'une application web :
+```txt
+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
+```
+
+## **EXEMPLE RAPIDE**
+```php
+'; // définition d'une paire clé-valeur dans l'instance
+
+// pour obtenir la valeur échappée suffit de la demander
+$x = $block['foo']; // $x = 'abc " < >'
+
+// échappement à la demande en utilisant un assistant
+$y = $block('hsc', 'any value to escape'); // ou
+$y = $block->hsc('any value to escape'); // utilisation de la saisie assistée
+
+// récupération de la valeur brut (non échappée)
+$z = $block->raw('foo'); // $z = 'abc " < >'
+
+// le type de la valeur est préservé, sont échappés toutes les chaînes de caractères et instances avec la méthode magique __toString()
+$block['bar'] = new stdClass();
+$bar = $block['bar'];
+```
+
+### **CODAGE STANDARD**
+
+Génération de la page d'accueil en utilisant plusieurs blocs `PhpEcho` séparés en plusieurs fichiers.
+Pour bien comprendre comment les fichiers sont trouvés, le chemin partiel de chaque bloc est préfixé
+par le chemin complet du répertoire racine des vues défini avec `PhpEcho::setTemplateDirRoot()`.
+```php
+ 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;
+```
+Comme vous pouvez le voir, vous composez votre vue qu'avec des blocs que vous
+devez garder indépendants le plus possible les uns des autres. Dans le contexte des vues,
+absolument tous les composants ne sont que des instances de `PhpEcho`.
+Tout est automatiquement câblé en arrière-plan et échappé par le moteur quand cela est nécessaire.
+Comme `PhpEcho` est très souple, vous composez votre vue bloc par bloc.
+
+### **CONTEXTE HTML**
+### **MISE EN PAGE - LAYOUT**
+On va créer un simple formulaire de connexion basé sur la description ci-dessus.
+En premier, création d'un fichier de mise en page appelé `main.php` dans `View/layout` avec
+des valeurs requises :
+* une description (texte)
+* un titre (texte)
+* un bloc `PhpEcho` en charge du rendu du corps de la page
+```php
+
+
+
+
+
+
+ = $this['title'] ?>
+
+
+= $this['body'] ?>
+
+
+```
+Comme toutes les instances de `PhpEcho` sont préservées et transformées en texte que quand
+cela est nécessaire, vous pouvez les appeler directement dans le code comme indiqué ci-dessus.
+
+### **FORMULAIRE**
+
+Ensuite, on créé un bloc vue appelé `login.php` dans le répertoire `View/block` contenant le code
+HTML du formulaire :
+Notez bien que `$this['url_submit']` et `$this['login']` sont automatiquement échappées
+
+```php
+
+
Please login :
+
+```
+
+### **PAGE**
+
+Enfin, on code une page `page/login.php` basée sur `layout/main.php` et on y injecte
+le corps de la page en utilisant le bloc `block/login.php`.
+```php
+ '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',
+ ]),
+]);
+```
+Code équivalent :
+```php
+renderBlock()`: le bloc enfant est anonyme dans le contexte parental et n'est plus manipulable une fois rendu
+* `$this->addBlock()`: le bloc enfant est nommé et peut être manipulable dans le contexte parental directement par son nom
+* `$this->renderByDefault()`: le bloc enfant est nommé et si le bloc parent ne fournit aucun bloc spécifique avec le même nom
+alors le moteur utilisera celui défini par défaut
+
+Notez bien encore que la vue complète doit être perçue comme un énorme arbre et que tous les blocs sont tous reliés entre eux.
+Vous ne devez jamais déclarer un bloc totalement indépendant au sein d'un autre bloc.
+Ceci n'est pas autorisé :
+```php
+
+
Please login :
+
+```
+cela doit être remplacé par une des méthodes décrites ci-dessus :
+```php
+
+
Please login :
+
+```
+Ainsi, vous ne coupez par l'arbre ;-)
+
+## **MANIPULATION ET ACCÈS À LA BALISE HEAD**
+
+Quand vous codez les vues d'un site, vous allez utiliser plein de petits blocs qui seront insérés à la
+bonne place au moment du rendu. Comme beaucoup le savent, la meilleure architecture est de s'efforcer
+de garder les blocs indépendants les uns des autres. Parfois, vous aurez le besoin d'ajouter des dépendances
+directement dans l'en-tête de la page. Dans toutes les instances de `PhpEcho`, vous disposez d'une méthode
+nommée `addhead()` qui est prévue pour.
+
+Imaginez que vous êtes dans les tréfonds du DOM, vous avez besoin de déclarer un lien vers votre librairie.
+Dans le code de votre bloc, vous n'avez qu'à rajouter :
+```php
+addHead('']);
+```
+Pas d'inquiétude concernant les caractères dangereux, tous sont échappés. Voici le code HTML gténéré :
+```html
+
+```
+Il est aussi possible de le faire en utilisant l'assistant `attributes()` :
+```php
+attributes(['type' => 'text', 'name' => 'name', 'required', 'value' => ' < > " ']) ?>>
+```
+Comme vous pouvez le voir, il y a une tonne de méthodes pour arriver au résultat souhaité.
+
+Revenons au problème précédent des clés échappées. Voici le code d'un assistant qui préserve les clés et
+échappe les valeurs en une seule fois.
+```php
+ $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**
\ No newline at end of file
diff --git a/changelog.md b/changelog.md
index 450c2b3..f5a58b4 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,13 @@
# **PhpEcho**
+**Changelog 6.1.0**
+1. New options to parameter the engine values extractor using `setSeekValueMode(string $mode)`, `$mode` among `current|parents|root`
+2. If the current block is not able to provide a value to be rendered then the engine will automatically seek for it using the `seekValueMode` parameter
+3. Detect an infinite loop when building a view
+4. Allowing the render of recursive arrays of PhpEcho blocks
+5. Cloning a `PhpEcho` block is now forbidden, the engine will throw an `BadMethodCallException`
+6. Tests are updated
+
**Changelog 6.0.1**
1. Minor bugfix in `addBlock()`
@@ -7,13 +15,13 @@
1. Code refactoring
2. As PHP is now a pretty self-describing and self-documenting language, the quantity of PHPDoc is now heavily reduced
3. Removed feature: space notation for arrays. Any space in a key is now preserved, the engine doesn't interpret them as sub-arrays anymore
-5. New feature: management of local and global vars, please note that the local always override the global ones
-3. New feature: defining global values that will be available through the whole tree of blocks using: `injectVars(array $vars)`
-4. New feature: defining local values after instantiating a `PhpEcho` block at once using: `setVars(array $p)`
-6. Internal heavy change: there's no more copy of variables between blocks (reduce the memory footprint and increase the global performance)
-7. If the current block is not able to provide a value to be rendered then the engine will automatically seek for it in the root of the tree
-8. Better management of values composed of nested array
-9. Cloning a `PhpEcho` block is now possible, the cloned value keeps everything but the link to its parent block. The new one is orphan
+4. New feature: management of local and global vars, please note that the local always override the global ones
+5. New feature: defining global values that will be available through the whole tree of blocks using: `injectVars(array $vars)`
+6. New feature: defining local values after instantiating a `PhpEcho` block at once using: `setVars(array $p)`
+7. Internal heavy change: there's no more copy of variables between blocks (reduce the memory footprint and increase the global performance)
+8. If the current block is not able to provide a value to be rendered then the engine will automatically seek for it in the root of the tree
+9. Better management of values composed of nested array
+10. Cloning a `PhpEcho` block is now possible, the cloned value keeps everything but the link to its parent block. The new one is orphan
**Changelog 5.4.1:**
1. Minor bugfix in method `isArrayOfPhpEchoBlocks(mixed $p)` when `$p` is an empty array
@@ -32,7 +40,7 @@
3. You can now define the seek order to get the first value either
from the `local` or `global` context using `getAnyParam(string $name, string $seek_order = 'local'): mixed`
4. It's possible to set at once a parameter into the local and global context using `setAnyParam(string $name, mixed $value)`
-4. It's possible to unset at once a parameter from the local and the global context using `unsetAnyParam(string $name)`
+5. It's possible to unset at once a parameter from the local and the global context using `unsetAnyParam(string $name)`
Test files are updated
**Changelog 5.2.1:**
@@ -65,12 +73,12 @@
1. Removing th constant `HELPER_BOUND_TO_CLASS_INSTANCE`, it's replaced by `PhpEcho::addBindableHelper`
2. Removing the constant `HELPER_RETURN_ESCAPED_DATA`. Now, the engine is able to check when data must
be escaped and preserve the native datatype when it's safe in HTML context
-2. Instead of dying silently with `null` or empty string, the engine now throws in all case an `Exception`
+3. Instead of dying silently with `null` or empty string, the engine now throws in all case an `Exception`
You must produce a better code as it will crash on each low quality segment.
-3. Add new method `renderBlock()` to link easily a child block to its parent
-4. Many code improvements
-5. Fully tested: the core and all helpers have been fully tested
-6. Add new helper to the standard library `renderIfNotSet()` that render a default value instead
+4. Add new method `renderBlock()` to link easily a child block to its parent
+5. Many code improvements
+6. Fully tested: the core and all helpers have been fully tested
+7. Add new helper to the standard library `renderIfNotSet()` that render a default value instead
of throwing an `Exception` for any missing key in the stored key-value pairs
**Changelog 5.0.0:**
diff --git a/tests/README.md b/tests/README.md
index a7b8846..0ed8f4f 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,6 +1,6 @@
# PhpEcho : a native PHP templating engine in one class
-`2023-08-12` `PHP 8.0+` `6.0.0`
+`2023-09-24` `PHP 8.0+` `6.1.0`
## TESTS
diff --git a/tests/autowire.php b/tests/autowire.php
index bc9915d..d74eb72 100644
--- a/tests/autowire.php
+++ b/tests/autowire.php
@@ -5,6 +5,8 @@
/** @var Pilot $pilot */
+PhpEcho::setSeekValueMode('parents');
+
$root = new PhpEcho(id: 'root');
$block_1 = new PhpEcho(id: 'block_1');
$block_11 = new PhpEcho(id: 'block_11');
@@ -24,7 +26,7 @@
$pilot->run(
id: 'autowire_001',
test: fn() => $block_1['a'],
- description: 'no scalar vars defined, after injecting them, check the values are available for the whole tree components',
+ description: 'no scalar vars defined, after directly injecting them into the root, check the values are available for the whole tree components',
);
$pilot->assertIsInt();
$pilot->assertEqual(1);
@@ -32,7 +34,40 @@
$pilot->run(
id: 'autowire_002',
test: fn() => $block_121['b'],
- description: 'no scalar vars defined, after injecting them, check the values are available for the whole tree components and escaped',
+ description: 'no scalar vars defined, after directly injecting them into the root, check the values are available for the whole tree components and escaped',
+);
+$pilot->assertIsString();
+$pilot->assertEqual('abc " < >');
+
+
+$root = new PhpEcho(id: 'root');
+$block_1 = new PhpEcho(id: 'block_1');
+$block_11 = new PhpEcho(id: 'block_11');
+$block_12 = new PhpEcho(id: 'block_12');
+$block_121 = new PhpEcho(id: 'block_121');
+$block_1211 = new PhpEcho(id: 'block_1211');
+
+$root['block_1'] = $block_1;
+$block_1['block_11'] = $block_11;
+$block_1['block_12'] = $block_12;
+$block_12['block_121'] = $block_121;
+$block_121['block_1211'] = $block_1211;
+
+
+$block_121->injectVars(['a' => 1, 'b' => 'abc " < >']);
+
+$pilot->run(
+ id: 'autowire_003',
+ test: fn() => $block_1['a'],
+ description: 'no scalar vars defined, after directly injecting them into one leaf, check the values are available for the whole tree components',
+);
+$pilot->assertIsInt();
+$pilot->assertEqual(1);
+
+$pilot->run(
+ id: 'autowire_004',
+ test: fn() => $block_121['b'],
+ description: 'no scalar vars defined, after directly injecting them into one leaf, check the values are available for the whole tree components and escaped',
);
$pilot->assertIsString();
$pilot->assertEqual('abc " < >');
diff --git a/tests/core.php b/tests/core.php
index 9939353..be6ae21 100644
--- a/tests/core.php
+++ b/tests/core.php
@@ -145,3 +145,82 @@ class: $block,
);
$pilot->assertIsBool();
$pilot->assertEqual(false);
+
+
+PhpEcho::setTemplateDirRoot(__DIR__.DIRECTORY_SEPARATOR.'view');
+
+$data = [
+ 'abc' => new PhpEcho('block/block_02.php', ['block_02_text' => 'abc'], 'block_abc'),
+ 'def' => new PhpEcho('block/block_02.php', ['block_02_text' => 'def'], 'block_def'),
+ 'ghi' => new PhpEcho('block/block_02.php', [
+ 'jkl' => new PhpEcho('block/block_02.php', ['block_02_text' => 'jkl'], 'block_jkl'),
+ 'block_02_text' => 'ghi',
+ ], 'block_ghi'),
+ 'mno' => new PhpEcho('block/block_02.php', ['block_02_text' => 'mno'], 'block_mno'),
+ 'pqr' => new PhpEcho('block/block_02.php', ['block_02_text' => 'pqr'], 'block_pqr'),
+];
+
+$root = new PhpEcho(id: 'root');
+
+$pilot->runClassMethod(
+ id: 'core_015',
+ class: $root,
+ description: 'check isArrayOfPhpEchoBlocks with a recursive array',
+ method: 'isArrayOfPhpEchoBlocks',
+ params: [$data],
+);
+$pilot->assertIsBool();
+$pilot->assertEqual(true);
+
+$data['xyz'] = 'break php echo array';
+
+$pilot->runClassMethod(
+ id: 'core_016',
+ class: $root,
+ description: 'check isArrayOfPhpEchoBlocks with a recursive array',
+ method: 'isArrayOfPhpEchoBlocks',
+ params: [$data],
+);
+$pilot->assertIsBool();
+$pilot->assertEqual(false);
+
+$data = [
+ 'abc' => new PhpEcho('block/block_02.php', ['block_02_text' => 'abc'], 'block_abc'),
+ 'def' => new PhpEcho('block/block_02.php', ['block_02_text' => 'def'], 'block_def'),
+ 'ghi' => new PhpEcho('block/block_02.php', [
+ 'block_02_text' => [
+ new PhpEcho('block/block_02.php', ['block_02_text' => 'ghi555'], 'block_555'),
+ new PhpEcho('block/block_02.php', ['block_02_text' => 'ghi666'], 'block_666')],
+ ], 'block_ghi'),
+ 'mno' => new PhpEcho('block/block_02.php', ['block_02_text' => 'mno'], 'block_mno'),
+ 'pqr' => new PhpEcho('block/block_02.php', ['block_02_text' => 'pqr'], 'block_pqr'),
+];
+
+$root = new PhpEcho(file: 'layout_01.php', vars: ['body' => $data], id: 'root');
+
+ob_start();
+echo $root;
+$html = ob_get_clean();
+$pilot->run(
+ id : 'core_017',
+ test : fn() => $html,
+ description : 'recursive array of php echo block rendering'
+);
+$pilot->assertEqual(<<
+
+
+
+
+
+
abc
+
def
+
ghi555
+
ghi666
+
+
mno
+
pqr
+
+
+html);
+
diff --git a/tests/global_tests_result.jpg b/tests/global_tests_result.jpg
index e7ec229..968931f 100644
Binary files a/tests/global_tests_result.jpg and b/tests/global_tests_result.jpg differ
diff --git a/tests/heredoc.php b/tests/heredoc.php
index e1aea86..c2bf9df 100644
--- a/tests/heredoc.php
+++ b/tests/heredoc.php
@@ -460,4 +460,4 @@
-html);
\ No newline at end of file
+html);
diff --git a/tests/infinite_loop.php b/tests/infinite_loop.php
new file mode 100644
index 0000000..4d2accf
--- /dev/null
+++ b/tests/infinite_loop.php
@@ -0,0 +1,53 @@
+setCode('
+
+html);
+
+
+$layout = new PhpEcho(file: 'layout_07.php', id: 'root');
+$layout['block'] = new PhpEcho(file: 'block/block_07.php');
+
+$pilot->run(
+ id: 'infinite_loop_02',
+ test: fn() => (string)$layout,
+ description: 'infinite loop, block calling each others'
+);
+$pilot->assertException(InvalidArgumentException::class);
diff --git a/tests/options.php b/tests/options.php
new file mode 100644
index 0000000..616e632
--- /dev/null
+++ b/tests/options.php
@@ -0,0 +1,156 @@
+run(
+ id: 'option_001',
+ test: fn() => $block['def'],
+ description: 'null if not exists deactivated, seek mode current, value is only available in the current block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('block_value');
+
+$pilot->run(
+ id: 'option_002',
+ test: fn() => $block['abc'],
+ description: 'null if not exists deactivated, seek mode current, value in root is not accessible from the child'
+);
+$pilot->assertException(InvalidArgumentException::class);
+
+PhpEcho::setNullIfNotExists(true);
+$pilot->run(
+ id: 'option_003',
+ test: fn() => $block['xyz'],
+ description: 'null if not exists activated, seek mode current, current block asked key does not exists'
+);
+$pilot->assertEqual(null);
+
+$pilot->run(
+ id: 'option_004',
+ test: fn() => $block['abc'],
+ description: 'null if not exist activated, seek mode current, asked key from the current block does not exist in the whole tree'
+);
+$pilot->assertEqual(null);
+
+PhpEcho::setNullIfNotExists(false);
+PhpEcho::setSeekValueMode('parents');
+
+$sub_block = new PhpEcho(id: 'sub_block');
+$block['sub_block'] = $sub_block;
+
+$pilot->run(
+ id: 'option_005',
+ test: fn() => $sub_block['def'],
+ description: 'null if not exist deactivated, seek mode parents, asked key from the current block only exist in the parent block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('block_value');
+
+$pilot->run(
+ id: 'option_006',
+ test: fn() => $block['abc'],
+ description: 'null if not exist deactivated, seek in parents activated, asked key from the current block only exist in the root block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('root_value');
+
+PhpEcho::setNullIfNotExists(false);
+PhpEcho::setSeekValueMode('root');
+
+$pilot->run(
+ id: 'option_007',
+ test: fn() => $sub_block['def'],
+ description: 'null if not exist deactivated, seek mode root, asked key from the current block only exist in the parent block'
+);
+$pilot->assertException(InvalidArgumentException::class);
+
+$pilot->run(
+ id: 'option_008',
+ test: fn() => $block['abc'],
+ description: 'null if not exist deactivated, seek mode root, asked key from the current block only exist in the root block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('root_value');
+
+PhpEcho::setNullIfNotExists(true);
+PhpEcho::setSeekValueMode('root');
+
+$pilot->run(
+ id: 'option_009',
+ test: fn() => $sub_block['def'],
+ description: 'null if not exist activated, seek mode root, asked key from the current block only exist in the parent block'
+);
+$pilot->assertEqual(null);
+
+$pilot->run(
+ id: 'option_010',
+ test: fn() => $block['abc'],
+ description: 'null if not exist deactivated, seek mode root, asked key from the current block only exist in the root block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('root_value');
+
+PhpEcho::setNullIfNotExists(true);
+PhpEcho::setSeekValueMode('parents');
+
+$pilot->run(
+ id: 'option_011',
+ test: fn() => $sub_block['def'],
+ description: 'null if not exist activated, seek mode parents, asked key from the current block only exist in the parent block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('block_value');
+
+$pilot->run(
+ id: 'option_012',
+ test: fn() => $block['abc'],
+ description: 'null if not exist activated, seek mode parents, asked key from the current block only exist in the root block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('root_value');
+
+$pilot->run(
+ id: 'option_013',
+ test: fn() => $block['xyz'],
+ description: 'null if not exist activated, seek mode parents, asked key does not exists in the whole tree'
+);
+$pilot->assertEqual(null);
+
+PhpEcho::setNullIfNotExists(true);
+PhpEcho::setSeekValueMode('parents');
+
+$pilot->run(
+ id: 'option_014',
+ test: fn() => $sub_block['def'],
+ description: 'all options activated, asked key from the current block only exist in the parent block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('block_value');
+
+$pilot->run(
+ id: 'option_015',
+ test: fn() => $block['abc'],
+ description: 'all options activated, asked key from the current block only exist in the root block'
+);
+$pilot->assertIsString();
+$pilot->assertEqual('root_value');
+
+$pilot->run(
+ id: 'option_016',
+ test: fn() => $block['xyz'],
+ description: 'all options activated, asked key does not exists in the whole tree'
+);
+$pilot->assertEqual(null);
\ No newline at end of file
diff --git a/tests/tests.php b/tests/tests.php
index 7fd9089..835d13f 100644
--- a/tests/tests.php
+++ b/tests/tests.php
@@ -3,11 +3,8 @@
/**
* TESTS ARE WRITTEN FOR EXACODIS PHP TEST ENGINE
* AVAILABLE AT https://github.com/rawsrc/exacodis
- *
- * To run the tests, you must only define a db user granted with all privileges
*/
-$a = 1;
//region setup test environment
include_once '../vendor/exacodis/Pilot.php';
include_once '../vendor/exacodis/Report.php';
@@ -17,7 +14,7 @@
use Exacodis\Pilot;
-$pilot = new Pilot('PhpEcho - A native PHP template engine - v.6.0.1');
+$pilot = new Pilot('PhpEcho - A native PHP template engine - v.6.1.0');
$pilot->injectStandardHelpers();
include 'filepath.php';
@@ -25,9 +22,11 @@
include 'helpers.php';
include 'stdHelpers.php';
include 'core.php';
+include 'options.php';
include 'autowire.php';
include 'view.php';
include 'heredoc.php';
include 'viewBuilder.php';
+include 'infinite_loop.php';
$pilot->createReport();
\ No newline at end of file
diff --git a/tests/view.php b/tests/view.php
index 5f3808c..68378ea 100644
--- a/tests/view.php
+++ b/tests/view.php
@@ -220,6 +220,8 @@
html);
+PhpEcho::setSeekValueMode('parents');
+
$layout = new PhpEcho('layout_06.php');
$layout['block_03_text'] = 'foo_text';
$layout->addBlock('block', 'block/block_03.php'); // bloc_03 expects to have a value for 'block_03_text' which is defined in the layout
@@ -263,4 +265,4 @@