Skip to content

[Draft RFC] Support named extension declarations (Extension Methods RFC, in review)#43

Draft
hollyschilling wants to merge 2 commits into
composer:mainfrom
hollyschilling:extension-declarations
Draft

[Draft RFC] Support named extension declarations (Extension Methods RFC, in review)#43
hollyschilling wants to merge 2 commits into
composer:mainfrom
hollyschilling:extension-declarations

Conversation

@hollyschilling

@hollyschilling hollyschilling commented Jul 11, 2026

Copy link
Copy Markdown

What

Draft support for the named form of extension declarations from the proposed PHP "Extension Methods" RFC family, which is currently in informal review:

The named form declares an autoloadable symbol, namespaced exactly like a class declaration's name:

namespace Acme\DomKit;

extension DomTraversal on \DOMElement $el
{
    public function firstByClass(string $class): ?\DOMElement { /* ... */ }
}

→ class-map entry Acme\DomKit\DomTraversal mapping to this file. The target after on and the receiver variable are not part of the name and are not captured.

This follows the approach used when enum support was added (composer/composer#9670): parsing is purely text-based (PhpFileCleaner + regex), so it does not depend on token_get_all() or on the running PHP understanding the syntax, and works on every PHP version this package supports.

How

  • PhpFileParser: extension is added to the early-exit prefilter, and the main regex gets a dedicated branch that requires the full extension Name on Target $var tail. extension and on are contextual identifiers in the RFC grammar, so requiring the complete tail (including the mandatory receiver variable) is what makes false positives near-impossible — extension remains a perfectly valid class/function/method name today. The target may be an identifier, qualified/fully-qualified name, or a built-in type name (string, int, float, bool, array). Keywords match case-insensitively like the existing ones.
  • PhpFileCleaner: the keyword shortlist was keyed by first character, so extension collided with enum under e; the config now holds a list per character. extension gets a tail-aware pattern so the single-match early return keeps everything the parser regex needs to see (a name-only pattern would truncate the declaration after the name and drop the match).
  • The anonymous form (extension \DateTimeImmutable $date { ... }, extension string $s { ... }) declares no addressable symbol and produces no entry — it can't match because there is no Name on sequence.
  • The RFC's use extension Acme\DomKit\Traversal; import statement declares nothing and produces no entry — it can't match because no on Target $var tail follows.
  • PSR-4 / PSR-0 validation needed no changes: filterByNamespace() is symbol-agnostic, so a named extension declaration matching the file path is accepted as the file's expected symbol (covered by tests). PSR-4 itself needs no amendment either — the spec's "classes, interfaces, traits, and other similar structures" wording covers extension declarations, as it did enums.
  • Duplicate names: extension names live in the same name→file map as class-like symbols and reuse the existing ambiguous-class warning behavior (covered by tests). Note that name-collision policy between extensions and class-like symbols is still an open question in the RFC; if the RFC lands on a separate symbol space, this can be revisited.
  • Target capture (future-proofing only): the regex captures the raw target token after on as a named group (exttarget), exactly as written in the source — relative names and import aliases stay unresolved (covered by tests, including an aliased-target fixture). Nothing consumes it yet and the class map output is unchanged; this settles the pattern shape for the target-hint optimization described in the RFC's Future Scope (the engine filters autoload candidates by target, with a load-the-rest fallback). The extension branch of the parser regex is exposed as an @internal constant so the tests assert against the exact pattern used in production.

Deliberately not in this PR

No new autoload artifact and no map format change (no autoload_extensions.php, no target metadata in the emitted map). That half waits until the engine-side hint API is a live proposal — keeping this PR minimal and mergeable on the enum precedent.

Tests

New fixtures cover: global + namespaced named forms, fully-qualified/qualified/unqualified/scalar/array targets, a multiline declaration with comments between tokens, non-lowercase keywords, both anonymous forms (no entries), an extension declared alongside a class in one file (both mapped), PSR-4 acceptance + PSR-4 violation for a mis-named extension, duplicate-name ambiguity, and the false-positive zoo: class extension, function extension(), $obj->extension(), new extension(), use extension …;, and a declaration inside a string literal.

Fixture files use the proposed syntax and are not valid PHP on any current version; they sit under tests/Fixtures/, which lint, php-cs-fixer, and PHPStan already exclude (same mechanism as the enum fixtures on PHP < 8.1).

Why now / draft status

Map entries for extension names are inert until PHP ships the engine-side autoload trigger (a later phase of the RFC), so landing this early is harmless and unblocks the ecosystem story. The grammar is draft-stable but not final — this PR tracks the RFC and should stay draft until the RFC is accepted.

Disclosure: I am the author of the RFC drafts linked above.

🤖 Generated with Claude Code

hollyschilling and others added 2 commits July 11, 2026 12:22
… RFC)

Recognizes the proposed `extension Name on Target $var { ... }` declaration
in the class map, following the approach used for enum support. The full
"Name on Target $var" tail is required, so the anonymous form, the RFC's
"use extension ..." import statement, and existing symbols named
"extension" cannot produce false positives. Parsing is purely text-based
and works on any PHP version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The target after "on" is now captured as a named group (exttarget),
exactly as written in the source — relative names and import aliases stay
unresolved. Nothing is emitted yet: the class map output is unchanged.
This settles the regex shape for the target-hint optimization described
in the RFC's Future Scope. The extension branch of the parser regex is
exposed as an @internal constant so tests assert against the exact
pattern used in production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant