Settings Schema Registry
The Settings Schema Registry is a runtime registry of settings form-builder. The admin Settings page renders first-party Capell settings groups (core, admin, and frontend) as tabs when they are registered. Marketplace and third-party package settings should be exposed through package-owned Filament pages that reuse the same registry.
Architecture
Section titled “Architecture”Core Components
Section titled “Core Components”-
SettingsSchemaRegistry (
Capell\Core\Support\Settings\SettingsSchemaRegistry)- Central registry maintaining all settings schemas
- Organizes schemas by group (e.g., ‘core’, ‘admin’, ‘frontend’)
- Supports multiple schemas per group (composable)
- Stores optional metadata for package-owned settings pages
-
SettingsSchemaBootstrapper (
Capell\Core\Support\Settings\SettingsSchemaBootstrapper)- Manages extension callbacks
- Executes after package registration
- Allows dynamic schema modifications
-
HasSchema Contract (
Capell\Admin\Filament\Contracts\HasSchema)- Interface all settings schemas must implement
- Defines
make(Schema $schema): arraymethod
Basic Usage
Section titled “Basic Usage”Registering a Settings Schema
Section titled “Registering a Settings Schema”In your package’s service provider:
use Capell\YourPackage\Filament\Settings\YourSettingsSchema;use Capell\YourPackage\Settings\YourSettings;use Capell\Core\Support\Settings\SettingsGroupMetadata;use Capell\Core\Support\Settings\SettingsSchemaRegistry;use Filament\Support\Icons\Heroicon;
private function registerSettingsSchemas(): self{ $registry = resolve(SettingsSchemaRegistry::class);
// Register the settings class (for form hydration/saving) $registry->registerSettingsClass('yourgroup', YourSettings::class);
$registry->registerMetadata(new SettingsGroupMetadata( group: 'yourgroup', label: 'your-package::settings.title', icon: Heroicon::OutlinedCog6Tooth, navigationGroup: 'capell-admin::navigation.group_system', packageName: 'capell-app/your-package', ));
// Register the schema class (for form building) $registry->register('yourgroup', YourSettingsSchema::class);
return $this;}Creating a Package Settings Page
Section titled “Creating a Package Settings Page”Package settings pages should extend AbstractPackageSettingsPage:
<?php
declare(strict_types=1);
namespace Capell\YourPackage\Filament\Pages;
use Capell\Admin\Filament\Pages\AbstractPackageSettingsPage;
class YourPackageSettingsPage extends AbstractPackageSettingsPage{ protected static string $settingsGroup = 'yourgroup';
protected static ?string $slug = 'your-package/settings';}Register the page from your provider:
CapellAdmin::registerExtensionPage('capell-app/your-package', YourPackageSettingsPage::class);Creating a Settings Schema
Section titled “Creating a Settings Schema”All settings schemas must implement the HasSchema contract:
<?php
declare(strict_types=1);
namespace Capell\YourPackage\Filament\Settings;
use Capell\Admin\Filament\Contracts\HasSchema;use Filament\Forms\Components\TextInput;use Filament\Forms\Components\Checkbox;use Filament\Schemas\Components\Section;use Filament\Schemas\Schema;
class YourSettingsSchema implements HasSchema{ public static function make(Schema $schema): array { return [ Section::make(__('your-package::settings.general')) ->columnSpanFull() ->schema([ TextInput::make('api_key') ->label(__('your-package::settings.api_key')) ->required(),
Checkbox::make('enabled') ->label(__('your-package::settings.enabled')) ->default(true), ]) ->columns(2), ]; }}Field Presentation Guidelines
Section titled “Field Presentation Guidelines”Settings schemas should return top-level Section components, not bare fields or bare Grid components. A section gives labels, helper text, toggles, and inputs the Filament card background they need in both light and dark mode. Never leave labels and inputs floating directly on the page background, and do not call contained(false) on a section that contains normal form fields unless the section is immediately wrapped by another contained panel.
Use Section::make()->columns() for simple responsive layout inside the panel. Use a nested Grid only when it makes the schema clearer, and keep that grid inside a contained section.
Good:
Section::make(__('your-package::settings.display')) ->columnSpanFull() ->schema([ TextInput::make('items_per_page') ->label(__('your-package::settings.items_per_page')) ->numeric(), ]) ->columns(2);Avoid:
Grid::make(2) ->schema([ TextInput::make('items_per_page'), ]);
Section::make(__('your-package::settings.display')) ->contained(false) ->schema([ TextInput::make('items_per_page'), ]);Creating a Settings Class
Section titled “Creating a Settings Class”Your settings class should extend Spatie\LaravelSettings\Settings:
<?php
declare(strict_types=1);
namespace Capell\YourPackage\Settings;
use Capell\Core\Contracts\SettingsContract;use Capell\YourPackage\Filament\Settings\YourSettingsSchema;use Spatie\LaravelSettings\Settings;
class YourSettings extends Settings implements SettingsContract{ public string $api_key; public bool $enabled;
public static function group(): string { return 'yourgroup'; }
public static function schema(): string { return YourSettingsSchema::class; }}Advanced Usage
Section titled “Advanced Usage”Multiple Schemas Per Group (Composition)
Section titled “Multiple Schemas Per Group (Composition)”You can register multiple schemas for the same group. They will be merged together:
$registry = resolve(SettingsSchemaRegistry::class);
// Core schema$registry->register('admin', AdminCoreSchema::class, 'core');
// Additional schema from a plugin$registry->register('admin', AdminExtendedSchema::class, 'extended');Both schemas will appear on the package-owned settings page for that group.
Replacing an Existing Schema
Section titled “Replacing an Existing Schema”To override a schema registered by another package:
$registry = resolve(SettingsSchemaRegistry::class);
// Replace the core admin schema with a custom one$registry->replace('admin', CustomAdminSchema::class, 'AdminSettingsSchema');Removing a Schema
Section titled “Removing a Schema”To remove a schema entirely:
$registry = resolve(SettingsSchemaRegistry::class);
// Remove a specific schema$registry->remove('admin', 'AdminSettingsSchema');
// Remove all schemas from a group$registry->removeGroup('admin');Dynamic Schema Registration
Section titled “Dynamic Schema Registration”Use the bootstrapper to register schemas after all packages have loaded:
use Capell\Core\Support\Settings\SettingsSchemaBootstrapper;use Capell\Core\Support\Settings\SettingsSchemaRegistry;
// In your service provider's boot methodresolve(SettingsSchemaBootstrapper::class)->extend(function (SettingsSchemaRegistry $registry): void { // This runs after all packages are registered $registry->register('admin', MyDynamicSchema::class);});Registry API Reference
Section titled “Registry API Reference”SettingsSchemaRegistry Methods
Section titled “SettingsSchemaRegistry Methods”register(string $group, string $schemaClass, ?string $key = null): void
Section titled “register(string $group, string $schemaClass, ?string $key = null): void”Register a new schema for a group.
Parameters:
$group- Settings group identifier (e.g., ‘core’, ‘admin’, ‘frontend’)$schemaClass- Fully qualified class name implementingHasSchema$key- Optional unique identifier (defaults to class basename)
Example:
$registry->register('admin', AdminSettingsSchema::class);$registry->register('admin', CustomSchema::class, 'my_custom');registerSettingsClass(string $group, string $settingsClass): void
Section titled “registerSettingsClass(string $group, string $settingsClass): void”Register the primary settings class for a group.
Parameters:
$group- Settings group identifier$settingsClass- Fully qualified settings class name
Example:
$registry->registerSettingsClass('admin', AdminSettings::class);replace(string $group, string $schemaClass, string $key): void
Section titled “replace(string $group, string $schemaClass, string $key): void”Replace an existing schema in a group.
Parameters:
$group- Settings group identifier$schemaClass- New schema class$key- Key of the schema to replace (must exist)
Throws: InvalidArgumentException if key doesn’t exist
Example:
$registry->replace('admin', NewAdminSchema::class, 'AdminSettingsSchema');remove(string $group, string $key): void
Section titled “remove(string $group, string $key): void”Remove a specific schema from a group.
Parameters:
$group- Settings group identifier$key- Schema key to remove
Example:
$registry->remove('admin', 'AdminSettingsSchema');removeGroup(string $group): void
Section titled “removeGroup(string $group): void”Remove all schemas from a group.
Parameters:
$group- Settings group identifier
Example:
$registry->removeGroup('admin');getSchemas(string $group): array
Section titled “getSchemas(string $group): array”Get all schema classes for a group.
Parameters:
$group- Settings group identifier
Returns: array<string, class-string<HasSchema>>
Example:
$schemas = $registry->getSchemas('admin');foreach ($schemas as $key => $schemaClass) { // Process each schema}getSchema(string $group, string $key): ?string
Section titled “getSchema(string $group, string $key): ?string”Get a specific schema by group and key.
Parameters:
$group- Settings group identifier$key- Schema key
Returns: class-string<HasSchema>|null
Example:
$schema = $registry->getSchema('admin', 'AdminSettingsSchema');getSettingsClass(string $group): ?string
Section titled “getSettingsClass(string $group): ?string”Get the primary settings class for a group.
Parameters:
$group- Settings group identifier
Returns: class-string|null
Example:
$settingsClass = $registry->getSettingsClass('admin');getGroups(): array
Section titled “getGroups(): array”Get all registered group names.
Returns: array<string>
Example:
$groups = $registry->getGroups();// ['core', 'admin', 'frontend', 'ai-orchestrator']hasGroup(string $group): bool
Section titled “hasGroup(string $group): bool”Check if a group has any schemas registered.
Parameters:
$group- Settings group identifier
Returns: bool
Example:
if ($registry->hasGroup('admin')) { // Admin group exists}all(): array
Section titled “all(): array”Get all registered schemas across all groups.
Returns: array<string, array<string, class-string<HasSchema>>>
Example:
$allSchemas = $registry->all();// [// 'core' => ['CoreSettingsSchema' => CoreSettingsSchema::class],// 'admin' => ['AdminSettingsSchema' => AdminSettingsSchema::class],// ]Package Integration Examples
Section titled “Package Integration Examples”Core Package
Section titled “Core Package”The Core package registers the registry and bootstrapper:
// In CapellServiceProvider::packageRegistered()private function registerSettingsSchemaRegistry(): self{ $this->app->singleton( SettingsSchemaRegistry::class, fn (): SettingsSchemaRegistry => new SettingsSchemaRegistry(), );
$this->app->singleton( SettingsSchemaBootstrapper::class, fn (): SettingsSchemaBootstrapper => new SettingsSchemaBootstrapper( resolve(SettingsSchemaRegistry::class), ), );
return $this;}Admin Package
Section titled “Admin Package”The Admin package registers core and admin schemas:
// In AdminServiceProvider::bootInstalledPackage()private function registerSettingsSchemas(): self{ $registry = resolve(SettingsSchemaRegistry::class);
$registry->registerSettingsClass('core', CoreSettings::class); $registry->register('core', CoreSettingsSchema::class);
$registry->registerSettingsClass('admin', AdminSettings::class); $registry->register('admin', AdminSettingsSchema::class);
return $this;}Frontend Package
Section titled “Frontend Package”The Frontend package registers its own schemas:
// In FrontendServiceProvider::bootInstalledPackage()private function registerSettingsSchemas(): self{ $registry = resolve(SettingsSchemaRegistry::class);
$registry->registerSettingsClass('frontend', FrontendSettings::class); $registry->register('frontend', FrontendSettingsSchema::class);
return $this;}Custom Plugin Package
Section titled “Custom Plugin Package”Your plugin can add its own settings or extend existing ones:
// In YourPluginServiceProvider::bootInstalledPackage()private function registerSettingsSchemas(): self{ $registry = resolve(SettingsSchemaRegistry::class);
// Add your own settings group $registry->registerSettingsClass('myplugin', MyPluginSettings::class); $registry->register('myplugin', MyPluginSettingsSchema::class);
// Or extend an existing group $registry->register('admin', MyPluginAdminExtensionSchema::class, 'myplugin_admin');
return $this;}Settings Page Integration
Section titled “Settings Page Integration”First-party settings groups use the registry through the admin SettingsPage tabs. Package settings pages use the same registry through AbstractPackageSettingsPage:
- Page Ownership - The package registers a concrete page with
CapellAdmin::registerExtensionPage() - Icon & Label - Derived from package metadata when available
- Schema Composition - All schemas for the page’s group are merged together
- Form Hydration - The registered settings class populates form values
- Saving - Data is saved to the registered settings class for that group
Configurators
Section titled “Configurators”Configurators are package-owned classes that build Filament schemas for configurable admin surfaces such as page types, widget types, layout containers, sites, languages, and themes. They implement Capell\Admin\Contracts\ConfiguratorInterface, expose a stable getKey(), provide a getSort() order, and return a configured Schema from configure(Schema $schema, ?ConfiguratorContextData $context = null).
Use a configurator when the extension point is an existing admin editing surface. Use a settings schema when the package owns saved package configuration. Keep both focused on assembling Filament components; move business operations into Actions and keep user-facing text in translation files.
Configurator schemas follow the same field presentation rules as settings schemas: fields belong in contained Section components. contained(false) is only appropriate for chrome-free display inside another panel, not for standalone labels, helper text, or inputs.
Testing
Section titled “Testing”Unit Testing the Registry
Section titled “Unit Testing the Registry”use Capell\Core\Support\Settings\SettingsSchemaRegistry;
it('registers a schema for a group') ->tap(function (): void { $registry = new SettingsSchemaRegistry(); $registry->register('admin', MockAdminSchema::class);
expect($registry->hasGroup('admin'))->toBeTrue(); });Feature Testing Settings Schemas
Section titled “Feature Testing Settings Schemas”use Capell\Core\Support\Settings\SettingsSchemaRegistry;use Capell\YourPackage\Filament\Pages\YourPackageSettingsPage;
it('displays settings schema in package settings page') ->actingAs($this->createAdmin()) ->get(YourPackageSettingsPage::getUrl()) ->assertSuccessful();
it('registers schema in registry') ->tap(function (): void { $registry = resolve(SettingsSchemaRegistry::class);
expect($registry->hasGroup('yourgroup'))->toBeTrue(); expect($registry->getSettingsClass('yourgroup')) ->toBe(YourSettings::class); });Best Practices
Section titled “Best Practices”1. Use Descriptive Group Names
Section titled “1. Use Descriptive Group Names”Choose clear, unique group names:
- ✅
'yourpackage','ai-orchestrator','ecommerce' - ❌
'settings','config','options'
2. Register Schemas in Boot
Section titled “2. Register Schemas in Boot”Always register schemas in bootInstalledPackage() or similar boot methods, not in register().
3. One Settings Class Per Group
Section titled “3. One Settings Class Per Group”Each group should have exactly one settings class (for form hydration/saving), but can have multiple schema classes (for form building).
4. Explicit Keys for Important Schemas
Section titled “4. Explicit Keys for Important Schemas”When registering schemas that others might want to replace/remove, use explicit keys:
$registry->register('admin', ImportantSchema::class, 'important_feature');5. Document Extension Points
Section titled “5. Document Extension Points”If your package allows schema extensions, document which groups and keys are available for replacement.
6. Validate Schema Classes
Section titled “6. Validate Schema Classes”The registry validates that all schemas implement HasSchema. Ensure your schemas are properly typed:
class MySchema implements HasSchema{ public static function make(Schema $schema): array { return [ // Form components ]; }}Troubleshooting
Section titled “Troubleshooting”Schema Not Appearing
Section titled “Schema Not Appearing”Problem: Your schema is registered but doesn’t appear in the package settings page.
Solutions:
- Ensure the group name matches exactly
- Check that
registerSettingsClass()was called - Verify the schema class implements
HasSchema - Clear cache:
php artisan cache:clear
InvalidArgumentException
Section titled “InvalidArgumentException”Problem: Exception thrown when registering or replacing schemas.
Solutions:
- Check that the schema class exists and is autoloaded
- Verify the schema implements
HasSchema - For
replace(), ensure the key exists before replacing
Settings Not Saving
Section titled “Settings Not Saving”Problem: Changes in settings page don’t persist.
Solutions:
- Ensure
registerSettingsClass()was called for the group - Check that the settings class extends
Spatie\LaravelSettings\Settings - Verify database migrations have run for the settings table
- Check
group()method returns the correct group name
Performance Considerations
Section titled “Performance Considerations”Registry Instantiation
Section titled “Registry Instantiation”The registry is instantiated fresh on each request - no caching is needed for package settings page use cases.
Schema Loading
Section titled “Schema Loading”Schemas are loaded and merged when building the form. For many schemas (>20), consider:
- Lazy loading schema components
- Conditionally hiding schemas based on package installation status
Extension Callbacks
Section titled “Extension Callbacks”Extension callbacks via the bootstrapper run on every request. Keep them lightweight:
- ✅ Simple schema registration
- ❌ Heavy computation or database queries