mazechazer/soundless

Functional HTML templating in PHP

v0.1.0 2017-04-15 18:38 UTC

This package is auto-updated.

Last update: 2024-05-05 01:56:45 UTC


README

You've got a whole language at your fingertips. Just use it!

Soundless is a functional templating library for HTML, inspired by elm-lang/html.

  • Plain PHP
  • No echo
$ composer require mazechazer/soundless

The idea is to build up an object representation of the HTML using functions and vararg lists.

<?php
>>> function hello(string $name) {
...     return
...         html()(
...             body()(
...                 p()(text("Hello $name!"))));
... }
>>> renderToString(hello('World'))
=> "<!DOCTYPE html><html><body><p>Hello World!</p></body></html>"

Soundless gives you

  • Automatic escaping
  • Easy source code formatting, no fiddling with HTML syntax/whitespace
  • Seamless and type safe integration into other parts of your app
  • All PHP features for composition, abstraction, type safety...

Possible drawbacks

  • The syntax is a little clumsy
  • HTML related tools (i.e. autocompletion/inspections of your IDE) won't work
  • Performance should be ok, but that hasn't been tested

With Soundless you can make your templates much more modular by breaking them down into resusable functions.

In lack of real documentation, some tinkering with Soundless using PsySH:

$ mkdir soundless-tinkering
$ cd soundless-tinkering
$ composer require mazechazer/soundless
$ psysh
>>> include 'vendor/autoload.php'
>>> use function Soundless\{renderToString,div,p,text}
>>> use function Soundless\Attributes\{id,classList}
>>>
>>> # Let's create a simple text node
>>> $text = text('Hello Soundless!')
>>> renderToString($text)
=> "Hello Soundless!"
>>>
>>> # Text value contents are escaped automatically
>>> renderToString(text('<script>alert("XSS!");</script>'))
=> "&lt;script&gt;alert(&quot;XSS!&quot;);&lt;/script&gt;"
>>>
>>> # Let's build a more complex node, a paragraph
>>> $paragraph = p(id('foobar'))(text('Foobar!'))
>>> renderToString($paragraph)
=> "<p id="foobar">Foobar!</p>"
>>>
>>> # Attribute values are escaped, too
>>> renderToString(p(id('"><script>alert("XSS!");</script>'))())
=> "<p id="&quot;&gt;&lt;script&gt;alert(&quot;XSS!&quot;);&lt;/script&gt;"></p>"
>>>
>>> # Nest some nodes
>>> $box = div(
        id('message')
    )(
        p()(text('Important message!'))
    )
>>> renderToString($box)
=> "<div id="message"><p>Important message!</p></div>"
>>>
>>> # Set a class conditionally
>>> renderToString(p(classList(['highlight' => true]))())
=> "<p class="highlight"></p>"
>>> renderToString(p(classList(['highlight' => false]))())
=> "<p></p>"

And, to scare you a little, a more complex example using template composition and dependency injection:

<?php
class BaseTemplate
{
    /**
     * @param string $title
     * @param Node $content
     * @return Node
     */
    public function render(string $title, Node $content): Node
    {
        return
            html()(
                head()(title()(text($title))),
                body()(...$content));
    }
}

class SearchResultsTemplate
{
    /** @var BaseTemplate */
    private $baseTemplate;
    /** @var PaginationTemplate */
    private $paginationTemplate;
    /** @var Translator */
    private $translator;

    public function __construct(
        BaseTemplate $baseTemplate,
        PaginationTemplate $paginationTemplate,
        Translator $translator
    ) {
        $this->baseTemplate = $baseTemplate;
        $this->paginationTemplate = $paginationTemplate;
        $this->translator = $translator;
    }

    /**
     * @param string $term
     * @param int $currentPage
     * @param int $numberOfPages
     * @param SearchResult[] $searchResults
     * @return Node
     */
    public function render(
        string $term,
        int $currentPage,
        int $numberOfPages,
        $searchResults
    ): Node
    {
        $headerMessage =
            $this->translator->translate('Search Results for “%s”', $term);

        $pagesMessage =
            $this->translator->translate(
                'Page %1$s of %2$s', $currentPage, $numberOfPages);

        $titleMessage = $this->translator->translate('Search Results');

        $content =
            main()(
                h1()(text($headerMessage)),
                p()(text($pagesMessage)),
                ul()(...map($searchResults, [$this, 'searchResult'])),
                $this->paginationTemplate->render(
                    '/search/'.urlencode($term).'?page=%s',
                    $currentPage,
                    $numberOfPages));

        return $this->baseTemplate->render($titleMessage , $content);
    }

    /**
     * @param SearchResult $searchResult
     * @return Node
     */
    public function searchResult(SearchResult $searchResult)
    {
        return
            li(
                classList([
                    'highlight' => $searchResult->getHighlight()
                ])
            )(
                a(href($searchResult->url))(
                    p(class_('search-result-title'))(
                        text($searchResult->getTitle())),
                    p(class_('search-result-snippet'))(
                        text($searchResult->getSnippet()))));
    }
}