ShapeRegistry
How a plugin publishes metadata that other plugins can discover without importing it.
Commander answers "run this named thing". ShapeRegistry answers "what named things exist?" — it is the discovery half of the same boundary.
use App\Plugins\ShapeRegistry;
$shapes = app(ShapeRegistry::class);
API#
public function declare(string $kind, string $key, array $shape): void
public function of(string $kind): array
public function get(string $kind, string $key): ?array
| Parameter | Meaning |
|---|---|
$kind |
Category of thing being published: entity, filter-source, metric. |
$key |
Identifier within that kind. |
$shape |
Flat serialisable array. Never a class name, never a closure. |
The flat-array rule#
A shape describes what exists and which command to call. It never carries executable code or a class reference.
$shapes->declare('filter-source', 'product', [
'entity' => 'product',
'label' => 'Products',
'commands' => [
'sources' => 'ecommerce.product.filter.sources',
'options' => 'ecommerce.product.filter.options',
'query' => 'ecommerce.product.query',
],
]);
The consumer reads names and calls them through Commander:
foreach (app(ShapeRegistry::class)->of('filter-source') as $provider) {
$sources = app(Commander::class)->run($provider['commands']['sources'], [], 'filter-bar');
}
If shapes could carry class references, the consumer would need the owner's classes on the
autoload path, and dependencies() would have to list the owner. Keeping shapes flat is what
lets a consumer declare no dependencies at all and still work with any provider that
shows up.
It also means shapes can be serialised — cached, sent to the admin UI, or rendered in a control panel — without executing anything.
Ownership#
PluginManager sets the current plugin key before calling each boot(), so every shape and
command is attributed automatically. The plugin detail screen lists what a plugin publishes:
entities, filter sources, and registered commands.
Declaring in boot()#
public function boot(PluginManager $manager): void
{
parent::boot($manager);
$shapes = app(ShapeRegistry::class);
$commander = app(Commander::class);
$shapes->declare('entity', 'product', [
'label' => 'Product',
'route' => '/products',
]);
$commander->register('ecommerce.product.filter.options',
fn (array $a) => ProductFilterSources::options((string) ($a['source'] ?? '')));
}
Do not query the database in boot(). It runs during migrate:fresh too, when the
tables may not exist. Declare static metadata here; resolve dynamic lists through a command
at request time.
Missing owners#
A consumer iterating of('filter-source') over an empty registry simply does nothing. A
consumer calling a command from a shape that is no longer registered gets CommandNotFound
from run(), or the default from tryRun().
Neither is an error condition — it is what a plugin being uninstalled looks like.