lucinda/view-language

High performance view templating API for PHP applications using tags & expressions inspired by Java JSTL and C Compiler

v4.0.4 2023-12-26 12:24 UTC

README

Table of contents:

About

This API is the PHP compiler for ViewLanguage templating engine, a markup language inspired by JSP&JSTL @ Java that acts like an extension of HTML standard, designed to completely eliminate the need for scripting in views by:

  • interfacing variables through expressions.
  • interfacing logics (control structures, repeating html segments) through tags

diagram

In order to achieve its goals, following steps need to be observed:

API is fully PSR-4 compliant, only requiring PHP8.1+ interpreter and SimpleXML extension. To quickly see how it works, check:

  • installation: describes how to install API on your computer, in light of steps above
  • unit tests: API has 100% Unit Test coverage, using UnitTest API instead of PHPUnit for greater flexibility
  • examples: shows an example how to template with ViewLanguage, including explanations for each step
  • reference guide: shows list of tags that come with API

Expressions

An expression is a ViewLanguage representation of a scripting variable. Syntax for an expression is:

${variableName}

where variableName can be:

Description ViewLanguage Example PHP Translation
a scalar variable ${foo} $foo
an array variable, where hierarchy is represented by dots ${foo.bar} $foo["bar"]
a back-end helper function (native or user-defined) ${htmlspecialchars(${foo.bar})} htmlspecialchars($foo["bar"])
a short if using ternary operators ${(${foo.bar}!=3?"Y":"N")} ($foo["bar"]!=3?"Y":"N")

A very powerful feature is the ability to nest expressions: writing expressions whose key(s) are expressions themselves. This can go at any depth and it is very useful when iterating through more than one list and linking a one to another's key/value association:

ViewLanguage Example PHP Translation
${foo.bar.${baz}} $foo["bar"][$baz]

Tags

A tag is a ViewLanguage representation of a scripting logic. All tags act like an extension of HTML standard and as such they have names and optionally attributes and bodies. There are two types of tags known by ViewLanguage:

  • macro tags: api-defined tags to be processed before content is compiled.
  • library tags: api/user-defined tags subject to compilation that belong to a library and have an unique name within that library.

A very powerful feature is the ability of tags to be recursive: it is allowed to put View Language tags inside View Language tags. Whenever that happens, compiler goes progressively deeper until no tags are left!

Macro Tags

Macro tags work in a way similar to C macros: before code is compiled, they are read and "expanded" so that compilation will run on a full source. Syntax is identical to that of normal HTML tags:

<NAME ATTRIBUTE="value" .../>

Where:

  • NAME: name of tag that performs a single logical operation.
  • ATTRIBUTE: configures tag behavior

API defines following macro tags:

  • escape: tag whose body will be ignored by compiler. This is necessary to mark content inside as not subject to parsing.
  • import: tag whose declaration will be replaced by compiler with the body of file pointed by its "file" attribute. This is crucial for layouting/templating.
  • namespace: tag whose declaration will inform compiler where to look for tag libraries not found in default folder.

At the moment, it is not possible for users to define their own macro tags!

Library Tags

Library tags are compilable api/user-defined HTML snippets expected to implement scripting logic in a View Language application. They are non-static repeating snippets of template (html) code that depend on variables and thus can't be loaded using .

Their syntax extends HTML standard:

<LIBRARY:TAG ATTRIBUTE="value" ...>...</LIBRARY:TAG>

or, if they have no body:

<LIBRARY:TAG ATTRIBUTE="value" .../>

Where:

  • LIBRARY: namespace that contains related logical operations to perform on a template. Rules:
    • Value must be lowercase and alphanumeric.
    • "-" sign is allowed as well to replace spaces in multi-word values
  • TAG: name of tag that performs a single logical operation.Rules:
    • Value must be lowercase and alphanumeric.
    • sign is allowed as well to replace spaces in multi-word values
  • ATTRIBUTE: configures tag behavior (can be zero or more). Rules:
    • Name must be lowercase and alphanumeric.
    • "_" sign is allowed as well to replace spaces in multi-word names
    • Value can only be primitive (string or number) or ViewLanguage expressions.
    • Unlike standard HTML, attributes cannot be multilined currently.

