parisek / twig-attribute
A Twig extension for adding attributes to an element
Requires
- php: ^8.3
- twig/twig: ^3.27
Requires (Dev)
- ergebnis/composer-normalize: ^2.0
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.0 || ^11.0
This package is auto-updated.
Last update: 2026-06-01 17:46:49 UTC
README
A Twig 3 extension that gives templates a create_attribute() function for
collecting, sanitizing, and rendering HTML attributes — backed by a vendored,
maintained port of Drupal's Attribute class.
The package ships its own port (under Drupal\Component\Attribute) rather than
depending on Drupal core. The vendored sources are refreshed from Drupal 11.x
on each release; the API matches what Drupal templates expect.
Installation
composer require parisek/twig-attribute
Requires PHP ^8.3 and Twig ^3.0. No Drupal dependencies.
Usage
Plain PHP:
$twig = new \Twig\Environment($loader); $twig->addExtension(new \Parisek\Twig\AttributeExtension());
Symfony service:
services: Parisek\Twig\AttributeExtension: tags: [{ name: twig.extension }]
In templates
{% set my_attribute = create_attribute() %}
{% set my_classes = [
'kittens',
'llamas',
isKitten ? 'cats' : 'dogs',
] %}
<div{{ my_attribute.addClass(my_classes).setAttribute('id', 'myUniqueId') }}>
{{ content }}
</div>
<div{{ create_attribute({'class': ['region', 'region--header']}) }}> {{ content }} </div>
The full API (class methods, escape semantics, without filter behavior) mirrors
Drupal's Attribute class.
Upgrading from 1.5.x to 1.6.0
Action required: none for the vast majority of consumers. Run
composer update parisek/twig-attribute.
What changes under the hood:
- The vendored
Drupal\Component\Attribute\*classes are refreshed from Drupal 11.x core. Existing methods (addClass,setAttribute,removeAttribute,hasClass,merge,toArray,__toString, iterator support) keep their signatures and render output. - New methods become available — your existing templates ignore them
unless you opt in:
hasAttribute(string $name): bool— check existence without throwing.removeClass(...$classes): static— symmetric counterpart ofaddClass.getClass(): AttributeArray— read the class collection.jsonSerialize(): string— JSON encoding support (returns the rendered attribute string).__clone()— deep-clone correctness.
- The package now ships its own test suite (
tests/AttributeTest.php, 18 methods, pure PHPUnit). Runvendor/bin/phpunitto verify the install if you want extra confidence. - Composer constraints tightened to Twig 3+ and PHP ^8.3. Both
were already required transitively by
drupal/core-utility ^10.0 || ^11.0in 1.5.x, so this change doesn't shrink the real install matrix. - Both
drupal/core-renderanddrupal/core-utilityare no longer required by this package.Html::escape()is inlined as a 5-LOC private helper.NestedArray::mergeDeepandPlainTextOutput::renderFromHtmlare inlined as minimalParisek\Twig\Internal\*shims.
Edge cases that may need action
- You were reaching
Drupal\Component\Render\…orDrupal\Component\Utility\…classes through this package's transitive install. Unusual, but possible if you wrote framework-level code on top of the Attribute classes. Fix: add the relevantdrupal/core-*package to your owncomposer.jsonrequire. This is the correct long-term shape regardless — relying on transitive availability is fragile. - You're on PHP < 8.3 or Twig 2. You couldn't actually install 1.5.x
cleanly against modern Drupal 10/11 either, so this is more about
cleaning up your constraints. Bump PHP/Twig in your own project, or
pin
parisek/twig-attributeto1.5.*to stay on the previous floor.
Direct PHP usage
If your code does new \Drupal\Component\Attribute\AttributeCollection(...)
in PHP (instead of using create_attribute() from Twig), the refresh
adds methods but doesn't remove any. Your existing calls keep working
in 1.6.0.
Development
composer install vendor/bin/phpunit # 41 tests vendor/bin/phpstan analyse # level 5
Source-of-truth for the vendored Drupal port is Drupal 11.x core at
git.drupalcode.org/project/drupal/-/tree/11.x/core/lib/Drupal/Core/Template.
When the upstream changes meaningfully, copy the relevant files into
.upstream/ (gitignored scratch dir) and re-port. See docs/refresh-decisions.md
for the rationale behind the inline shims that let this package drop
drupal/core-render and drupal/core-utility.
Use cases
- Drupal — Pattern Lab
- WordPress — Timber
- Pimcore — Templates
- parisek/styleguide — the package that drove the 1.6.0 refresh.