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'] ?> - - - - - -``` -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 - - - - -``` -or like this: -```php - - - - - - - - - - -``` -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 - - -renderByDefault('preloader', 'block/preloader.php') ?> -renderByDefault('top_header', 'block/top_header.php') ?> -renderByDefault('navbar', 'block/navbar.php') ?> -renderByDefault('navbar_mobile', 'block/navbar_mobile.php') ?> - -renderByDefault('footer', 'block/footer.php') ?> -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'] ?> - - - - - -``` -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'] ?> + + + + + +``` +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('

Block a

'); + +$b = new PhpEcho(id: 'b'); +$b->setCode('

Block b

'); + + +$layout['a'] = $a; +$layout['b'] = $b; + +$layout['block_01'] = $layout['a']; +$layout['block_02'] = $layout['a']; + +ob_start(); +echo $layout; +$html = ob_get_clean(); + +$pilot->run( + id: 'infinite_loop_01', + test: fn() => $html, + description: 'multiple usage of the same block, no infinite loop' +); +$pilot->assertIsString(); +$pilot->assertEqual(<< + + + + + +

Block a

Block a

+ +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 @@

abcdef

-html); \ No newline at end of file +html); diff --git a/tests/view/block/block_07.php b/tests/view/block/block_07.php new file mode 100644 index 0000000..94565e0 --- /dev/null +++ b/tests/view/block/block_07.php @@ -0,0 +1,2 @@ + +

renderBlock('block/block_08.php') ?>

\ No newline at end of file diff --git a/tests/view/block/block_08.php b/tests/view/block/block_08.php new file mode 100644 index 0000000..1bf3acc --- /dev/null +++ b/tests/view/block/block_08.php @@ -0,0 +1,2 @@ + +

renderBlock('block/block_07.php') ?>

\ No newline at end of file