BuilderPage
BuilderPage is the Primix base class for admin pages that expose a structured configuration UI — a form, a table, or any static layout — backed by a single Eloquent model or a raw configuration store. It composes the Primix HasForm, HasNavigation, and InteractsWithRecord traits so you can focus on declaring the shape of the page rather than wiring lifecycle hooks.
Heads-up: If you need a full drag-and-drop visual builder canvas (the Tagixo page editor), extend
PrimixVisualBuilderPageinstead. The signal is simple: if the page needs a canvas with module slots, use the visual builder; if it needs a form or a table, useBuilderPage.
When to use BuilderPage
| Scenario | Use |
|---|---|
| Editing a single record that always exists (site settings, homepage meta) | BuilderPage |
| Listing and editing multiple records | ListPage + EditPage (or a custom BuilderPage with a table component) |
| A page that lets the user arrange modules visually | PrimixVisualBuilderPage |
| A dashboard or read-only summary with no form | BuilderPage (implement form() to return an empty schema) |
Extending BuilderPage
Place the class anywhere under app/Primix/ — Primix auto-discovers pages in directories registered with AdminPanelProvider::discoverPages(). The conventional location is:
app/Primix/Pages/MyPage.php
namespace App\Primix\Pages;
use Primix\Pages\BuilderPage;
class MyPage extends BuilderPage
{
// implement required methods
}
You do not need to use any traits manually; BuilderPage already composes them.
Required methods
form(Schema $schema): Schema
Declares the form schema rendered on the page. Return the $schema object with your fields attached.
| Parameter | Type | Description |
|---|---|---|
$schema |
Primix\Forms\Schema |
Empty schema instance injected by the framework |
use Primix\Forms\Schema;
use Primix\Forms\Fields\TextInput;
use Primix\Forms\Fields\Toggle;
public function form(Schema $schema): Schema
{
return $schema->fields([
TextInput::make('site_name')->required(),
Toggle::make('maintenance_mode'),
]);
}
loadData(): array
Called once when the page mounts. Return an associative array whose keys match the field names declared in form().
| Return key | Expected value |
|---|---|
| Any field name | The current persisted value for that field |
public function loadData(): array
{
$settings = \App\Models\SiteSettings::first();
return [
'site_name' => $settings?->site_name,
'maintenance_mode' => $settings?->maintenance_mode ?? false,
];
}
saveStructure(array $data): void
Called when the user submits the form. Receives the validated form data. Persist it however your domain requires.
| Parameter | Type | Description |
|---|---|---|
$data |
array |
Validated field values, keyed by field name |
Warning: Do not swallow exceptions silently inside
saveStructure. If you catch\Exceptionfor logging, always re-throw (or throw aPrimix\Exceptions\SaveException) so the framework can display the error banner to the user. Silent failures are a common footgun — the user sees the success toast while the data was never written.
public function saveStructure(array $data): void
{
\App\Models\SiteSettings::updateOrCreate(
[],
[
'site_name' => $data['site_name'],
'maintenance_mode' => $data['maintenance_mode'],
]
);
}
Artisan scaffold
Generate a skeleton with the correct namespace and method stubs:
php artisan primix:make-page MyPage --context=page
Generated output (abbreviated):
namespace App\Primix\Pages;
use Primix\Forms\Schema;
use Primix\Pages\BuilderPage;
class MyPage extends BuilderPage
{
protected static ?string $navigationLabel = 'My Page';
protected static ?string $navigationIcon = 'heroicon-o-document';
protected static ?int $navigationSort = 10;
public function form(Schema $schema): Schema
{
return $schema->fields([
//
]);
}
public function loadData(): array
{
return [];
}
public function saveStructure(array $data): void
{
//
}
}
AdminPanelProvider registration
Auto-discovery (recommended)
// app/Providers/AdminPanelProvider.php
$panel->discoverPages(
in: app_path('Primix/Pages'),
for: 'App\\Primix\\Pages',
);
All classes in the directory that extend a Primix page base are registered automatically. No further action needed when you add a new page.
Manual registration
$panel->pages([
\App\Primix\Pages\MyPage::class,
]);
Use manual registration when you need fine-grained control over which pages appear in a given panel.
Navigation configuration
Static properties set the default navigation metadata:
| Property | Type | Default | Description |
|---|---|---|---|
$navigationLabel |
?string |
Class name (humanised) | Sidebar label |
$navigationIcon |
?string |
null |
Heroicon identifier |
$navigationGroup |
?string |
null |
Group heading in the sidebar |
$navigationSort |
?int |
null |
Sort order within the group |
Override with methods for i18n or dynamic values:
public static function getNavigationLabel(): string
{
return __('admin.pages.my_page');
}
public static function getNavigationGroup(): ?string
{
return __('admin.groups.settings');
}
Authorization
Override canAccess() to restrict who can open the page. The method is called before the page renders.
public static function canAccess(): bool
{
return auth()->user()?->can('manage-settings') ?? false;
}
Three common patterns:
Role check
public static function canAccess(): bool
{
return auth()->user()?->hasRole('admin') ?? false;
}
Gate
public static function canAccess(): bool
{
return \Illuminate\Support\Facades\Gate::allows('view-settings-page');
}
Super-admin flag
public static function canAccess(): bool
{
return auth()->user()?->is_super_admin ?? false;
}
Performance note:
canAccess()is evaluated on every request that touches the sidebar (to determine whether to render the nav item). Keep it cheap — avoid N+1 queries or external HTTP calls. Cache aggressively if the check is expensive.
Preview URL
By default, BuilderPage hides the preview button. To enable it, override getPreviewUrl():
public function getPreviewUrl(): ?string
{
return route('home');
}
For unpublished pages where the public route returns 404, generate a signed URL:
public function getPreviewUrl(): ?string
{
$record = \App\Models\Page::find($this->record?->id);
if (! $record) {
return null;
}
return \Illuminate\Support\Facades\URL::signedRoute(
'preview.page',
['page' => $record->slug],
now()->addMinutes(30),
);
}
Guard note: Ensure your preview route verifies the signature with the
signedmiddleware and checks that the authenticated user is an admin, otherwise the signed URL is a public bypass for unpublished content.
Complete example
Model
// app/Models/MarketingPage.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MarketingPage extends Model
{
protected $table = 'marketing_page';
public $timestamps = true;
protected $casts = [
'hero_visible' => 'boolean',
];
}
BuilderPage
// app/Primix/Pages/HomepageBuilder.php
namespace App\Primix\Pages;
use App\Models\MarketingPage;
use Primix\Forms\Fields\TextInput;
use Primix\Forms\Fields\Textarea;
use Primix\Forms\Fields\Toggle;
use Primix\Forms\Schema;
use Primix\Pages\BuilderPage;
class HomepageBuilder extends BuilderPage
{
protected static ?string $navigationLabel = 'Homepage';
protected static ?string $navigationIcon = 'heroicon-o-home';
protected static ?string $navigationGroup = 'Content';
protected static ?int $navigationSort = 1;
public static function canAccess(): bool
{
return auth()->user()?->hasRole('editor') ?? false;
}
public function form(Schema $schema): Schema
{
return $schema->fields([
TextInput::make('hero_title')
->label('Hero title')
->required()
->maxLength(120),
Textarea::make('hero_subtitle')
->label('Hero subtitle')
->rows(3),
Toggle::make('hero_visible')
->label('Show hero section')
->default(true),
]);
}
public function loadData(): array
{
$page = MarketingPage::first();
return [
'hero_title' => $page?->hero_title ?? '',
'hero_subtitle' => $page?->hero_subtitle ?? '',
'hero_visible' => $page?->hero_visible ?? true,
];
}
public function saveStructure(array $data): void
{
MarketingPage::updateOrCreate([], [
'hero_title' => $data['hero_title'],
'hero_subtitle' => $data['hero_subtitle'],
'hero_visible' => $data['hero_visible'],
]);
// Bust the public page cache so visitors see the change immediately.
\Illuminate\Support\Facades\Cache::forget('public.homepage');
}
public function getPreviewUrl(): ?string
{
return route('home');
}
}
After creating the page, clear Octane's opcode cache:
php artisan optimize:clear
php artisan octane:reload
First load
- Primix calls
HomepageBuilder::canAccess()— renders the nav item only for editors. - On navigation,
loadData()fetches the singleMarketingPagerow and populates the form. - The user edits fields and clicks Save.
saveStructure()upserts the row, then clears the homepage cache key.
Subsequent reloads
The form mounts fresh on each page load (Livewire full-page component). loadData() is called again, so the form always reflects the database state — there is no client-side stale state to worry about.
See also
- Custom form modules
- Filament SDK integration
- Primix SDK integration
- Standalone page builder integration