yonld / purephp
Pure is a PHP template engine inspired by ReactJS.
Requires
- php: >=8.1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.84
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-23 02:50:02 UTC
README
Purephp is a PHP templating engine inspired by ReactJS functional components.
π Documentation
- English: https://yonld.github.io/purephp/
- δΈζ: https://yonld.github.io/purephp/zh/
Why use Purephp?
To enjoy pure PHP programming.
In traditional approaches, mixing HTML code, PHP code, and other template syntax in the view layer can be frustrating for developers.
However, with Purephp:
- Everything is 100% native PHP code.
- Encapsulate components to eliminate repetitive HTML code.
- The syntax closely resembles HTML.
- Compile data-free shapes into flat renderers, for performance on par with compiled template engines.
Install
composer require yonld/purephp
Quick start
A component is one file: a call function that returns a Pure\Component\Call,
plus the template it renders and the typed props of its prepare() hook,
registered lazily so pure compile can precompile it:
<?php // components/Card.cmp.php use Pure\Component\Call; use Pure\Core\Slot; use function Pure\Component\{component, register}; use function Pure\HTML\{div, h2, p}; function Card(mixed ...$children): Call { return component(__FUNCTION__, ...$children); } register(Card(...), factory: static fn () => div( h2(Slot::value('title')), p(Slot::value('content')) )->class('card'), prepare: static function (string $title, string $content): array { return ['title' => $title, 'content' => $content]; } ); echo Card()->title('Card Title')->content('Card Content');
Children are passed to the call, props are set as fluent setters, and the result nests wherever a tag does:
<?php use function Pure\Component\{component, register}; use function Pure\HTML\{button, div, h2, li, ul}; // a unit with a children slot, its props as plain bindings function Card(mixed ...$children): Pure\Component\Call { return component(__FUNCTION__, ...$children); } register(Card(...), static fn () => div( Slot::raw('children'), h2(Slot::value('type'))->class('card-title'), ul(Slot::each('features', li(Slot::value('value')))), button(Slot::value('text'))->class(Slot::value('class')) )->class('card')); echo div( Card(h2('Pro')) ->type('Free') ->features([['value' => '10 users'], ['value' => '2 GB']]) ->text('Sign up for free') ->class('btn btn-lg') );
A call resolves the registered binder, artifacts, cache and errors, and
pure check validates the fluent props against the template's slots: a #[Prop]
declaration on a prepare() parameter
(slot, item, required, deprecated) is verified against the signature and
the template, #[Trusted] marks a prop that carries markup (it must bind a raw
slot, and the development guard warns when a call passes a value that is not
Markup), and #[Binds] declares the keys of a hook whose returned array
cannot be read.
The above code will output:
<div class="card"><h2>Card Title</h2><p>Card Content</p></div>
register(Card(...)) derives the name and file from the call function and only
stores the factory; a request that finds a fresh artifact
never builds the template. Run vendor/bin/pure compile components to
precompile, and pure compile --list to see the units found. A call renders the
fragment; a full document's header is the caller's to prepend
($root->documentHeader(), or a literal <!DOCTYPE html> / <?xml version="1.0"?>).
Under standard PHP-FPM every request starts fresh, so enable
Compile::cachePath() (or precompile with pure compile) to load generated
renderers instead of rebuilding them; long-running workers keep them in memory.
While developing, Compile::guard(true) (or PURE_COMPILE_GUARD=1) reports
shapes rebuilt per request, bindings the template never reads (with a
did you mean), and attribute names one edit away from a standard one.
See the compiled rendering guide
for caching, conditionals and mixed lists.
Snippets and debugging
For small fragments, one-off snippets and debugging you can build a regular tag tree and render it immediately:
<?php use function Pure\HTML\a; use function Pure\HTML\div; div( 'Hello ', a('PHP')->href('https://www.php.net') )->class('container')->style('background: #fff;')->data_key('primary')->print();
The above code will output:
<div class="container" style="background: #fff;" data-key="primary">Hello <a href="https://www.php.net">PHP</a></div>
The tag tree's render() and print() are the debug/snippet outlet. Production
pages should compile shapes, because a shape is compiled and static markup is
escaped once instead of on every render.
Compiled components
Inside a template, nested shapes use Slot::child(), lists use Slot::each(),
and conditionals use Slot::if(). Everything else is plain PHP.
A parent takes its children's markup as an ordinary value and passes it through
a raw slot: div(Slot::raw('body')) bound as component('Page')->body(Card(...)).
Pre-rendered markup passed into a raw slot needs no (string) cast, and an array of them is concatenated in order.
For production, pure compile precompiles every *.cmp.php unit (and every
lower-level *.shape.php template) into a *.pure.php artifact that returns a
Renderer without building the shape tree:
vendor/bin/pure compile components # *.pure.php: the compiled renderer vendor/bin/pure compile --plain components # + *.plain.php: a dependency-free view vendor/bin/pure compile --list components # name -> file (component|page) vendor/bin/pure check components # slots vs. bindings vs. parameters
use Pure\Core\HTML; $page = require __DIR__ . '/page.pure.php'; echo $page->render(['title' => 'Card Title']); // the view body echo HTML::DOCUMENT_HEADER . $page->render($data); // a whole document
A *.plain.php view is markup and native PHP only β load it by extracting the
data into locals and nothing of purephp is needed at render time:
ob_start(); extract($data, EXTR_SKIP); require __DIR__ . '/views/index.plain.php'; $html = (string)ob_get_clean();
pure compile --check reports stale or missing artifacts for CI
(--check --plain covers both flavors), and pure check validates the
component contract β the slots a template reads against the bindings and typed
props of its unit (prepare() hook or typed call function). See
Compiled Components
for the artifact contract, the freshness rules and the plain-view caveats.
Examples
examples/bootstrap is a small MVC setup with three pages behind one router:
controllers stay thin, app/dao/ reads the records, app/services/ turns them
into the props of one component, and each component fetches its own slice there
β the page function carries no page data. views/features.cmp.php and
views/pricing.cmp.php are component units that compile
into a strict artifact (*.pure.php, loaded by the unit's binder) and a
dependency-free view (*.plain.php, required by the example's plain()
helper); the two controllers of a page share the bindings its fluent component
calls produce (featuresBindings() / pricingBindings()), which the plain
loader renders to strings before the view loads. The cover page
is static markup through the string renderer (views/cover.php), so it has
neither variant. Routes:
/cover the static cover page
/plain/features the plain features view
/plain/pricing the plain pricing view
/pure/features the compiled features artifact
/pure/pricing the compiled pricing artifact
vendor/bin/pure compile --plain examples/bootstrap
php -S localhost:8000 -t examples/bootstrap/public \
examples/bootstrap/public/index.php
# http://localhost:8000/cover, and either flavor of each page:
# /pure/features /plain/features /pure/pricing /plain/pricing
A request that matches nothing gets a 404 that lists every route.
event-counter and xml follow the same layout β a views/<page>.cmp.php
unit plus a public/index.php router for /, /pure and /plain β and xml
adds write.php, the CLI entry that writes example.xml.
Every artifact is byte-identical to its template, and every plain view to its
artifact, preceded by the document header only when the view's root heads a
document (<html> or an XML tree; a fragment view starts with its markup). See
the examples.
Development
composer quality # syntax-check + cs-check + phpstan + tests: run this before committing composer test # PHPUnit only composer cs-fix # apply the PHP CS Fixer rules composer phpstan # static analysis (level 9) composer bench # benchmarks
The documentation site lives in site/ (VitePress):
npm run docs:dev to preview locally, npm run docs:build to build it
(this also regenerates llms.txt and verifies its links).
License
MIT Β© YonLD