Standard Tags

API includes a standard library containing tags for programming language instructions where LIBRARY is empty:

  • :for: iterates numerically through a list
  • :foreach: iterates through a dictionary by key and value
  • :if: evaluates body fitting condition
  • :elseif: evaluates body fitting condition that did not met previous if/elseif.
  • :else: evaluates body that did not met previous if/else if
  • :set: creates a variable and/or sets a value for it.
  • :unset: unsets a variable.
  • :while: performs a loop on condition.
  • :break: ends loop.
  • :continue: skips evaluating the rest of current loop and moves to next iteration.

Standard tags work with ATTRIBUTE values of following types:

  • scalars: strings or integers
  • EXPRESSION: ViewLanguage expressions. If helper functions are used as attribute values, they must be left undecorated: count(${asd}) instead of ${count(${asd})}.
  • CONDITION: syntax is expected to be C-like, but ultimately matches that of language code is compiled into (in our case, PHP). Example: ${x}==true means in PHP $x==true.

User Defined Tags

In order to break up HTML response into discrete units, developers must create their own libraries & tags. User defined tags are found on disk according to these rules:

  • library name must be a folder inside tags folder supplied on compilation
  • tag code muse be a HTML file inside library folder whose name equals name
ViewLanguage Example PHP Translation
<foo:baz attr="1"/> $contents = file_get_contents($tagsFolder."/foo/baz.html");
// replaces all occurrences of $[attr] with 1

Configuration

To configure this API you must have a XML with a templating tag inside:

<templating compilations_path="..." tags_path="..." templates_path="..." templates_extension="..." />

Where:

  • compilations_path: (mandatory) path into which PHP compilations of ViewLanguage templates are saved
  • tags_path: (optional) path into which ViewLanguage tag libraries are located
  • templates_path: (optional) path into which templates are located
  • templates_extension: (optional) template files extension. If not set, "html" is assumed!

Example:

<templating compilations_path="compilations" tags_path="application/taglib" templates_path="application/views" templates_extension="html"/>

Compilation

Once you have completed step above, you need to instantiate Lucinda\Templating\Wrapper in order to be able to compile templates later on:

$wrapper = new Lucinda\SQL\Wrapper(simplexml_load_file(XML_FILE_NAME));

Object has following method that can be used to compile one or more templates:

Method Arguments Returns Description
compile string $template, array $data string Compiles ViewLanguage template into HTML and returns result

How are templates compiled?

As in any other templating language, compilation first traverses the tree of dependencies ever deeper and assembles result into a PHP file then produces an HTML by binding it to data received by user. It thus involves following steps:

  • if a PHP compilation for $template argument exists, checks if elements referenced inside have changed since it was last updated. If it doesn't exist or it changed:
    • parses tags recursively (backing-up tag bodies in compilation file to be excluded from parsing) and appends results to compilation file
    • parses tags defined in templates, to know where to locate user-defined tag libraries not defined in default taglib folder
    • parses library tags recursively (backing-up tag bodies in compilation file to be excluded from parsing) and replaces them with relevant PHP/HTML code in compilation file.
    • parses expressions and replaces them with relevant PHP code in compilation file.
    • restores backed up tags bodies (if any) in compilation file
    • caches new compilation on disk along with a checksum of its parts (templates, tags) for future validations
  • in output buffer, loads compilation file, binds it to $data supplied by user and produces a final HTML out of it

Since the whole process is somewhat performance hungry, PHP compilation files will be cached on disk and returned directly on next requests unless one of its components (template or tag) has changed. This makes API able to compile in around 0.001 sec amortised time, thus bringing no performance taxation whatsoever but all the advantages of an elegant view!

Installation

First choose a folder where API will be installed then write this command there using console:

composer require lucinda/templating

Then create a configuration.xml file holding configuration settings (see configuration above) and a index.php file in project root with following code:

require(__DIR__."/vendor/autoload.php");
$wrapper = new Lucinda\SQL\Wrapper(simplexml_load_file("configuration.xml"));

To compile a template:

$html = $wrapper->compile(TEMPLATE_NAME, USER_DATA);

Where:

  • TEMPLATE_NAME is the base template that must obey following rules:
    • must be a path to a file located in templates_path (see configuration)
    • file it points to must have templates_extension (see configuration)
    • because of above, must not include extension
  • USER_DATA is a list of values from back-end to be accessed in template, obeying following rules:
    • must be an array
    • entry keys must be strings or integers (the usual PHP requirements)
    • entry values must be scalars or arrays
    • if array is multidimensional, keys and values in siblings must obey same rules as above

To display results:

header("Content-Type: text/html; charset=UTF-8");
echo $html;

Unit Tests

For tests and examples, check following files/folders in API sources:

  • unit-tests.sql: SQL commands you need to run ONCE on server (assuming MySQL) before unit tests execution
  • test.php: runs unit tests in console
  • unit-tests.xml: sets up unit tests and mocks "sql" tag
  • tests: unit tests for classes from src folder

Examples

Assuming configuration.xml (see configuration and installation) is:

<xml>
   <templating compilations_path="compilations" tags_path="application/taglib" templates_path="application/views" templates_extension="html"/>
</xml>

Let's create a application/views/index.html template with following body:

<import file="header"/>
Hello, dear ${data.author}! Author of:
<ul>
    <:foreach var="${data.apis}" key="name" val="url">
    <my:api id="${name}" link="${url}"/>
    </:foreach>
</ul>
<import file="footer"/>

What above does is:

  • loads body of a application/views/header.html template
  • echoes value of author array key of $data via expressions
  • loops through value of apis array key of $data via <:foreach>
  • on every iteration, loads body of application/taglib/my/api.html template (user tag), binding attributes to values
  • loads body of a application/views/footer.html template

Contents of application/views/header.html file:

<html>
    <head>
        <title>View Language API Tutorial</title>
    </head>
    <body>

Contents of application/taglib/my/api.html file:

<li><a href="$[id]">$[link]</a></li>

Contents of application/views/footer.html:

    </body>
</html/>

As one can see above, templates depend on variables to be received from back-end (author & apis), both keys of $data array. Assuming value of latter is:

$data = [
	"author" => "Lucian Popescu", 
	"apis" => ["View Language API" => "https://www.lucinda-framework.com/view-language", "STDOUT MVC API" => "https://www.lucinda-framework.com/stdout-mvc"]
];

Now let's compile application/views/index.html template (see compilation) and bind it to $data:

require(__DIR__."/vendor/autoload.php");
$wrapper = new Lucinda\SQL\Wrapper(simplexml_load_file("configuration.xml"));
$html = $wrapper->compile("index", $data);

