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

Twital Laravel Package

goetas/twital

Twital is a PHP template engine that adds Angular-style HTML syntax to Twig. Write cleaner, more expressive templates with custom tags and attributes, then compile them to standard Twig for familiar, fast rendering in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Twital is not a Twig extension in the Twig\Extension\ExtensionInterface sense — it is a template-to-template compiler. It parses HTML-flavoured source containing t:* attributes and emits plain Twig source, which you then hand to a normal Twig\Environment to render. Everything about day-to-day use follows from that one fact: you own two stages (compile → render), not one.

Minimal wiring:

composer require goetas/twital
use Twital\Engine;
use Twig\Environment;
use Twig\Loader\ArrayLoader;

$engine = new Engine();
$engine->addExtension(new Twital\Extension\CoreExtension());   // t:if, t:for, t:text, ...
$engine->addExtension(new Twital\Extension\TwigExtension());   // interop with native {% ... %}

$twigSource = $engine->compile($twitalSource);          // string in, string out
$twig = new Environment(new ArrayLoader(['t.twig' => $twigSource]));
echo $twig->render('t.twig', ['user' => $user]);

Where to look first, in this order:

  1. Twital/Extension/CoreExtension.php — this file is the feature list. Every t:* attribute is a visitor registered here (t:if, t:for, t:text, t:attributes, t:block, t:extends, t:include, t:macro, t:set, t:if/t:elseif/t:else). Read it before reading any docs.
  2. tests/ — the fixture format is usually “input Twital file → expected Twig file”, which doubles as your syntax reference.
  3. Twital\Compiler / visitor pipeline — only when you need a custom attribute.

First realistic use case in a Laravel app: you have a designer-authored, Vue/Alpine-style marketing or email template and you want t:for/t:if/:attr sugar instead of Twig’s {% %} soup. Convert one template, compile it, diff the output, render it — that is your whole pilot.

Implementation Patterns

Compile-then-render as a cached artefact (the pattern that survives production).

Do not compile on every request. Treat Twital like Blade: compile to a .twig file under storage/framework/views, key on source path + mtime (or a hash of the source), and let Twig load the compiled file.

$cachePath = storage_path("framework/views/twital/{$hash}.twig");

if (! is_file($cachePath) || filemtime($cachePath) < filemtime($sourcePath)) {
    file_put_contents($cachePath, $engine->compile(file_get_contents($sourcePath)));
}

return $twig->render($cachePath, $data);

This gives you correct invalidation on deploy (php artisan view:clear can be taught to nuke the Twital directory too) and makes stack traces point at a file you can actually open.

Integration with rcrowe/twigbridge via a loader decorator.

The cleanest Laravel integration is not a custom view engine. Twital’s output is Twig, so wrap the Twig loader: intercept getSourceContext(), compile, return a Twig\Source with the compiled code. TwigBridge then keeps handling everything else (extensions, escaping, caching, view() facades).

class TwitalLoader implements LoaderInterface
{
    public function __construct(private LoaderInterface $inner, private Engine $engine) {}

    public function getSourceContext(string $name): Source
    {
        $src = $this->inner->getSourceContext($name);
        return new Source($this->engine->compile($src->getCode()), $src->getName(), $src->getPath());
    }
    // isFresh() must forward to the *original* source, not the compiled stub
}

This is the extension point most people miss: the loader is the seam, not the renderer.

A custom view engine (View::addExtension('twital', ...)) only pays off if you want .twital files resolved by Laravel’s own view finder (namespaces, view:cache, @include-style paths). It is more wiring than the loader decorator and buys you nothing extra; do it second, if at all.

Attribute catalogue you will actually reach for:

<div t:if="user.isAdmin" t:attributes='{"class": "badge"}'></div>
<tr t:for="row in rows">...</tr>
<td t:text="row.total"></td>
<a t:href="url" t:text="label"></a>          <!-- dynamic attribute syntax -->
{% if debug %}...{% endif %}                  <!-- native Twig still works -->

Mixing native Twig tags with t:* attributes in the same file is supported and is the pragmatic path — use t:* for the ergonomic 80% and drop to {% %} for macros, {% set %}, and anything stateful.

Custom attributes via visitors. Each t:foo is a visitor class; register your own through an extension implementing Twital\Extension\ExtensionInterface and returning visitors from getVisitors(). This is where you add project-specific sugar (e.g. t:money="order.total", t:icon="name"). Keep these in app/Support/Twital/ so they are discoverable and testable.

Golden-file testing. Because compile() is pure string-in/string-out, test Twital templates without booting Laravel or Twig:

$this->assertSame(
    file_get_contents(__DIR__.'/fixtures/table.twig'),
    (new Engine(...))->compile(file_get_contents(__DIR__.'/fixtures/table.twital'))
);

This is the fastest feedback loop available and makes upgrades to Twig or Twital reviewable as a diff.

Gotchas and Tips

There is no Laravel package, no service provider, no config file. You are writing the integration from scratch, and you will own it forever. Budget for that. The low ecosystem opportunity score here is real — 126 stars and no Laravel glue means few answers when something breaks.

You are parsing HTML, not strings. Twital’s compiler builds a DOM-like tree, so malformed markup, unclosed tags, <input>/<img> without self-closing slashes, and stray < in text will either throw or silently restructure your output. Validate templates as HTML in CI if you let designers author them.

Compiled output is whitespace- and case-sensitive in ways the source was not. Diff the compiled Twig on your first few templates. Attributes on SVG (viewBox, preserveAspectRatio) are a common casualty of HTML parsing — check them explicitly.

Errors point at the compiled file, not the source. A Twig Twig\Error\SyntaxError on line 14 of the compiled template may be line 9 of your .twital file. In APP_DEBUG, write the compiled artefact next to the source with a .compiled.twig suffix so the line you see is at least inspectable; keep the original in the exception message. This is the single biggest debugging tax with Twital.

Escaping is Twig’s, not Twital’s. t:text emits {{ ... }} and is therefore subject to Twig’s autoescape setting for the compiled file’s extension. If you compile to .twig, TwigBridge’s autoescape strategy applies; if you compile to a .html.twig in a cache dir, verify the strategy still matches. Do not disable autoescape to “fix” a t:attributes quoting problem — use the JSON-object form instead.

Do not recompile per request. The naive $twig->render → compile → render flow is 2–4× the parse cost of plain Twig. It is fine for a pilot and wrong for the homepage. Cache aggressively and invalidate on mtime or source hash, and hook your invalidation into php artisan view:clear.

Pin Twig. Twital’s visitors walk Twig’s AST/output conventions; a Twig major bump (2 → 3 already caused churn) can change generated code subtly. Constrain twig/twig in composer.json to the version you tested against rather than inheriting whatever TwigBridge pulls in.

Namespace collisions with Vue/Alpine are the main “can we use this?” question. t:* attributes survive in the browser, v-if/x-show do not get evaluated unless a client framework is mounted. If the same template ships to the browser, decide up front whether t:* is stripped at compile time (it is — it becomes {% %}) or whether you also ship a client framework. Don’t mix t:if and v-if on the same node; the semantics differ and neither tool knows about the other.

t:for still exposes Twig’s loop variable, and inner t:* attributes see the loop variable normally. What it does not do is scope anything beyond Twig’s own semantics — do not expect Vue’s reactive scoping.

Extension order matters. Register CoreExtension before your own extensions; custom visitors that target the same attribute name as a core visitor will conflict, and the resolution order is registration order, not “last wins”.

Upgrade risk is concentrated in one place. Because Twital is a small, single-maintainer package whose whole job is code generation, treat a version bump as you would a Blade compiler bump: re-run the golden-file suite, diff every fixture, and read the compiled output of your three most complex templates. Last release is recent (2025-04), but the surface area for regressions is the compiler, and the compiler is the whole package.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle