Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Dtmpl Laravel Package

coolms/dtmpl

A sandboxed template language for non-PHP authors, with lexer, parser, AST and runtime, plus extensible widgets, filters and loaders. No PHP, filesystem or arbitrary calls—framework-optional, with a Symfony bundle available.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Verify the PHP constraint before anything else. The package requires PHP ^8.5. Run composer why-not coolms/dtmpl (or just try composer require coolms/dtmpl) — on a typical Laravel app pinned to 8.2–8.4 this will refuse to install. If you cannot move the runtime, stop here; there is no supported fallback.

php -v                     # must be >= 8.5
composer require coolms/dtmpl

2. Read in this order:

Where Why
README.md The language in 30 lines: {if:}, {loop:}, {var: … filter:arg}, {endif}, {endloop}
docs/ Syntax reference — block names, filter call syntax, whitespace handling, escaping rules
CHANGELOG.md Confirms how young the API is; expect churn
src/ — DtmplEngine, the Loader namespace, Widget and filter contracts The four seams you will actually wire up: widgets, filters, constants, loaders

3. First use case — a designer-editable "content page". Do not start by migrating Blade views. Start with the one thing DTMPL is genuinely better at: a template an editor can change without a deploy.

use CoolMS\Dtmpl\DtmplEngine;

// The engine is framework-optional: a loader + optional cache pool.
// Exact constructor signature lives in src/ and docs/ — check there, not in blog posts.
$engine = new DtmplEngine(/* loader, cache pool */);

$html = $engine->render('page.dtmpl', [
    'page'  => ['title' => 'Hello', 'isPublished' => true],
    'posts' => $postDtos,
]);

Bind it singleton in a service provider, point the loader at resource_path('dtmpl') (or a storage disk for CMS-authored templates), and return response($html) from the controller. That is the whole first increment — no widgets, no custom filters, no cache.

Implementation Patterns

Container binding + config file. Register a config/dtmpl.php with paths, cache_store, widgets, filters, constants, and a DtmplEngine singleton built from it. Everything the host supplies is a constructor/mutator argument, so the provider is the single wiring point; nothing else in the app should new DtmplEngine.

Context as arrays/DTOs, never raw Eloquent models. The engine resolves page.title through symfony/property-access. Models mostly work via ArrayAccess and magic attributes, but you inherit lazy loading inside {loop:} and hidden-attribute surprises. Build an explicit array or readonly DTO in a small "view model" layer: it documents exactly which fields template authors may use, and it makes the sandbox boundary real.

The widget seam → your Blade/components registry. This is the seam worth investing in. Map widget names to small classes with a render(array $props): string shape, and implement them by delegating to Laravel:

// config/dtmpl.php
'widgets' => [
    'button'   => App\Dtmpl\Widgets\ButtonWidget::class,   // returns view('components.button', $props)->render()
    'card'     => App\Dtmpl\Widgets\CardWidget::class,
],

Template authors then write {widget:button label="Save" variant="primary"} and get your design system with zero PHP exposure. Keep widget props scalar-only and validate them; never pass a prop straight into {!! !!} equivalent output.

Filters as invokable classes. Add project-specific filters (currency, markdown, initials) as single-purpose classes in App\Dtmpl\Filters and register them once. Resist the urge to reach into the engine's internals — filters are the intended extension point for formatting, widgets for markup.

Loader strategy split. Filesystem loader for developer-owned templates in resources/dtmpl/; a database/disk loader for tenant- or CMS-authored templates keyed by tenant:template. Prefer one loader per origin over one loader with magic paths — it keeps cache keys and permission checks honest.

Cache the parsed AST. Lexing and parsing per request is the obvious cost. Feed the engine a PSR-6 pool and key compiled templates on sha1($source) rather than path+mtime so DB-sourced templates invalidate correctly. Ship an Artisan command (dtmpl:clear) — php artisan view:clear will not touch this cache.

Preview and validate before save. Expose a "preview" endpoint that parses and renders against sample context, returning errors with line numbers. This is the killer feature for the designer-facing pitch: authors get feedback in the CMS instead of a 500 on the live site.

Test with golden files. Render each real template against a fixture context and assert against a checked-in HTML snapshot. Because the language is sandboxed and deterministic, snapshot tests catch both engine upgrades and accidental template edits.

Blade interop: if you only need a fragment, a Blade::directive('dtmpl', ...) that renders a DTMPL file to a string and injects it is a low-risk way to adopt the engine incrementally inside existing Blade layouts.

Gotchas and Tips

  • PHP 8.5 is a hard gate. This alone blocks most Laravel apps today. Check your CI matrix, Docker base image (php:8.5-cli), and hosting before you plan anything around this package.
  • PSR-6 vs PSR-16 mismatch. The package depends on psr/cache (PSR-6 CacheItemPoolInterface). Laravel's Cache repository implements PSR-16, not PSR-6. Wrap a Laravel store with symfony/cache's PSR-6 adapter (or cache/adapters); do not expect Cache::store() to be injectable as-is.
  • Symfony translation contracts ≠ Laravel's translator. The property/translation contracts expect trans($id, array $parameters) with %placeholder% syntax; Laravel uses __('key', ['name' => …]) with :placeholder. Write a ~30-line adapter class implementing the contract over app('translator') and register it — otherwise translation filters silently mis-substitute or throw.
  • Confirm the escaping story before trusting template authors. Verify in docs/ whether {var:…} auto-escapes or whether you must apply an escape/raw filter. If unescaped output is available and templates can be edited by non-developers, that is a stored-XSS path. Default to escaping and make raw output an explicit, reviewed filter.
  • "Sandboxed" ≠ safe for untrusted input. The sandbox prevents PHP execution, not resource exhaustion. A {loop:posts:post} over a 10k-row collection, or deeply nested loops, will still melt a request. Cap loop sizes in the loader/context, page your collections, and put a time/memory budget around render calls.
  • N+1 inside loops. {loop:posts:post}{var:post.author.name}{endloop} will lazy-load per iteration if you pass models. Eager-load in the context builder or pass DTOs — the template has no way to fix this for you.
  • Debugging parse errors. Catch the parser exception at the boundary and rethrow with the template name, line and column; log the offending source line. A dtmpl:lint command that walks every registered template and parses it is worth an hour on day one and pays for itself in CI.
  • Cache invalidation is your job. No view:clear, no opcache invalidation, no automatic mtime checks unless you build them. Version cache keys and add a clear command to your deploy script.
  • Extension points: use only the four seams. Widgets, filters, constants, loaders. If a template needs logic that does not fit one of those, the answer is "prepare it in the context," not "add a {php:} block" — the moment PHP leaks in, you have lost the sandbox guarantee and the reason to use this over Blade.
  • Maturity risk: treat it as a vendored dependency. Zero stars, zero dependents, single maintainer, docs present but thin. Wrap DtmplEngine behind your own TemplateRenderer interface so swapping to Twig/Liquid/Blade later is a provider change rather than a codebase-wide edit. Pin the exact version in composer.json and read CHANGELOG.md on every bump.
  • Watch for whitespace and encoding quirks. Designer-facing languages usually preserve whitespace literally; check the docs for trim/control syntax, and add an editor-side formatter note to your authoring guide so output does not drift between environments.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels