coroq/html

HTML escaping for PHP templates

Maintainers

Package info

github.com/ozami/coroq-html

pkg:composer/coroq/html

Transparency log

Statistics

Installs: 280

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-08-06 13:06 UTC

This package is auto-updated.

Last update: 2026-08-06 23:57:50 UTC


README

HTML escaping for PHP templates.

Install

composer require coroq/html

Quickstart

use function Coroq\Html\h;

<p><?= h('cookies & cream') ?></p>
<!-- Output: <p>cookies &amp; cream</p> -->

Use h() when echoing - it's safe everywhere.

How it works

h() uses deferred escaping - it doesn't escape immediately. Instead, it wraps the value in an Html object.

Html objects are like DOM elements - they have a tag, attributes, and children. Children can be strings or other Html objects:

use Coroq\Html\Html;

$label = (new Html())->tag('strong')->append('Home');
$link = (new Html())
  ->tag('a')
  ->attr('href', '/home')
  ->addClass('nav-link')
  ->append($label);  // Html as child

echo $link;
// Output: <a href="/home" class="nav-link"><strong>Home</strong></a>

h() wraps the value in an Html object without a tag, just adding the value as a child:

$wrapped = h('text');  // Returns Html object with no tag, 'text' as child
echo $wrapped;         // Output: text (escaped at render time)

So you can call h() without worrying about double-escaping:

echo h('&');           // wraps '&' → &amp;
echo h(h('&'));        // already an Html → returned unchanged → &amp;

$link = (new Html())->tag('a')->attr('href', '/x');
echo h($link);         // already an Html → returned unchanged → <a href="/x"></a>

At render time, string children are escaped, Html children render themselves.

Where h() goes

<p><?= h($name) ?></p>                            // yes - escape as you echo
<a href="<?= h($url) ?>">                         // yes - h() escapes quotes too

<?= h(el('a', ['href' => $url])) ?>               // yes - attributes are escaped for you
<?= h(el('a', ['href' => h($url)])) ?>            // no  - throws; pass the plain value

Building elements

el($tag, $attrs, $children) creates any element. Attribute values that are null or false are left out:

use function Coroq\Html\{h, el};

echo h(el('article', ['id' => 'post-1'], [
  el('h2', [], $title),
  el('p', ['class' => 'lead'], $summary),
]));

There are no per-tag helpers. A mistyped tag shows up in the output, so a shorter name buys little; if a project writes the same element often, a one-line function of its own reads better than anything this library could guess:

$card = fn($body) => el('div', ['class' => 'card'], $body);

An element can also be built up step by step, which is how you adjust one that was handed to you:

echo h(el('div')->addClass('card')->append($content));

Conditional markup

use function Coroq\Html\{h, el};

<!-- Link if URL exists, otherwise plain text -->
<span><?= h($user->url ? el('a', ['href' => $user->url], $user->name) : $user->name) ?></span>

<!-- Highlight errors -->
<td><?= h($hasError ? el('span', ['class' => 'error'], $value) : $value) ?></td>

A null or false value removes an attribute, so a condition can go straight into the value:

<?= h(el('input', ['type' => 'text', 'name' => 'detail', 'disabled' => !$editable])) ?>
<!-- $editable = false: <input type="text" name="detail" disabled> -->
<!-- $editable = true:  <input type="text" name="detail"> -->

Repeating markup

echo h(el('ul', [], array_map(fn($item) => el('li', [], $item), $items)));

append() adds each item of an iterable as a child, so repeating markup is plain array_map. The list must be flat - a nested one throws, since nesting an array has no meaning in the output.

Reusing markup

An Html object is mutable, so a variable holding one is not safe to reuse - every use would share and modify the same element:

$cell = el('td', ['class' => 'num']);                        // don't
$cell = fn($v) => el('td', ['class' => 'num'], $v);          // do

To reuse a piece of markup, write a function that returns a fresh element.

Trusted HTML

use function Coroq\Html\{h, noEscape};

echo h(noEscape('<strong>bold</strong>'));

Never use noEscape() with user input.

HTML templates

Html objects build markup through method calls. When markup is easier to write as actual HTML, use a template: HTML with embedded PHP — classic PHP style — that renders as an HtmlInterface object.

Extend HtmlTemplate:

use Coroq\Html\HtmlTemplate;
use function Coroq\Html\h;

final class ProductCard extends HtmlTemplate
{
  public function __construct(
    private Product $product,
    private int $priceWithTax,
  ) {
  }

  protected function render(): void
  {
    ?>
    <div class="card">
      <h5><?= h($this->product->name) ?></h5>
      <p>JPY <?= h(number_format($this->priceWithTax)) ?></p>
    </div>
    <?php
  }
}

The constructor takes the data the template uses; render() echoes the HTML, escaping with h() as usual.

Echo it, or embed it anywhere trusted HTML goes:

echo new ProductCard($product, 5500);
<td><?= h(new ProductCard($product, 5500)) ?></td>
echo h(el('div', ['class' => 'cart'], new ProductCard($product, 5500)));

A template's output is not escaped — like noEscape(), but structural: the template escapes its own data inside render().

Custom HTML values

HtmlInterface marks a value as already-safe HTML — anything implementing it is rendered as-is. Html, NoEscape and HtmlTemplate all do.

Extend HtmlTemplate for hand-written markup. Implement HtmlInterface directly to build the string yourself:

use Coroq\Html\HtmlInterface;
use function Coroq\Html\h;

final class Badge implements HtmlInterface
{
  public function __construct(private string $label, private string $color) {}

  public function __toString(): string
  {
    return '<span class="badge badge-' . h($this->color) . '">' . h($this->label) . '</span>';
  }
}

Escape interpolated values with h(). Concatenation calls __toString(), so no cast is needed.

Requirements

  • PHP 8.0 or higher
  • No dependencies

Scope

Does: HTML escaping, element generation, class-based HTML templates Doesn't: Template engines, HTML parsing, DOM manipulation

Reference

Core functions

  • h($content) - escape content, safe to call multiple times
  • noEscape($html) - mark HTML as safe (dangerous with user input)

HtmlTemplate

  • extend it, take the template's data as constructor parameters, and echo the HTML in render(): void
  • echo the object or cast it to string to render

Elements

  • el($tag, $attrs, $children) - any element; void tags such as br and img get no closing tag

Fluent methods

  • ->tag($name) - set the tag name
  • ->append($content) - add content; each item of an iterable becomes a child, and null adds nothing
  • ->prepend($content) - the same, at the beginning
  • ->children($iterable) - replace all children
  • ->attr($name, $value) - set attribute (a null or false value removes it)
  • ->attrs($array) - set several attributes at once
  • ->addClass($classes) - add CSS classes; an empty or null name adds nothing
  • ->id($id) - set id; null removes
  • ->data($name, $value) - set a data- attribute
  • ->style($name, $value), ->styles($array) - add CSS properties
  • ->title($value), ->alt($value), ->role($value) - set those attributes; null removes
  • ->autocomplete($value), ->placeholder($value) - the same
  • ->ariaLabel($value), ->ariaHidden($bool) - aria-label and aria-hidden; other aria-* go through attr()
  • ->close($mode) - force Html::CLOSE, Html::NO_CLOSE or Html::SELF_CLOSE
  • ->wrap($wrapper) - put this element inside another and return the wrapper

An Html is write-only. Nothing comes back out of it except the rendered, escaped HTML, so a value you read from one can never be mistaken for a value the library has already made safe. Do any inspecting or rewriting of your content before you put it in.