PHPackages                             banulakwin/laravel-page-builder - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. banulakwin/laravel-page-builder

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

banulakwin/laravel-page-builder
===============================

Config-driven CMS-like page content with dynamic fields, repeaters, and nested structures.

v1.0.1(3w ago)0121MITPHPPHP ^8.2CI passing

Since May 17Pushed 3w agoCompare

[ Source](https://github.com/banulalakwindu/laravel-page-builder)[ Packagist](https://packagist.org/packages/banulakwin/laravel-page-builder)[ Docs](https://github.com/banulalakwindu/laravel-page-builder)[ RSS](/packages/banulakwin-laravel-page-builder/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (10)Versions (3)Used By (1)

Laravel Page Builder (`banulakwin/laravel-page-builder`)
========================================================

[](#laravel-page-builder-banulakwinlaravel-page-builder)

[![Latest Version on Packagist](https://camo.githubusercontent.com/5c0e2f485fa0241435e3dbc27084d6f75e4418d5336c57685dc7ab2314430728/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f62616e756c616b77696e2f6c61726176656c2d706167652d6275696c6465722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/banulakwin/laravel-page-builder)[![Tests](https://github.com/banulalakwindu/laravel-page-builder/actions/workflows/tests.yml/badge.svg)](https://github.com/banulalakwindu/laravel-page-builder/actions/workflows/tests.yml)[![Total Downloads](https://camo.githubusercontent.com/378f9684c8467b582530ad8784acd4a427e7461f699a016a11de84915aafe0ea/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f62616e756c616b77696e2f6c61726176656c2d706167652d6275696c6465722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/banulakwin/laravel-page-builder)[![License](https://camo.githubusercontent.com/942e017bf0672002dd32a857c95d66f28c5900ab541838c6c664442516309c8a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e7376673f7374796c653d666c61742d737175617265)](LICENSE)

Portable Laravel package: a **CMS-style** layer where page **structure and defaults** are defined in PHP (per-page files under `app/Cms/Pages` by default, with optional fallback `config/pages.php`), synced into **`page_contents`**, and read at runtime **only from the database** via `PageService` (never from disk on web requests).

Supports **dynamic field types**, **file uploads (images)** on a configurable disk, **repeaters** and **groups** with **unlimited nesting** via a recursive field resolver.

---

Requirements
------------

[](#requirements)

- PHP `^8.2`
- Laravel `illuminate/*` `^11.0|^12.0|^13.0` (see `composer.json` for split packages)

---

Installation
------------

[](#installation)

Registration is automatic via Composer `extra.laravel.providers`:

- `Banulakwin\PageBuilder\PageBuilderServiceProvider`

Optional facade alias (see **Resolving `PageService`**): `Page` → `Banulakwin\PageBuilder\Facades\Page`.

### Configure, migrate, sync

[](#configure-migrate-sync)

```
php artisan vendor:publish --tag=page-config
php artisan vendor:publish --tag=page-builder-config
php artisan migrate
php artisan page:sync
```

TagCopies`page-config``config/pages.php` — **optional fallback** when no files exist in `pages_path``page-builder-config``config/page-builder.php` — `pages_path`, optional registry cache, upload disk, migrations`page-builder-migrations`Package migration files → `database/migrations/` (optional; see **Database migrations** below)### Database migrations (same pattern as `banulakwin/laravel-seo-engine`)

[](#database-migrations-same-pattern-as-banulakwinlaravel-seo-engine)

By default, migrations are registered with **`loadMigrationsFrom()`** when **`config('page-builder.register_migrations')`** is **`true`** (default). Run **`php artisan migrate`** — no publish step is required.

To **own** migrations in the app: publish with **`php artisan vendor:publish --tag=page-builder-migrations`**, then set **`register_migrations` =&gt; false** in **`config/page-builder.php`** (or **`PAGE_BUILDER_REGISTER_MIGRATIONS=false`** in `.env`) so Laravel does not load the same files twice.

---

Page definitions (`PageRegistry`)
---------------------------------

[](#page-definitions-pageregistry)

1. **Primary:** one PHP file per page under **`config('page-builder.pages_path')`**, default **`app/Cms/Pages/{pageKey}.php`**. Filename (without `.php`) = page key. Each file **must** return:

```
return [
    'sections' => [
        'hero' => [
            'fields' => [
                'title' => ['type' => 'text', 'default' => 'Welcome'],
                // …
            ],
        ],
    ],
];
```

2. **`Banulakwin\PageBuilder\Support\PageRegistry::all()`** loads and validates all files. If the directory is missing or empty, it returns **`config('pages.definitions', [])`** (or legacy top-level page keys in `config/pages.php`). Publish `page-config` for **`catalog`** (enable/disable) and optional **`definitions`** fallback.

### Page catalog (HTTP on/off)

[](#page-catalog-http-onoff)

`config/pages.php` may define **`catalog`**: each key is a page slug; value is **`['enabled' => true]`** or boolean shorthand. If **`catalog`** is non-empty, only listed keys with **`enabled`** true are treated as available. When **`catalog`** is empty, no gating is applied (backward compatible).

**Portable APIs (all live in the package):**

APIPurpose**`PageService::getPageWhenEnabled($slug)`**Abort then return the same shape as **`getPage()`** (preferred in controllers).**`PageService::getSectionWhenEnabled($slug, $section)`**Abort then return **`getSection()`**.**`page_when_enabled()`** / **`page_section_when_enabled()`**Global helpers wrapping the service.**`PageCatalog::enabled()`** / **`abortUnlessEnabled()`**Check or abort without loading DB rows.**`abort_unless_page_builder_page_enabled()`**Global helper for **`PageCatalog::abortUnlessEnabled()`**.Route middleware **`page-builder:{slug}`**Alias registered by the service provider (override name via **`page-builder.middleware_alias`** / **`PAGE_BUILDER_MIDDLEWARE_ALIAS`**). Optional third segment: HTTP status, e.g. **`page-builder:home:403`**.Use **`getPage()`** / **`getSection()`** only when gating must not run (e.g. Filament, jobs).

### Common sections (shared across pages)

[](#common-sections-shared-across-pages)

`config/pages.php` may define **`common_sections`**: section slugs and fields shared by every page (navbar, footer, etc.).

- On **`php artisan page:sync`**, those fields are stored under a reserved internal page key **`__common`** (not a public route slug).
- **`PageService::getPage($slug)`** merges **`__common`** sections first, then page-specific sections (`array_replace`). A page section with the same slug overrides a common section.
- **`PageService::getSection($slug, $section)`** loads **only** that page slug — it does **not** include `__common`. Use **`getPage()`** or load **`__common`** separately when you need shared content in one section.

For Inertia, pass field metadata without defaults/rules:

```
use Banulakwin\PageBuilder\Support\PageRegistry;

$definitions = PageRegistry::publicFieldMetadataBySection('home');
$commonDefinitions = PageRegistry::publicFieldMetadataBySection('__common');
```

With **`@banulakwin/inertia-page-builder`**, use **`parseCmsSection($cms, 'section-slug', $definitions['section-slug'] ?? [])`** per section.

3. **`php artisan page:sync`** uses `PageRegistry::all()` (after clearing optional registry cache) and `firstOrCreate`s rows — **never overwrites** existing values.

See **[AGENTS.md](./AGENTS.md)** for step-by-step rules when adding pages or sections (AI-friendly).

### `config/pages.php` (merged key: `pages`)

[](#configpagesphp-merged-key-pages)

Optional **fallback** only. Top-level keys are **page names** (e.g. `home`). Each page has `sections`; each section has `fields`.

Each field is an array with at least `type`. Supported types:

TypeBehaviour`text`, `textarea`, `url`Read from request using dot path `{section}.{field}` (and nested paths for repeaters/groups).`image`If `Request::hasFile(path)`, file is stored on the field’s `disk` / `path` when set, otherwise `page-builder.upload_disk` / `page-builder.upload_directory`; otherwise existing path is read from request input.`repeater`Nested `fields` define each row; stored as **JSON** in one DB row for that field key.`group`Nested `fields`; stored as **JSON** object in one DB row.Optional `default` is used by `php artisan page:sync` only when creating a missing row (existing values are never overwritten).

#### Optional attributes on each field

[](#optional-attributes-on-each-field)

All keys below are **optional** (in addition to `type` and usually `default`):

KeyPurpose`label`Human label in admin / metadata for the frontend.`rules`List of Laravel **string** validation rules (e.g. `required`, `image`, `mimes:jpg,png`, `max:2048` in KB for files).`meta`Filament + Inertia hints: `aspect_ratio`, `aspect_ratio_mobile`, `width` / `height`, `width_mobile` / `height_mobile`, `min_height_mobile` / `min_height_desktop`, `max_height_mobile` / `max_height_desktop`, `sizes` (``), `object_position`, `crop`, `image_editor`, etc.`disk`Filesystem disk for `image` uploads (`FieldResolver`, Filament `FileUpload`).`path`Directory on that disk for stored files (relative to disk root); when omitted, global upload directory / admin prefix applies.**Registry helpers** (for Filament CMS admin and Inertia props):

- `PageRegistry::sectionsForPage(string $page)` — raw `sections` for that page.
- `PageRegistry::fieldDefinitionsForPage(string $page)` — alias of `sectionsForPage`.
- `PageRegistry::publicFieldMetadataBySection(string $page)` — per-section field configs with `default` and `rules` stripped for safe client use; keeps `type`, `label`, `meta`, `disk`, `path`, and nested `fields`.

**Rule parsing** for admin / tooling: `Banulakwin\PageBuilder\Support\CmsFieldRules` (`isRequired`, `maxFileKilobytes`, `acceptedMimeTypes`, etc.).

### `config/page-builder.php` (merged key: `page-builder`)

[](#configpage-builderphp-merged-key-page-builder)

Config keyPurpose`register_migrations`Load package migrations via `loadMigrationsFrom()` (default `true`). Set `false` after publishing migrations into the app. Env: `PAGE_BUILDER_REGISTER_MIGRATIONS`.`upload_disk`Filesystem disk name (default `public`). Env: `PAGE_BUILDER_DISK`.`upload_directory`Directory on that disk (default `pages`).`pages_path`Absolute path to page PHP files, or empty string for `app_path('Cms/Pages')`. Env: `PAGE_BUILDER_PAGES_PATH`.`registry_cache_ttl`Seconds to cache `PageRegistry::all()`; `0` disables. Env: `PAGE_BUILDER_REGISTRY_CACHE_TTL`.`registry_cache_key`Cache key when TTL &gt; 0. Env: `PAGE_BUILDER_REGISTRY_CACHE_KEY`.`middleware_alias`Route middleware alias for **`EnsurePageBuilderPageIsEnabled`** (default `page-builder`). Env: `PAGE_BUILDER_MIDDLEWARE_ALIAS`. Set to empty string to skip registration.---

Rules (design constraints)
--------------------------

[](#rules-design-constraints)

- **Never change the DB schema** to add content fields — add them in **`app/Cms/Pages/*.php`** (or fallback `config/pages.php`) and run **`page:sync`** for new keys only.
- **Repeaters** (and nested structures) are stored as **JSON** in `page_contents.value`.
- **Images** store the **relative path** returned by `store()` (e.g. `pages/xxx.jpg` on the `public` disk → URL typically `/storage/pages/xxx.jpg` after `php artisan storage:link`).

---

Database
--------

[](#database)

Table: **`page_contents`**

ColumnNotes`page`Logical page name (`home`, …)`section`Section id (`hero`, …)`key`Field name (`title`, `items`, …)`value``longText`, nullable — scalar string or JSON for complex fields`deleted_at`Nullable timestamp — **soft deletes** (`SoftDeletes` on `PageContent`)Unique index: **`(page, section, key)`** (applies to soft-deleted rows too). **`page:sync`** uses **`withTrashed()->firstOrCreate()`** so it does not insert a duplicate when a trashed row still exists; if the row was trashed, it is \*\*`restore()`\*\*d (defaults are not written over existing values). **`FieldResolver::handleSave`** uses **`withTrashed()->updateOrCreate()`** then **`restore()`** when the record was trashed.

---

Architecture
------------

[](#architecture)

### Model

[](#model)

`Banulakwin\PageBuilder\Models\PageContent` — fillable: `page`, `section`, `key`, `value`; uses **`SoftDeletes`** (`deleted_at`).

### Page service (singleton)

[](#page-service-singleton)

`Banulakwin\PageBuilder\Services\PageService`:

MethodReturns`getPage(string $page)``[section => [key => value]]` — merges **`__common`** then page-specific rows`getSection(string $page, string $section)``[key => value]` for one section on that page only (no **`__common`** merge)`getPageWhenEnabled(string $page)`Same as **`getPage()`** after catalog check`getSectionWhenEnabled(string $page, string $section)`Same as **`getSection()`** after catalog check### Resolving `PageService` (prefer dependency injection)

[](#resolving-pageservice-prefer-dependency-injection)

Avoid `app(PageService::class)` in controllers: it hides dependencies and is harder to test than **constructor** or **method** injection.

**Recommended — method injection (shortest for a single action):**

```
use Banulakwin\PageBuilder\Services\PageService;
use Inertia\Inertia;

public function index(PageService $pageService)
{
    return Inertia::render('Home', [
        'page' => $pageService->getPage('home'),
    ]);
}
```

**Also good — constructor injection (several actions use the service):**

```
use Banulakwin\PageBuilder\Services\PageService;
use Inertia\Inertia;

public function __construct(
    protected PageService $pageService,
) {}

public function index()
{
    return Inertia::render('Home', [
        'page' => $this->pageService->getPage('home'),
    ]);
}
```

**Optional — facade** (nice syntax; hides the dependency, so prefer DI in application code):

```
use Banulakwin\PageBuilder\Facades\Page;
use Inertia\Inertia;

return Inertia::render('Home', [
    'page' => Page::getPage('home'),
]);
```

**Optional — global helper** (`page('home')`) for Blade or one-off calls; use sparingly in large apps.

### Field resolver (singleton)

[](#field-resolver-singleton)

`Banulakwin\PageBuilder\Support\FieldResolver`:

- `handleSave(Request $request, string $page, string $section, array $fields): void`
    Loops top-level `$fields`, runs `processField()` for each, **JSON-encodes** arrays, then `updateOrCreate` on `PageContent` for `(page, section, key)`.
- `processField(Request $request, array $config, string $path): mixed`
    Recursive; request paths use **dot notation** rooted at the section name (e.g. `hero.title`, `hero.items.0.title`).

```
use Banulakwin\PageBuilder\Support\FieldResolver;
use Banulakwin\PageBuilder\Support\PageRegistry;

public function update(Request $request, FieldResolver $fieldResolver)
{
    $pages = PageRegistry::all();
    $fields = (array) ($pages['home']['sections']['hero']['fields'] ?? []);

    $fieldResolver->handleSave($request, 'home', 'hero', $fields);

    return redirect()->back();
}
```

### Sync command

[](#sync-command)

`php artisan page:sync` walks **`PageRegistry::all()`** (file definitions, else `config('pages')`) and uses **`firstOrCreate`** on `(page, section, key)` with encoded defaults. **Does not overwrite** existing rows.

### Helper

[](#helper)

`page(string $page): array` — convenience wrapper around `PageService::getPage()`. Defined in `src/helpers.php` (loaded via Composer `autoload.files`). Prefer injecting `PageService` in controllers; the helper is fine for views or quick calls.

---

Laravel / Inertia usage
-----------------------

[](#laravel--inertia-usage)

### Controller (read)

[](#controller-read)

Use **method or constructor injection** (see **Resolving `PageService`** above). Example with method injection:

```
use Banulakwin\PageBuilder\Services\PageService;
use Inertia\Inertia;

public function index(PageService $pageService)
{
    return Inertia::render('Home', [
        'page' => $pageService->getPage('home'),
    ]);
}
```

### React (Inertia props)

[](#react-inertia-props)

Assume `page` is the prop above (or your app’s `cms` prop). Each **section key** is a slug; under it, each **field key** maps to a string. Repeaters are **JSON strings** in the DB — parse on the client if needed.

```
const hero = page.hero ?? {};

{hero.title}
{hero.image ?  : null}

const items = JSON.parse(page.features?.items ?? '[]');
items.map((item: { title: string }) => …);
```

With **`@banulakwin/inertia-page-builder`**, call **`parseCmsSection(cms, 'hero')`** so `slug` comes from the section key; you do not need `slug` / `name` fields in the PHP definition unless they are real content.

Adjust `/storage/` prefix if you use a different disk or CDN.

---

Example config shape
--------------------

[](#example-config-shape)

After publishing `page-config`, define pages in `config/pages.php`. Minimal example:

```
return [
    'home' => [
        'sections' => [
            'hero' => [
                'fields' => [
                    'title' => ['type' => 'text', 'default' => 'Welcome'],
                    'subtitle' => ['type' => 'textarea', 'default' => ''],
                    'image' => ['type' => 'image', 'default' => null],
                ],
            ],
            'features' => [
                'fields' => [
                    'items' => [
                        'type' => 'repeater',
                        'fields' => [
                            'title' => ['type' => 'text'],
                            'description' => ['type' => 'textarea'],
                        ],
                    ],
                ],
            ],
        ],
    ],
];
```

---

Testing
-------

[](#testing)

```
composer test          # Run PHPUnit
composer pint          # Fix code style
composer phpstan       # Static analysis
composer quality       # Run all (pint + phpstan + test)
```

---

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for details.

---

Contributing
------------

[](#contributing)

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/your-feature`)
3. Run `composer quality` to ensure tests and style pass
4. Commit and push
5. Open a pull request

---

Package layout (reference)
--------------------------

[](#package-layout-reference)

```
config/
  page-builder.php
  pages.php
database/migrations/
  *_create_page_contents_table.php
src/
  Console/SyncPageContent.php
  Facades/Page.php
  Models/PageContent.php
  Services/PageService.php
  Support/FieldResolver.php
  PageBuilderServiceProvider.php
  helpers.php

```

---

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance95

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

23d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/64958389?v=4)[Banula Lakwindu](/maintainers/banulalakwindu)[@banulalakwindu](https://github.com/banulalakwindu)

---

Top Contributors

[![banulalakwindu](https://avatars.githubusercontent.com/u/64958389?v=4)](https://github.com/banulalakwindu "banulalakwindu (2 commits)")

---

Tags

laravelcmscontent managementdynamic-fieldspage builderRepeater

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/banulakwin-laravel-page-builder/health.svg)

```
[![Health](https://phpackages.com/badges/banulakwin-laravel-page-builder/health.svg)](https://phpackages.com/packages/banulakwin-laravel-page-builder)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.4k](/packages/larastan-larastan)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76318.2M110](/packages/laravel-mcp)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k28.4M134](/packages/laravel-cashier)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)[api-platform/laravel

API Platform support for Laravel

59156.3k10](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
