concept-labs / phtmal
(C)oncept-Labs Abstract HTML
Installs: 9
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/concept-labs/phtmal
Requires
- php: >=8.2
README
Tiny, fluent HTML node tree with a minimal CSS-like selector engine — built to be lightweight, readable, and extensible. Designed for future integration with a layout package (name TBD) and for use in server-side rendering scenarios.
Highlights
- Fluent builder via
__call()($ul->li('A')->end()), with optional subtree callbacks- Pretty vs minified rendering
- Safe text nodes and explicit
raw()nodes- Normalized attributes (boolean attrs supported)
- Minimal CSS-like querying (tag,
*,#id,.class,[attr], combinators>+~,:first-child,:last-child,:nth-child)- Extensible: subclasses can override behavior and constants; interfaces define the contract
- NEW: Parse raw HTML into a Phtmal tree via
HtmlParserInterface+DomHtmlParser
Installation
Once published to Packagist:
composer require concept-labs/phtmal
For local/VCS use meanwhile:
Path repository (local dev):
{
  "repositories": [
    { "type": "path", "url": "../phtmal" }
  ],
  "require": {
    "concept-labs/phtmal": "*"
  }
}
VCS repository (GitHub):
{
  "repositories": [
    { "type": "vcs", "url": "https://github.com/Concept-Labs/phtmal" }
  ],
  "require": {
    "concept-labs/phtmal": "dev-main"
  }
}
Quick start
use Concept\Phtmal\Phtmal; $html = (new Phtmal('ul')) ->li(['class' => 'item'], function (Phtmal $li) { $li->span('A'); }) ->li('B')->end() ->top(); echo $html->render(); // pretty echo (string)$html; // minified
Attributes & boolean attributes:
$btn = (new Phtmal('button', 'Save')) ->class('btn', 'btn-primary') ->attr('disabled', ['disabled']); // short boolean form → <button disabled>…</button>
Text vs raw HTML:
$div = (new Phtmal('div'))->text('Safe <b>text</b>'); // escaped $div->raw('<b>UNSAFE</b>'); // unescaped (use with care)
Querying:
$items = $html->query('li.item:first-child, li.item:last-child'); $second = $html->queryOne('#main > .card:nth-child(2)');
HTML parsing (NEW)
You can parse raw HTML (documents or fragments) into a Phtmal tree using the parser interface.
Interfaces
- HtmlParserInterface— contract:- parseDocument(string $html, array $options = []): PhtmalNodeInterface
- parseFragment(string $html, string $containerTag = 'div', array $options = []): PhtmalNodeInterface
 
- DomHtmlParser— DOMDocument-based implementation (tolerant to malformed HTML).
Usage
Parse a full document:
use Concept\Phtmal\DomHtmlParser; $parser = new DomHtmlParser(); $root = $parser->parseDocument('<!doctype html><html><body><div id="x">t</div></body></html>'); // $root is the <html> node echo $root->render(); // pretty echo (string)$root; // minified
Parse a fragment (no implied <html>/<body>):
$parser = new DomHtmlParser(); $list = $parser->parseFragment('<li>A</li><li class="x">B</li>', 'ul'); echo (string)$list; // <ul><li>A</li><li class="x">B</li></ul>
Scripts/styles are imported as RAW nodes (not escaped):
$parser = new DomHtmlParser(); $div = $parser->parseFragment('<script>if (a < b) { alert("x"); }</script>', 'div'); echo (string)$div; // <div><script>if (a < b) { alert("x"); }</script></div>
Custom factory (use your subclass of Phtmal):
class MyNode extends Concept\Phtmal\Phtmal {} $parser = new DomHtmlParser(fn(string $tag, ?string $text, array $attr) => new MyNode($tag, $text, $attr)); $tree = $parser->parseFragment('<span>Hello</span>', 'div');
Options
parseDocument() and parseFragment() accept the same $options array:
| Option | Type | Default | Description | 
|---|---|---|---|
| dropComments | bool | true | Drop HTML comments. | 
| preserveWhitespace | bool | false | Keep whitespace-only text nodes outside <pre>/<textarea>. | 
| preservePreWhitespace | bool | true | Preserve whitespace in <pre>/<textarea>. | 
| encoding | string | 'UTF-8' | Input encoding hint for DOMDocument. | 
| rawTextTags | string[] | ['script','style'] | Treat content of these tags as RAW (unescaped). | 
Interfaces (core)
The library is interface-first. Documentation primarily lives on interfaces; implementations use {@inheritDoc}.
- PhtmalNodeInterface— node contract (fluent API, rendering, navigation, query integration).
- SelectorInterface— static querying:- select(PhtmalNodeInterface $root, string $selector): array.
- HtmlParserInterface— parse raw HTML into a Phtmal tree.
Key guarantees:
- Implementations escape text on render (except explicit #rawnodes).
- Attributes are normalized to lists of strings, enabling predictable rendering and boolean-shortcuts.
- Children order is stable.
Core API (from PhtmalNodeInterface)
// Builder & structure __call(string $tag, array $args): PhtmalNodeInterface end(): PhtmalNodeInterface top(): PhtmalNodeInterface append(PhtmalNodeInterface|string $nodeOrText): static raw(string $html): static // Content & attributes text(?string $text): static attr(string $name, string|array|null $value = null): static id(string $id): static class(string ...$class): static data(string $key, string $value): static aria(string $key, string $value): static // Navigation & mutation parent(): ?PhtmalNodeInterface firstChild(): ?PhtmalNodeInterface nextSibling(): ?PhtmalNodeInterface cloneDeep(): PhtmalNodeInterface detach(): static replaceWith(PhtmalNodeInterface $node): PhtmalNodeInterface // Rendering render(bool $minify = false, int $indentLevel = 0): string // Querying query(string $selector): array queryOne(string $selector): ?PhtmalNodeInterface // Meta getTag(): string
Notes:
- __call('li', [...])supports either- (text, attrs)or- (attrs, callback)— if a callback is the last argument, the method returns the parent (auto-jump back). Otherwise, it returns the new child (you can- ->end()manually).
- Boolean attributes are rendered in short form if normalized as ['disabled' => ['disabled']].
Extensibility recommendations
- Overridable constants (protected): VOID_ELEMENTS,INDENT,NL.
- Overridable hooks (protected): newNode(),escape(),renderAttributes(),_childrenRef().
- Open state (protected): parent,children,tag,attributes,text.
Testing & QA
Install dev tools:
composer require --dev phpunit/phpunit:^10 phpstan/phpstan:^1.11
Run tests and static analysis:
vendor/bin/phpunit vendor/bin/phpstan analyse
License
MIT