Skill: Craft Extension Point
"Every private method is a feature request waiting to happen."
The Standard
Manager/Driver Pattern: Taylor's signature extensibility mechanism. Register built-in drivers via create{Name}Driver() methods. Consumers add custom drivers via extend(). The default driver delegates through __call().
This is the primary extension mechanism for multi-implementation services: cache, mail, notifications, queue, session, filesystem, Scout engines, Socialite providers, Pennant feature stores.
Config-Driven Binding: Define contracts. Bind implementations via config. Users swap behavior by changing a config value. No code changes, no service provider overrides. Global behavior swap. Zero ceremony.
Macroable Registration: Runtime extension of any class using the Macroable trait. macro() for single methods, mixin() for bulk-registering from a class. Used across the framework: Collection, Builder, Request, Response, Router, Str.
Render Hooks (Filament): Named injection points where plugins insert Blade content. Typed enum constants define hook locations. Scoped hooks target specific pages. 60+ hook points across panels, tables, actions.
Adapter Pattern (League): Framework-agnostic core with framework-specific adapters. The interface IS the extension point. Implement it, pass it to the core, done. No registration, no discovery, no plugin manifest.
Strategy Pattern + Config: Accept interface implementations for custom behavior. Built-in factories for common cases, raw interface for everything else. The user chooses from your menu or brings their own dish.
Events: Fire domain events at lifecycle boundaries. Users hook in without touching core code. Events are the loosest coupling. The package doesn't know or care who's listening.
Static Callback Customization: Accept callbacks on the "god class" for customization points. Sanctum::authenticateAccessTokensUsing(), Cashier::formatCurrencyUsing(), Horizon::auth(). Called from AppServiceProvider::boot().
The use{Model}() Pattern: Let consumers swap internal model classes via static setters on the god class. Cashier::useCustomerModel(Team::class). The package references static::$customerModel instead of hardcoding.
The Hierarchy
Choose the right extension mechanism for the scope:
| Scope |
Mechanism |
Example |
| Multi-driver services |
Manager/Driver pattern |
Scout::extend('elastic', fn () => ...) |
| Global (app-wide) |
Config-driven binding |
Swap the Role model via config |
| Runtime (ad-hoc) |
Macroable registration |
Collection::macro('toUpper', ...) |
| UI injection |
Render hooks |
PanelsRenderHook::SIDEBAR_NAV_END |
| Framework-agnostic |
Adapter/Interface |
Implement FilesystemAdapter |
| Per-use (one call) |
Callback injection |
Custom filter closure |
| Reusable (shared) |
Interface implementation |
Custom Filter class |
| Decoupled (fire-and-forget) |
Events |
RoleAttachedEvent |
| Model customization |
use{Model}() static setters |
Sanctum::usePersonalAccessTokenModel() |
Start with config binding for simple swaps. Use the Manager pattern for driver-based architectures. Events for side effects. Macroable for ad-hoc runtime extensions.
The Anti-Patterns
| Don't |
Do |
Why |
private methods that prevent extension |
protected -- let subclasses override |
Every private is a closed door |
final classes that block inheritance |
final only on value objects and DTOs |
Extension is a feature, not a risk |
| No events at lifecycle boundaries |
Dispatch events at create, update, delete, attach, detach |
Users need hooks without modifying source |
| Hardcoded implementations |
Config-driven class resolution |
Users swap without forking |
| Only closures, no interfaces |
Closures for one-offs, interfaces for reusable strategies |
Closures don't compose, interfaces do |
| Reinventing the driver pattern |
Extend Illuminate\Support\Manager |
The wheel is already round |
| One extension mechanism for everything |
Match the mechanism to the scope |
Different problems need different tools |
See also: ground-extensibility (when and why to extend), craft-macro (deep dive on Macroable).
Real-World Examples
See examples.md.
1---2name: craft-extension-point3description: Crafting extension points. Making code flexible means fewer feature requests.4---56# Skill: Craft Extension Point78> "Every `private` method is a feature request waiting to happen."910## The Standard11121. **Manager/Driver Pattern**: Taylor's signature extensibility mechanism. Register built-in drivers via `create{Name}Driver()` methods. Consumers add custom drivers via `extend()`. The default driver delegates through `__call()`.1314 This is the primary extension mechanism for multi-implementation services: cache, mail, notifications, queue, session, filesystem, Scout engines, Socialite providers, Pennant feature stores.15162. **Config-Driven Binding**: Define contracts. Bind implementations via config. Users swap behavior by changing a config value. No code changes, no service provider overrides. Global behavior swap. Zero ceremony.17183. **Macroable Registration**: Runtime extension of any class using the `Macroable` trait. `macro()` for single methods, `mixin()` for bulk-registering from a class. Used across the framework: Collection, Builder, Request, Response, Router, Str.19204. **Render Hooks (Filament)**: Named injection points where plugins insert Blade content. Typed enum constants define hook locations. Scoped hooks target specific pages. 60+ hook points across panels, tables, actions.21225. **Adapter Pattern (League)**: Framework-agnostic core with framework-specific adapters. The interface IS the extension point. Implement it, pass it to the core, done. No registration, no discovery, no plugin manifest.23246. **Strategy Pattern + Config**: Accept interface implementations for custom behavior. Built-in factories for common cases, raw interface for everything else. The user chooses from your menu or brings their own dish.25267. **Events**: Fire domain events at lifecycle boundaries. Users hook in without touching core code. Events are the loosest coupling. The package doesn't know or care who's listening.27288. **Static Callback Customization**: Accept callbacks on the "god class" for customization points. `Sanctum::authenticateAccessTokensUsing()`, `Cashier::formatCurrencyUsing()`, `Horizon::auth()`. Called from `AppServiceProvider::boot()`.29309. **The `use{Model}()` Pattern**: Let consumers swap internal model classes via static setters on the god class. `Cashier::useCustomerModel(Team::class)`. The package references `static::$customerModel` instead of hardcoding.3132## The Hierarchy3334Choose the right extension mechanism for the scope:3536| Scope | Mechanism | Example |37|-----------------------------|-------------------------------|------------------------------------------|38| Multi-driver services | Manager/Driver pattern | `Scout::extend('elastic', fn () => ...)` |39| Global (app-wide) | Config-driven binding | Swap the `Role` model via config |40| Runtime (ad-hoc) | Macroable registration | `Collection::macro('toUpper', ...)` |41| UI injection | Render hooks | `PanelsRenderHook::SIDEBAR_NAV_END` |42| Framework-agnostic | Adapter/Interface | Implement `FilesystemAdapter` |43| Per-use (one call) | Callback injection | Custom filter closure |44| Reusable (shared) | Interface implementation | Custom `Filter` class |45| Decoupled (fire-and-forget) | Events | `RoleAttachedEvent` |46| Model customization | `use{Model}()` static setters | `Sanctum::usePersonalAccessTokenModel()` |4748Start with config binding for simple swaps. Use the Manager pattern for driver-based architectures. Events for side effects. Macroable for ad-hoc runtime extensions.4950## The Anti-Patterns5152| Don't | Do | Why |53|------------------------------------------|-----------------------------------------------------------|-------------------------------------------|54| `private` methods that prevent extension | `protected` -- let subclasses override | Every `private` is a closed door |55| `final` classes that block inheritance | `final` only on value objects and DTOs | Extension is a feature, not a risk |56| No events at lifecycle boundaries | Dispatch events at create, update, delete, attach, detach | Users need hooks without modifying source |57| Hardcoded implementations | Config-driven class resolution | Users swap without forking |58| Only closures, no interfaces | Closures for one-offs, interfaces for reusable strategies | Closures don't compose, interfaces do |59| Reinventing the driver pattern | Extend `Illuminate\Support\Manager` | The wheel is already round |60| One extension mechanism for everything | Match the mechanism to the scope | Different problems need different tools |6162**See also:** ground-extensibility (when and why to extend), craft-macro (deep dive on Macroable).6364## Real-World Examples6566See [examples.md](examples.md).