jdecool/twig-constant-accessor-bundle
Symfony bundle that integrates a Twig extension for reading PHP class constants and enum cases directly in templates. It exposes a dedicated accessor function so shared values can stay in PHP while remaining easy to use in Twig, without writing custom extensions.
Reality check before you type composer require: this is a Symfony bundle, not a Laravel package. There is no ExtraServiceProvider, no config/twig-constant-accessor.php to publish, and no auto-discovery. The 1.10.0 release ("Add TwigConstantAccessorBundle attribute") does not change that — a PHP attribute is Symfony autoconfiguration metadata, and Laravel has no autoconfiguration pass to read it. If you are on a standard Blade codebase, the honest answer is still: don't install it — read src/ and copy the ~50-line Twig extension into app/Twig/. It is only worth a real install if you are running Twig inside Laravel via rcrowe/twigbridge.
Path A — TwigBridge (the only sane "install" route):
composer require rcrowe/twigbridge
composer require jdecool/twig-constant-accessor-bundle
Then register the extension manually in config/twigbridge.php, because TwigBridge will not pick up a Symfony bundle's DI config or its attributes:
'extensions' => [
// ...
\JDecool\TwigConstantAccessorBundle\Twig\ConstantAccessorExtension::class,
],
If the extension's constructor takes the accessor service, bind it in a service provider:
$this->app->singleton(ConstantAccessor::class, fn () => new ConstantAccessor(config('twig-constants.aliases', [])));
Path B — vendored extension: copy the class out of vendor/jdecool/.../src/Twig/, drop it in app/Twig/, and register it in config/twigbridge.php the same way. This is the version you can actually maintain, and it sidesteps the new src/Attribute/ directory entirely.
First use case: kill magic strings in email/mail templates. Status enums are the classic win — instead of {% if order.status == 'shipped' %}, you write {% if order.status == constant('App\\Enums\\OrderStatus::SHIPPED') %} (or the bundle's aliased form, depending on what getFunctions() in src/Twig/ shows). Open src/Twig/*Extension.php first — verify the exact function name and argument order before writing templates; the bundle's whole value-add is over Twig's built-in constant() (namespace aliasing so templates don't carry FQCNs), and the API differs. As of 1.10.0 also skim src/Attribute/ to see what the new attribute actually declares, since the release note says nothing beyond its existence.
Alias your domain classes, don't inline FQCNs. The point of the package is that a template should say constant('OrderStatus::SHIPPED', 'enums'), not carry App\Enums\OrderStatus. Register aliases in a Laravel config file and feed them into the accessor:
// config/twig-constants.php
return [
'aliases' => [
'enums' => 'App\\Enums\\OrderStatus',
'payment' => 'App\\Enums\\PaymentMethod',
],
];
This gives you one place to rename a namespace instead of grepping resources/views.
Treat the new attribute as Symfony-only. The 1.10.0 TwigConstantAccessorBundle attribute is a declaration convenience for container-driven setups: you annotate a class and Symfony's autoconfiguration wires it. Under TwigBridge nothing scans attributes, so the attribute class is inert — do not move your alias map into it, and do not let a Symfony tutorial convince you the config file is obsolete. If the attribute turns out to carry an alias→class mapping, mirror that same shape in config/twig-constants.php so the two mental models match.
Pair it with backed enums (PHP 8.1+) sparingly. For a backed enum, constant('App\\Enums\\OrderStatus::SHIPPED') returns the case object, not the string. If your template compares to a DB column value you want:
{% if order.status == constant('App\\Enums\\OrderStatus::SHIPPED').value %}
If your Twig version supports the enum() function (Twig 3.x), prefer that and skip this package entirely for enum cases — the bundle is really about plain class constants.
Wrap repeated lookups in a domain facade. When a template needs five constants from one class, the pattern that ages well is a single class exposing the curated set:
final class Template
{
public const SHIPPING_LABEL = 'Standard (3-5 days)';
public const VAT_RATE = 0.20;
}
Templates then only need one alias, and you keep the blast radius small.
Testing. Because constants are resolved at render time via the PHP constant() call, a typo blows up at request time, not compile time. Add a smoke test that renders each Twig template which uses the accessor:
$this->get('/orders/1')->assertOk();
Better: a data-driven test asserting every config alias resolves (defined($fqcn.'::'.$name)) — this catches enum renames in CI rather than in production emails.
Migration tip. Add the extension, then convert templates incrementally. Leave a {# TODO: constant #} marker so the hardcoded-literal cleanup is greppable, and replace 'pending'/'paid' literals in the same PR as the enum definition change.
composer.json constraints before you plan upgrades, and expect to --ignore-platform-req or vendor the class anyway.constant() function. If the bundle registers a function with the same name, registration order decides which one wins — and that ordering differs between Symfony's bundle system and TwigBridge's extension list. Explicitly test one template after wiring it up; if you get "unknown class" errors, your extension is being shadowed.App\Enums\Foo is fine (Twig does not process \E), but in double-quoted strings you must write App\\Enums\\Foo. Standardise on single quotes.public and the class autoloadable. private/protected constants, interface constants inherited from a parent, and traits work inconsistently — stick to public const on a concrete class.php artisan view:clear. Cheap habit: clear view cache after deploying any constant change — and again after bumping 1.9 → 1.10, since the attribute may alter how the extension is constructed in Symfony-style setups.AppServiceProvider.{{ order.statusLabel }} from a presenter is usually the better Laravel answer anyway.How can I help you explore Laravel packages today?