Code Monkey home page Code Monkey logo

endobox's Introduction

endobox

endobox

minimal template engine.

Build Status Code Climate Latest Stable Version Total Downloads License

endobox

🌱 Native PHP syntax 📝 Markdown on-board  :rocket: Minimal API
Write templates in vanilla PHP. No need to learn a new syntax. A full-blown Markdown parser is built right in. Yes, it can be combined with PHP! Do powerful things with just a handful of elementary methods.

Documentation

Installation

Install endobox is via Composer:

composer require younishd/endobox

You will need PHP 7.0+.

Get started

The typical way to configure endobox to load templates for an application looks like this:

require_once '/path/to/vendor/autoload.php';

use endobox\Endobox;

$endobox = Endobox::create('path/to/templates');

You can add additional template locations:

$endobox->addFolder('another/path/to/templates');

Render templates

Instantiate a Box for your template:

$welcome = $endobox('welcome');

Render the template with some variables by calling render():

echo $welcome->render([ 'name' => "Alice" ]);

The template file itself could look like this:

welcome.php
<h1>Hello, <?= $name ?>!</h1>

File extensions

endobox decides how to render a template based on the file extension.

When you instantiate the template Box however, the extension is omitted.

$members = $endobox('members'); // no file extension

PHP: .php

PHP templates are processed by evaluating the code between PHP tags (i.e., <? … ?>) and returning the result.

members.php
<h1>Members</h1>
<ul>
    <?php foreach ($users as $u): ?>
        <li><?= $u->name ?></li>
    <?php endforeach ?>
</ul>

ℹ️ Protip: The <?= is syntactic sugar for <?php echo.

Markdown: .md

Markdown templates are processed by a Markdown parser (Parsedown) which produces the corresponding HTML code. This can be used for static content.

members.md
# Members

- Alice
- Bob
- Carol

PHP+Markdown: .md.php

As the name suggests, this template type combines both PHP and Markdown: The template gets evaluated as PHP first, then parsed as Markdown. Pretty neat.

members.md.php
# Members

<?php foreach ($users as $u): ?>
    - <?= $u->name ?>
<?php endforeach ?>

HTML: .html

HTML templates are always printed as is. No further processing takes place.

members.html
<h1>Members</h1>
<ul>
    <li>Alice</li>
    <li>Bob</li>
    <li>Carol</li>
</ul>

Data

Data is accessible inside a template as simple variables (e.g., $foo) where the variable name corresponds to the assigned array key or property.

<h1>Hello, <?= $username ?>!</h1>

Assign data

There are several ways to assign data to a template box:

// via assign(…)
$welcome->assign([ "username" => "eve" ]);

// via object property
$welcome->username = "eve";

// via render(…)
$welcome->render([ "username" => "eve" ]);

// implicitly
$welcome([ "username" => "eve" ]);

Shared data

Usually, template boxes are isolated from each other. Data that's been assigned to one box, will not be visible from another.

$welcome->username = "eve";          // not accessible to 'profile'
$profile->email = "[email protected]"; // not accessible to 'welcome'

If they should share their data however, you can link them together:

$welcome->link($profile);

Now, these template boxes are linked and they share the same data.

welcome.php
<h1>Hello, <?= $username ?>!</h1>
<p>Your email address is: <code><?= $email ?></code></p>
profile.php
<h1>Profile</h1>
<ul>
    <li>Username: <strong><?= $username ?></strong></li>
    <li>Email: <strong><?= $email ?></strong></li>
</ul>

Notice how welcome.php prints out $email which was initially assigned to $profile and profile.php echoes $username even though it was assigned to $welcome.

ℹ️ Protip: You can create template boxes using an existing Box object (instead of using the BoxFactory object) with $box->create('template') which has the advantage of linking the two boxes together by default.

Default values

Sometimes it can be useful to supply a default value to be printed in case a variable has not been assigned. You can easily achieve that using PHP 7's null coalescing operator: ??

<title><?= $title ?? "Default" ?></title>

