Documentation Index
Fetch the complete documentation index at: https://astraphp.com/llms.txt
Use this file to discover all available pages before exploring further.
Schema mutators
To support Lunar extension of the admin panel, mutators could be registered by developers.Core concepts
Page schema has stable IDs// Core schema defines stable IDs so extensions can target them.
$page = ViewPage::make('catalog.products.view')
->blocks([
DetailsBlock::make('details'),
RelatedTableBlock::make('variants')->section('relations'),
]);
interface SchemaMutator
{
public function appliesTo(string $pageId): bool;
/** Lower runs earlier; allow ordering */
public function priority(): int;
public function mutate(PageSchema $schema, SchemaContext $ctx): void;
}
final class SchemaContext
{
public function __construct(
public Request $request,
public User $user,
public array $routeParams,
public array $abilities,
) {}
}
Registry + pipeline
final class SchemaMutatorRegistry
{
/** @var list<SchemaMutator> */
private array $mutators = [];
public function register(SchemaMutator $m): void
{
$this->mutators[] = $m;
}
/** @return list<SchemaMutator> */
public function forPage(string $pageId): array
{
$list = array_filter($this->mutators, fn($m) => $m->appliesTo($pageId));
usort($list, fn($a, $b) => $a->priority() <=> $b->priority());
return $list;
}
}
final class SchemaPipeline
{
public function __construct(private SchemaMutatorRegistry $registry) {}
public function build(callable $baseFactory, string $pageId, SchemaContext $ctx): PageSchema
{
$schema = $baseFactory($ctx); // returns PageSchema (ViewPage/ListPage/FormPage)
foreach ($this->registry->forPage($pageId) as $mutator) {
$mutator->mutate($schema, $ctx);
}
return $schema;
}
}
Mutation operations (the API extensions actually use)
You want high-level operations, not “poke arrays”.abstract class PageSchema
{
public string $id;
// ---- blocks
public function addBlockAfter(string $existingBlockId, Block $block): void {}
public function addBlockBefore(string $existingBlockId, Block $block): void {}
public function addBlockToSection(string $sectionId, Block $block, ?int $position = null): void {}
public function removeBlock(string $blockId): void {}
// ---- forms
public function addFields(string $sectionId, array $fields, ?string $afterFieldKey = null): void {}
public function addSection(string $sectionId, string $title, ?string $afterSectionId = null): void {}
// ---- tables
public function addColumn(string $tableBlockId, Column $column, ?string $afterColumnKey = null): void {}
public function addFilter(string $tableBlockId, Filter $filter): void {}
public function addRowAction(string $tableBlockId, Action $action): void {}
}
Example mutators
Add a “Stock Movements” related table blockfinal class StockMovementsBlockMutator implements SchemaMutator
{
public function appliesTo(string $pageId): bool
{
return $pageId === 'catalog.products.view';
}
public function priority(): int { return 50; }
public function mutate(PageSchema $schema, SchemaContext $ctx): void
{
if (!in_array('inventory.view', $ctx->abilities)) {
return;
}
$productId = $ctx->routeParams['product']; // or record id
$schema->addBlockAfter('variants', RelatedTableBlock::make('stock_movements')
->title('Stock movements')
->stateKey('stock')
->endpoint(route('inventory.products.stock.data', ['product' => $productId]))
->list(StockMovementsListSchema::make()) // reuse list schema
);
}
}
final class TaxFieldsMutator implements SchemaMutator
{
public function appliesTo(string $pageId): bool
{
return $pageId === 'catalog.products.edit';
}
public function priority(): int { return 20; }
public function mutate(PageSchema $schema, SchemaContext $ctx): void
{
$schema->addFields('pricing', [
Field::select('tax_class_id')->label('Tax class')->optionsEndpoint(route('tax.classes.search')),
Field::toggle('prices_include_tax')->label('Prices include tax'),
], afterFieldKey: 'price');
}
}
final class VariantsEnhancementsMutator implements SchemaMutator
{
public function appliesTo(string $pageId): bool
{
return $pageId === 'catalog.products.view';
}
public function priority(): int { return 60; }
public function mutate(PageSchema $schema, SchemaContext $ctx): void
{
$schema->addColumn('variants', Column::badge('availability')->label('Availability'), afterColumnKey: 'sku');
$schema->addRowAction('variants', Action::button('Edit')
->icon('pencil')
->opensModal('variants.edit') // framework knows this means “modal form payload + submit”
);
}
}
How extensions register mutators
use App\Lunar\Http\Mutators\VariantsEnhancementsMutator;
use Lunar\Inventory\Http\Mutators\StockMovementsBlockMutator;
final class InventoryServiceProvider extends ServiceProvider
{
public function boot(): void
{
app(SchemaMutatorRegistry::class)->register(new StockMovementsBlockMutator);
app(SchemaMutatorRegistry::class)->register(new VariantsEnhancementsMutator);
}
}
How a controller uses the pipeline
final class ProductViewController extends Controller
{
public function __invoke(Request $request, SchemaPipeline $pipeline)
{
$ctx = new SchemaContext(
request: $request,
user: $request->user(),
routeParams: $request->route()->parameters(),
abilities: $this->abilitiesFor($request->user()),
);
$schema = $pipeline->build(
baseFactory: fn($ctx) => ProductViewSchema::build($ctx->request),
pageId: 'catalog.products.view',
ctx: $ctx
);
return Inertia::render($schema->component(), [
'page' => $schema->toArray(),
]);
}
}