Extensions
Berry has an extension method system.
This lets another package add methods to Berry tags without changing berry/html itself.
On this page we will build a tiny berry/cats package. It adds methods like purr(), catMood(), nap(), and stareAt() to Berry tags, and it shows the full extension flow from runtime registration to editor and PHPStan support.
What we are building
Section titled “What we are building”The goal is to make code like this work:
use Berry\Cats\CatMood;use Berry\Cats\NapSpot;use function Berry\Html\div;
echo div() ->purr() ->catMood(CatMood::Judging) ->nap(NapSpot::Sunbeam) ->stareAt('the treat cupboard', 10) ->text('This div has opinions.');Berry resolves those method calls at runtime after you register them with addMethod().
The custom types
Section titled “The custom types”Enums work well here because extension methods can accept predefined types instead of plain strings, which makes calls easier to read and helps developers make fewer mistakes.
namespace Berry\Cats;
enum CatMood: string{ case Judging = 'judging'; case Chaotic = 'chaotic'; case Sleepy = 'sleepy';}
enum NapSpot: string{ case Sunbeam = 'sunbeam'; case Keyboard = 'keyboard'; case FreshLaundry = 'fresh-laundry';}Extension methods can use enums, value objects, or scalar types in their signatures.
Registering methods
Section titled “Registering methods”Extension methods are registered with addMethod().
use Berry\Cats\CatMood;use Berry\Html\HtmlTag;use Berry\Html\HtmlVoidTag;
HtmlTag::addMethod('purr', function (HtmlTag $node): HtmlTag { return $node->attr('data-cat-purr', 'true');});
HtmlVoidTag::addMethod('purr', function (HtmlVoidTag $node): HtmlVoidTag { return $node->attr('data-cat-purr', 'true');});
HtmlTag::addMethod('catMood', function (HtmlTag $node, CatMood $mood): HtmlTag { return $node->attr('data-cat-mood', $mood->value);});
HtmlVoidTag::addMethod('catMood', function (HtmlVoidTag $node, CatMood $mood): HtmlVoidTag { return $node->attr('data-cat-mood', $mood->value);});After registration, Berry tags can respond to ->purr() and ->catMood(...).
Returning the node
Section titled “Returning the node”Most extension methods should return the node again so the API stays chainable.
use Berry\Cats\NapSpot;use Berry\Html\HtmlTag;use Berry\Html\HtmlVoidTag;
HtmlTag::addMethod('nap', function (HtmlTag $node, NapSpot $spot): HtmlTag { return $node->attr('data-cat-nap', $spot->value);});
HtmlVoidTag::addMethod('nap', function (HtmlVoidTag $node, NapSpot $spot): HtmlVoidTag { return $node->attr('data-cat-nap', $spot->value);});
HtmlTag::addMethod('stareAt', function (HtmlTag $node, string $target, int $seconds = 3): HtmlTag { return $node ->attr('data-cat-target', $target) ->attr('data-cat-stare-seconds', (string) $seconds);});
HtmlVoidTag::addMethod('stareAt', function (HtmlVoidTag $node, string $target, int $seconds = 3): HtmlVoidTag { return $node ->attr('data-cat-target', $target) ->attr('data-cat-stare-seconds', (string) $seconds);});Then the full chain works:
use Berry\Cats\CatMood;use Berry\Cats\NapSpot;use function Berry\Html\div;
div() ->purr() ->catMood(CatMood::Judging) ->nap(NapSpot::Sunbeam) ->stareAt('the treat cupboard', 10) ->class('cat') ->text('Waiting with purpose.');Where to register methods
Section titled “Where to register methods”Register methods on the most specific class that makes sense.
AbstractTagif the method should exist on all tagsHtmlTagif it should exist on normal HTML tagsHtmlVoidTagif it should exist on void HTML tags- a concrete element class if it only makes sense there
For berry/cats, registering on both HtmlTag and HtmlVoidTag makes sense, otherwise a method might work on div() but not on input().
Packaging the extension
Section titled “Packaging the extension”Collect the registration in one installer class.
<?php declare(strict_types=1);
namespace Berry\Cats;
use Closure;use Berry\Html\HtmlTag;use Berry\Html\HtmlVoidTag;
final class BerryCats{ public static function install(): void { HtmlTag::addMethod('purr', self::purr()); HtmlVoidTag::addMethod('purr', self::purr());
HtmlTag::addMethod('catMood', self::catMood()); HtmlVoidTag::addMethod('catMood', self::catMood());
HtmlTag::addMethod('nap', self::nap()); HtmlVoidTag::addMethod('nap', self::nap());
HtmlTag::addMethod('stareAt', self::stareAt()); HtmlVoidTag::addMethod('stareAt', self::stareAt()); }
private static function purr(): Closure { return function (HtmlTag|HtmlVoidTag $node): HtmlTag|HtmlVoidTag { return $node->attr('data-cat-purr', 'true'); }; }
private static function catMood(): Closure { return function (HtmlTag|HtmlVoidTag $node, CatMood $mood): HtmlTag|HtmlVoidTag { return $node->attr('data-cat-mood', $mood->value); }; }
private static function nap(): Closure { return function (HtmlTag|HtmlVoidTag $node, NapSpot $spot): HtmlTag|HtmlVoidTag { return $node->attr('data-cat-nap', $spot->value); }; }
private static function stareAt(): Closure { return function (HtmlTag|HtmlVoidTag $node, string $target, int $seconds = 3): HtmlTag|HtmlVoidTag { return $node ->attr('data-cat-target', $target) ->attr('data-cat-stare-seconds', (string) $seconds); }; }}Then an autoloaded bootstrap file can install the extension:
use Berry\Cats\BerryCats;
BerryCats::install();To have Composer load that file, add it to autoload.files:
{ "autoload": { "files": ["src/bootstrap.php"] }}Once Composer loads that file, the methods are available.
Static analysis
Section titled “Static analysis”Dynamic methods work at runtime, but editors and static analyzers still need a description of their signatures.
For packages, add a berry-method-extensions.json file and use berry/extension-method-stub-generator to generate stubs.
The extension JSON file
Section titled “The extension JSON file”berry-method-extensions.json is a single JSON object with an extensions array.
Each entry in extensions describes one group of methods. Usually this matches the methods you already registered at runtime.
namespace: the namespace for the generated stub class, usuallyBerry\\Htmlclass: one class name or a list of class names, for example["HtmlTag", "HtmlVoidTag"]uses: optional imports for custom types used in method signaturesmethods: the methods that should appear in generated stubs
Each method usually contains:
name: the method namedoc: a short description used in the generated@methodannotationreturns: usuallystaticso chaining is preservedargs: a list of arguments
Each argument can contain:
name: the parameter nametype: a PHP type stringdefaultValue: an optional default value written as PHP code, for example3ornull
This file does not register methods. It describes the method signatures for the stub generator.
Here is the same berry/cats extension described for tooling:
{ "extensions": [ { "namespace": "Berry\\Html", "class": ["HtmlTag", "HtmlVoidTag"], "uses": ["Berry\\Cats\\CatMood", "Berry\\Cats\\NapSpot"], "methods": [ { "name": "purr", "doc": "Adds a completely unnecessary purring aura", "returns": "static", "args": [] }, { "name": "catMood", "doc": "Sets the emotional state of the element", "returns": "static", "args": [ { "type": "CatMood", "name": "mood" } ] }, { "name": "nap", "doc": "Marks the element as napping in a preferred location", "returns": "static", "args": [ { "type": "NapSpot", "name": "spot" } ] }, { "name": "stareAt", "doc": "Stares at something for an optional number of seconds", "returns": "static", "args": [ { "type": "string", "name": "target" }, { "type": "int", "name": "seconds", "defaultValue": "3" } ] } ] } ]}This JSON mirrors the runtime methods above and shows the main ideas:
- one extension can target both
HtmlTagandHtmlVoidTag useslets the generated stub import enums or other custom PHP types- methods can have no arguments, typed arguments, and optional arguments with defaults
The generator scans installed packages, writes files into .berry/stubs, and creates .berry/extension.neon for PHPStan.
includes: - .berry/extension.neonThat is enough for editors and PHPStan to understand the cat methods.
Real world examples
Section titled “Real world examples”For production examples, look at berry/htmx and berry/alpinejs.
berry/htmxis a larger example with custom types imported throughusesberry/alpinejsis a simpler example with a lot of straightforward method signatures
They use the same overall pattern.