Escaping

Escaping is a form of data filtering which sanitizes unsafe, user supplied input prior to outputting it as HTML.

endobox provides two shortcuts to the htmlspecialchars() function: $escape() and its shorthand version $e()

<h1>Hello, <?= $escape($username) ?>!</h1>

<h1>Hello, <?= $e($username) ?>!</h1>
Escaping HTML attributes

⚠️ Warning: It's VERY important to always double quote HTML attributes that contain escaped variables, otherwise your template will still be open to injection attacks (e.g., XSS).

<!-- Good -->
<img src="portrait.jpg" alt="<?= $e($name) ?>">

<!-- BAD -->
<img src="portrait.jpg" alt='<?= $e($name) ?>'>

<!-- BAD -->
<img src="portrait.jpg" alt=<?= $e($name) ?>>

Chaining & Nesting

Since you're rarely dealing with just a single template you might be looking for a method that combines multiple templates in a meaningful way.

Chaining

By chaining we mean concatenating templates without rendering them.

Chaining two templates is as simple as:

$header($article);

Now, calling ->render() on either $header or $article will render both templates and return the concatenated result.

ℹ️ Protip: The benefit of not having to render the templates to strings right away is flexibility: You can define the layout made out of your templates before knowing the concrete values of their variables.

The general syntax for chaining a bunch of templates is simply:

$first($second)($third)($fourth); // and so on

Neat.

The more explicit (and strictly equivalent) form would be using append() or prepend() as follows:

$first->append($second)->append($third)->append($fourth);

Or…

$fourth->prepend($third)->prepend($second)->prepend($first);

ℹ️ Protip: Note that the previously seen Box::__invoke() is simply an alias of Box::append().

You have now seen how you can append (or prepend) Boxes together.

Notice however that the variables $first, $second, $third, and $fourth were objects of type Box which means they needed to be brought to life at some point — probably using the BoxFactory object created in the very beginning (which we kept calling $endobox in this document).

In other words the complete code would probably look something like this:

$first = $endobox('first');
$second = $endobox('second');
$third = $endobox('third');

echo $first($second)($third);

It turns out there is a way to avoid this kind of boilerplate code: You can directly pass the name of the template (a string) to the append() method instead of the Box object!

So, instead you could just write:

echo $endobox('first')('second')('third');

It looks trivial, but there is a lot going on here. The more verbose form would look as follows:

echo $endobox->create('first')->append('second')->append('third');

This is – in turn – equivalent to the following lines:

echo ($_ = $endobox->create('first'))
        ->append($endobox->create('second')->link($_))
        ->append($endobox->create('third')->link($_));

Notice that unlike before these (implicitly created) boxes are now all linked together automatically, meaning they share the same data.

The rule of thumb is: Boxes created from other boxes are linked by default.

Nesting

A fairly different approach (probably the template designer rather than the developer way) would be to define some sort of layout template instead:

layout.php
<html>
<head></head>
<body>
<header><?= $header ?></header>
<article><?= $article ?></article>
<footer><?= $footer ?></footer>

Then somewhere in controller land:

$layout = $endobox('layout');
$header = $endobox('header');   // header.html
$article = $endobox('article'); // article.php
$footer = $endobox('footer');   // footer.html

echo $layout->render([
    'header' => $header,
    'article' => $article->assign([ 'title' => "How to make Lasagna" ]),
    'footer' => $footer
]);

This should be fine, but we can get rid of some boilerplate code here: $header and $footer really don't need to be variables.

That's where nesting comes into play!

Use the $box() function to instantiate a template Box from inside another template:

layout.php
<html>
<head></head>
<body>
<header><?= $box('header') ?></header>
<article><?= $article ?></article>
<footer><?= $box('footer') ?></footer>

Then simply…

echo $endobox('layout')->render([
    'article' => $endobox('article')->assign([ 'title' => "How to make Lasagna" ])
]);

This is already much cleaner, but it gets even better: By using $box() to nest a template Box inside another these two boxes will be linked by default!