First, ViewLanguage base template is compiled into PHP, results saved in a compilations/index.php file (in case it doesn't exist already) with following body:

<html>
    <head>
        <title>View Language API Tutorial</title>
    </head>
    <body>
        Hello, <?php echo $data["author"]; ?>, author of:
        <ul>
            <?php foreach($data["apis"] as $name=>$url) { ?>
            <li><a href="<?php echo $url; ?>"><?php echo $name; ?></a></li>
            <?php } ?>
        </ul>
    </body>
</html/>

Then above file is loaded in output buffer and bound with $data, so the final HTML returned will be:

<html>
    <head>
        <title>View Language API Tutorial</title>
    </head>
    <body>
        Hello, Lucian Popescu, author of:
        <ul>
            <li><a href="https://www.lucinda-framework.com/view-language">View Language API</a></li>
            <li><a href="https://www.lucinda-framework.com/stdout-mvc">STDOUT MVC API</a></li>
        </ul>
    </body>
</html/>

Reference Guide

tag escape

Marks tag body to be ignored by ViewLanguage compiler.Syntax:

<escape>
...
</escape>

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<escape>
${foo.bar}
</escape>
${foo.bar}

tag import

Includes another view language template into current one. Syntax:

<import file="..."/>

Attributes:

Name Mandatory Data Type Description
file Y string Location of file whose sources should replace tag declaration relative to views folder supplied to compiler

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<import file="header"/> require_once($viewsFolder."/header.html")

tag namespace

Marks custom location of user defined tag library (must be placed BEFORE latter declaration). Syntax:

<namespace taglib="..." folder="..."/>

Attributes:

Name Mandatory Data Type Description
taglib Y string Name of tag library to look for.
folder Y string Location of folder tag library should be looked for relative to tags folder supplied to compiler

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<namespace taglib="foo" folder="bar"/>
...
<foo:baz attr="1"/>
...
$contents = file_get_contents($tagsFolder."/bar/foo/baz.html");
// replaces all instances of $[attr] with 1

tag :for

Creates a FOR loop. Syntax:

<:for var="..." start="..." end="..." (step="...")>
    ...
</:for>

Attributes:

Name Mandatory Data Type Description
var Y string Name of counter variable.
start Y integer Value of begin counter.
end Y integer Value of end counter.
step N integer Value of increment/decrement step (default: 1).

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:for var="i" start="0" end="10">
...
</:for>
for($i=0; $i<=10; $i=$i+1){
...
}
<:for var="i" start="10" end="0" step="-1">
...
</:for>
for($i=10; $i>=0; $i=$i-1){
...
}

tag :foreach

Creates a FOR EACH loop. Syntax:

<:foreach var="..." (key="...") val="...">
    ...
</:foreach>

Attributes:

Name Mandatory Data Type Description
var Y EXPRESSION Variable to iterate.
key N string Name of key variable.
val Y string Name of value variable.

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:foreach var="${a}" key="k" val="v">
...
</:foreach>
foreach($a as $k=>$v) {
...
}
<:foreach var="${a}" val="v">
...
</:foreach>
foreach($a as $v) {
...
}

tag :if

Creates an IF condition. Syntax:

<:if test="...">
    ...
</:if>

Tag must not be closed if folowed by a :else or :elseif!

Attributes:

Name Mandatory Data Type Description
test Y CONDITION Condition when body is executed.

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:if test="${x}==2">
...
</:if>
if($x==2) {
...
}

You can also run simple IF/ELSE statements from expressions using ternary operators!

tag :elseif

Creates an ELSE IF condition. Syntax:

<:elseif test="...">
    ...
</:if>

Tag must not be closed if folowed by a :else or :elseif!

Attributes:

Name Mandatory Data Type Description
test Y CONDITION Condition when body is executed.

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:elseif test="${x}==2">
...
</:if>
} elseif($x==2) {
...
}

tag :else

Creates an ELSE condition. Syntax:

<:else>
    ...
</:if>

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:else>
...
</:if>
} else {
...
}

tag :set

Sets a value to a variable.Syntax:

<:set var="..." val="..."/>

Attributes:

Name Mandatory Data Type Description
var Y string Name of variable to be created/updated.
val Y string
EXPRESSION
Value of variable.

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:set var="a" val="10" $a = 10;
<:set var="a" val="${x}" $a = $x;

tag :unset

Removes variable from memory. Syntax:

<:unset var="..."/>

Attributes:

Name Mandatory Data Type Description
var Y string Name of variable to be unset.

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:unset var="a" unset($a);

tag :while

Creates a WHILE loop. Syntax:

<:while test="...">
    ...
</:while>

Attributes:

Name Mandatory Data Type Description
test Y CONDITION Condition when body is executed.

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:while test="${x}!=2">
...
</:while>
while($x!=2) {
...
}

tag :break

Breaks a FOR/FOR EACH/WHILE statement loop. Syntax:

<:break/>

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:break/:while> break;

tag :continue

Continues to next step within a FOR/FOR EACH/WHILE statement loop. Syntax:

<:continue/>

Examples how this tag is compiled into PHP:

ViewLanguage Example PHP Translation
<:break/:while> break;