That allows us to condense this even further. Check it out:

layout.php
<html>
<head></head>
<body>
<header><?= $box('header') ?></header>
<article><?= $box('article') ?></article>
<footer><?= $box('footer') ?></footer>

All three templates are now nested using $box() and therefore linked to their parent (i.e., $layout).

This reduces our controller code to one line:

echo $endobox('layout')->render([ 'title' => "How to make Lasagna" ]);

Notice how we are assigning a title to the layout template even though the actual $title variable occurs in the nested article template.

ℹ️ Protip: The $box() function is also available as a method of Box objects (i.e., outside templates): You can instantiate new boxes with $box->create('template') where $box is some Box object that has already been created.

Functions

Functions are a cool and handy way of adding reusable functionality to your templates (e.g., filters, URL builders…).

Registering functions

You can register your own custom function (i.e., closure) by simply assigning it to a template Box just like data!

Here is a simple function $day() which returns the day of the week:

$calendar->day = function () { return date('l'); };

Inside your template file you can then use it in the same fashion as any variable:

<p>Today is <?= $day ?>.</p>

This would look like this (at least sometimes):

<p>Today is Tuesday.</p>

You can go even further and actually invoke the variable just like any function and actually pass some arguments along the way.

Below is a simple closure $a() that wraps and escapes some text in a hyperlink tag:

$profile->a = function ($text, $href) {
    return sprintf('<a href="%s">%s</a>',
            htmlspecialchars($href),
            htmlspecialchars($text));
};

Calling this function inside your template would look like this:

<p>Follow me on <?= $a("GitHub", "https://github.com/younishd") ?></p>

This would produce something like this:

<p>Follow me on <a href="https://github.com/younishd">GitHub</a></p>

Default functions

There are a couple of default helper functions that you can use out of the box (some of which you may have already seen):

Function Description Example
$box() or $b() Instantiate a Box from within another template. (See Nesting.) <article><?= $box('article') ?></article>
$markdown() or $m() Render some text as Markdown. Useful when the text is user input/stored in a database. <?= $markdown('This is some _crazy comment_!') ?>
$escape() or $e() Sanitize unsafe user input using htmlspecialchars(). (See Escaping.) <img src="portrait.jpg" alt="<?= $e($name) ?>">

Cloning

You can easily clone a template Box using the built-in clone keyword.

$sheep = $endobox('sheep');

$cloned = clone $sheep;

The cloned box will have the same content and data as the original one. However, chained or linked boxes are discarded.

License

endobox is open-sourced software licensed under the MIT license.

endobox's People

Contributors

younishd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

endobox's Issues

Box Render : Fatal error: Cannot use result of built-in function in write context in ...src\endobox\Box.php on line 140

Box Render method fatal error on php 7.2.6:

    /**
     * Render the box and everything attached to it then return the result.
     */
    public function render() : string
    {
        // assign data if any
        if (\func_num_args() > 0) {
            $this->assign(\func_get_args()[0]);
        }
    ...

However, this works if I remove the \ :

$this->assign(func_get_args()[0]);

or if I do:

$var = \func_get_args()[0];
$this->assign($var);

Meh

Method to link all chained boxes

Link all chained boxes.

Syntax 1:

$box('foo')('bar')->linkAll();

Syntax 2:

$box('foo')('bar')->link();

Syntax 3:

$box('foo')('bar')();

Default value for variable inside template

Add a short, native way to define a default value to be printed in lieu of a variable if it is undefined.

Currently (meh):

<title><?= isset($title) ? $title : "Default Title Here" ?></title>

Proposal (nice):

<title><?= $title("Default Title Here") ?></title>

Alternative (ugly):

<title><?= $default('title', "Default Title Here") ?></title>
<title><?= $d('title', "Default Title Here") ?></title>

It might not be trivial to make the nice syntax work, because if the variable $title is defined and happens to be an object it would trigger __invoke() instead of __toString().
Or: how can we distinguish between invoking a defined variable that is an object or defaulting an undefined variable?

Write DI friendly lib code

  • Get rid of the cluttering container in Factory.
  • Use abstract factory for Parsedown
  • Provide a facade with sensible default values

Clone is kind of useless

Shouldn't clone do a copy of the cloned box as well as everything that is linked to it and recreate the original box chain (without data)?

A--B--C--D

C' = clone C

A--B--C--D

A'--B'--C'--D'

Override magic box template type

We need an option to explicitly specify a template type instead of letting the magic box determine it by file extension.

Something like this:

$magic->append_template('foobar.xyz', endobox::MAGIC_TYPE_PLAINTEXT);
$magic->append_template('foobar.xyz', endobox::MAGIC_TYPE_PHP);
$magic->append_template('foobar.xyz', endobox::MAGIC_TYPE_MARKDOWN);
$magic->append_template('foobar.xyz', endobox::MAGIC_TYPE_MARKDOWNEXTRA);
$magic->append_template('foobar.xyz', endobox::MAGIC_TYPE_PHP_MARKDOWN);
$magic->append_template('foobar.xyz', endobox::MAGIC_TYPE_PHP_MARKDOWNEXTRA);

Set data via object properties

This can be done by overriding the magic functions:

  • __set
  • __get
  • __isset
  • __unset

We probably want to override them all to be consistent.

Global assign

Feature that lets you assign things globally. Maybe an assign method on BoxFactory level.

Should Box::__invoke(string) imply append()

Related to #35

Unclear if $foo('bar') should $foo->append($foo->create('bar')) in the same fashion as $foo($bar)if $bar is of type Box.

Also to be discussed if $foo->append('bar') should $foo->append($foo->create('bar')).

Bootstrap code

Factory should require its dependencies and some bootstrap code should inject them.

So no more

$e = new endobox\Factory('path/to/templates');

Instead

use \endobox\endobox;
$e = endobox::create('path/to/templates');

and…

class endobox
{
    public function __construct(Container $c, string $path) {…}
    public function create(string $path) {…}
}

Drawback: it literally breaks everything so it has to be 3.0

Define $listify() $li helper function

$listify = $li = function (
    array $items,
    string $item_format = '<li>%s</li>',
    string $list_format = '<ul>%s</ul>'
) {
    return \sprintf($list_format, \implode("\n", \array_map(function ($a) use ($item_format) {
        return \sprintf($item_format, \htmlspecialchars($a));
    }, $items)));
};
echo $li(["apple", "orange", "mango"]);
echo $listify(["apple", "orange", "mango"]);
echo $listify(["apple", "orange", "mango"], '<li class="fruit">%s<li>');
echo $listify(["apple", "orange", "mango"], '<li class="fruit">%s<li>', '<ol>%s</ol>');
<ul><li>apple</li>
<li>orange</li>
<li>mango</li></ul>

<ul><li>apple</li>
<li>orange</li>
<li>mango</li></ul>

<ul><li class="fruit">apple<li>
<li class="fruit">orange<li>
<li class="fruit">mango<li></ul>

<ol><li class="fruit">apple<li>
<li class="fruit">orange<li>
<li class="fruit">mango<li></ol>

Markdown Extra is confusing

Maybe the file extension should be .html.md for Markdown Extra and .html.md.php for dynamic Markdown Extra.

Explicit template type argument

Pass template type as optional argument to make() to override the detected type by file extension.

$endo = new endobox\Factory('bla/bla');

// something like this
$endo('mytemplate', $endo::MARKDOWN);

// or maybe
$endo('mytemplate', 'markdown');

Passing functions to Box

Allow passing functions to Box just like data.

The idea is something like this:

function assign(a)
{
    foreach(a as key => value) {
        if (value is callable) {
            assign(key, new class { func = value; function __toString() { return (string)value(); }
        } else {
            // ...
        }
    }
}
$box->assign(['time' => function(){ return time(); }])->render();
<p>The unix time is: <?= $time ?></p>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.