PHPackages                             ahmed-aliraqi/ui-manager - 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. ahmed-aliraqi/ui-manager

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

ahmed-aliraqi/ui-manager
========================

A powerful, class-driven UI management system for Laravel — manage layouts, pages, sections, and fields from a modern SPA dashboard.

v2.0.1(2mo ago)014MITPHPPHP ^8.2

Since Apr 22Pushed 2mo agoCompare

[ Source](https://github.com/ahmed-aliraqi/ui-manager)[ Packagist](https://packagist.org/packages/ahmed-aliraqi/ui-manager)[ RSS](/packages/ahmed-aliraqi-ui-manager/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (1)Dependencies (7)Versions (7)Used By (0)

UI Manager — Laravel Package
============================

[](#ui-manager--laravel-package)

A production-ready, class-driven UI management system for Laravel. Define your UI structure in PHP, embed the panel into any existing dashboard, and render content in Blade with a clean read-only API.

---

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

[](#requirements)

- PHP 8.2+
- Laravel 11+
- [Spatie Media Library v11](https://spatie.be/docs/laravel-medialibrary) (bundled migration included — no separate install needed)

---

Table of Contents
-----------------

[](#table-of-contents)

1. [Installation](#installation)
2. [Configuration](#configuration)
3. [Creating Pages](#creating-pages)
4. [Creating Sections](#creating-sections)
5. [Field Types](#field-types)
6. [Field Validation](#field-validation)
7. [Repeatable Sections](#repeatable-sections)
    - [List Label Field](#list-label-field)
    - [Repeatable Section Defaults](#repeatable-section-defaults)
8. [Multi-language / Translatable Fields](#multi-language--translatable-fields)
9. [Variables System](#variables-system)
10. [Blade Usage](#blade-usage)
11. [Embedded Panel](#embedded-panel)
12. [Artisan Commands](#artisan-commands)
13. [Extending the Package](#extending-the-package)
14. [Testing](#testing)
15. [Architecture Overview](#architecture-overview)

---

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

[](#installation)

```
composer require ahmed-aliraqi/ui-manager
```

```
php artisan ui-manager:install
```

This single command publishes the config, copies the pre-built panel assets to `public/vendor/ui-manager/`, and runs the package migrations (including Spatie Media Library's `media` table).

Use `--force` to overwrite previously published files:

```
php artisan ui-manager:install --force
```

### Manual publishing (optional)

[](#manual-publishing-optional)

```
php artisan vendor:publish --tag=ui-manager-config      # config/ui-manager.php
php artisan vendor:publish --tag=ui-manager-assets      # public/vendor/ui-manager/
php artisan vendor:publish --tag=ui-manager-migrations  # customise migrations
```

---

Configuration
-------------

[](#configuration)

After installation, `config/ui-manager.php` is available in your application:

```
return [

    'locales'        => ['en'],
    'default_locale' => 'en',

    'routes' => [
        'api_prefix'     => 'ui-manager/api',
        'api_middleware' => ['web'],
    ],

    // Paths scanned at boot for Page and Section classes.
    'discovery' => [
        'pages_path'         => 'app/Ui/Pages',
        'sections_path'      => 'app/Ui/Sections',
        'pages_namespace'    => 'App\\Ui\\Pages',
        'sections_namespace' => 'App\\Ui\\Sections',
    ],

    'variables' => [
        'delimiter_start' => '%',
        'delimiter_end'   => '%',
        'max_depth'       => 10,
    ],

    'media' => [
        'disk' => 'public',
    ],

    'cache' => [
        'enabled' => env('UI_MANAGER_CACHE', true),
        'ttl'     => 3600,      // seconds
        'prefix'  => 'ui_manager_',
    ],
];
```

### Protecting the API

[](#protecting-the-api)

Add your auth middleware to the API route config:

```
'routes' => [
    'api_middleware' => ['web', 'auth'],
],
```

---

Creating Pages
--------------

[](#creating-pages)

Pages are top-level groupings (e.g. Home, About, Contact) shown as sidebar items.

```
php artisan make:ui-page About
php artisan make:ui-page Marketing/Landing  # sub-namespace
```

Or manually:

```
// app/Ui/Pages/About.php
namespace App\Ui\Pages;

use AhmedAliraqi\UiManager\Core\Page;

class About extends Page
{
    protected string $name  = 'about';
    protected int    $order = 2;

    public function getDisplayName(): string
    {
        return __('About Us');
    }
}
```

PropertyTypeDefaultDescription`$name`string—Unique slug used in routes and cache`$visible`bool`true`Show in the panel sidebar`$order`int`0`Sidebar sort position---

Creating Sections
-----------------

[](#creating-sections)

Sections belong to a page and hold a typed set of fields.

```
php artisan make:ui-section Banner --page="App\Ui\Pages\Home"
php artisan make:ui-section SocialLinks --page="App\Ui\Pages\Home" --repeatable
```

Or manually:

```
// app/Ui/Sections/Banner.php
namespace App\Ui\Sections;

use AhmedAliraqi\UiManager\Core\Section;
use AhmedAliraqi\UiManager\Fields\Field;
use App\Ui\Pages\Home;

class Banner extends Section
{
    protected string $layout = 'default';
    protected string $name   = 'banner';
    protected string $page   = Home::class;

    public function fields(): array
    {
        return [
            Field::text('title')
                ->default('Welcome to our site')
                ->rules(['required', 'string', 'max:255']),
            Field::editor('description'),
            Field::image('background'),
        ];
    }
}
```

> **Field defaults** are defined per-field with `->default()`. For repeatable sections, override the section-level `default()` method to seed initial items (see [Repeatable Section Defaults](#repeatable-section-defaults)).

PropertyTypeDefaultDescription`$name`string—Unique slug within the page`$page`string—Fully-qualified Page class or slug`$layout`string`'default'`Layout name`$visible`bool`true`Show in the panel`$order`int`0`Sort position in page tabs`$label`string`''`Override the auto-generated tab label### Multiple layouts for the same section name

[](#multiple-layouts-for-the-same-section-name)

Give two sections the same `$name` but different `$layout` values — they are stored as separate DB records and cached independently:

```
// app/Ui/Sections/HeroHomepage.php
class HeroHomepage extends Section
{
    protected string $name   = 'hero';
    protected string $layout = 'homepage';
    protected string $page   = Home::class;

    public function fields(): array
    {
        return [Field::text('title')->default('Welcome home')];
    }
}

// app/Ui/Sections/HeroLanding.php
class HeroLanding extends Section
{
    protected string $name   = 'hero';
    protected string $layout = 'landing';
    protected string $page   = Home::class;

    public function fields(): array
    {
        return [Field::text('title')->default('Special offer')];
    }
}
```

**Blade — select variant by layout:**

```
ui('hero', layout: 'homepage')->field('title')   // → homepage variant
ui('hero', layout: 'landing')->field('title')    // → landing variant
ui()->section('hero', 'homepage')->field('title') // equivalent long-form
```

Without a layout argument, the first registered variant is returned (same as before for single-layout sections).

**API — pass `?layout=` query param:**

The panel and API calls accept an optional `?layout=` query param to target a specific variant:

```
GET  /ui-manager/api/pages/home/sections/hero?layout=homepage
PUT  /ui-manager/api/pages/home/sections/hero?layout=landing
POST /ui-manager/api/pages/home/sections/hero/items?layout=homepage

```

**Cache invalidation:**

```
app(UiManager::class)->flushCache('home', 'hero', 'homepage'); // flush one variant
app(UiManager::class)->flushCache('home', 'hero', 'landing');  // flush the other
```

---

### Registering sections outside `app/Ui/`

[](#registering-sections-outside-appui)

Auto-discovery scans `app/Ui/Pages` and `app/Ui/Sections` by default. To register classes from any other location, call the registries in a service provider:

```
use AhmedAliraqi\UiManager\Services\PageRegistry;
use AhmedAliraqi\UiManager\Services\SectionRegistry;

public function boot(PageRegistry $pages, SectionRegistry $sections): void
{
    $pages->registerClass(\App\Modules\Blog\Pages\BlogPage::class);
    $sections->registerClass(\App\Modules\Blog\Sections\PostListSection::class);
}
```

---

Field Types
-----------

[](#field-types)

All fields are created via the `Field` factory with a fluent builder API.

### Common builder methods (all types)

[](#common-builder-methods-all-types)

```
Field::text('name')
    ->label('Display Label')          // defaults to humanised field name
    ->help('Shown below the input')
    ->rules(['required', 'string'])
    ->required()      // prepends 'required' to the rules array
    ->nullable()      // appends 'nullable' to the rules array
    ->default($value)
    ->translatable()  // enable multi-locale input (see below)
    ->hasVariable()   // export this field as a variable (e.g. %section.name%) — see Variables
```

---

### Text

[](#text)

```
Field::text('title')
    ->label('Page Title')
    ->default('My Title')
    ->rules(['required', 'string', 'max:255'])
```

**Stored as:** plain string.

---

### Textarea

[](#textarea)

```
Field::textarea('excerpt')
    ->rules(['nullable', 'string', 'max:500'])
```

**Stored as:** plain string (multi-line).

---

### Rich Editor

[](#rich-editor)

```
Field::editor('body')
```

**Stored as:** HTML string. Render unescaped: `{!! ui('section')->field('body') !!}`.

---

### Select

[](#select)

```
Field::select('status')
    ->options([
        'draft'     => 'Draft',
        'published' => 'Published',
        'archived'  => 'Archived',
    ])
    ->default('draft')

// Multiple selection + search:
Field::select('tags')
    ->options(['php' => 'PHP', 'js' => 'JavaScript'])
    ->multiple()
    ->searchable()
```

**Stored as:** option key string (or array of keys when `->multiple()`).

**Access the human label in Blade:**

```
$field = ui()->section('post')->field('status');
echo $field->getString();  // "published"  (the stored key)
echo $field->label();      // "Published"  (the human label)
```

Use `->returnLabel()` on the field definition to make `getString()` return the label automatically.

---

### Image

[](#image)

```
Field::image('hero')
    ->accept(['image/jpeg', 'image/png', 'image/webp'])
    ->maxSize(2048)            // KB
    ->dimensions(1920, 1080)   // recommended width × height (informational)
    ->default('https://example.com/placeholder.jpg')  // URL default
```

**Stored as:** `{ "id": 42, "url": "...", "filename": "hero.jpg" }` (Spatie Media Library record). URL defaults are returned as-is when no DB record exists.

**Access:**

```
ui()->section('hero')->field('image')->getUrl()      // file URL
ui()->section('hero')->field('image')->getString()   // '' (not a string field)
```

---

### File

[](#file)

```
Field::file('cv')
    ->accept(['application/pdf', 'application/msword'])
    ->maxSize(10240)   // KB
    ->multiple(false)
```

**Stored as:** same `{ id, url, filename }` shape as Image.

---

### Color

[](#color)

```
Field::color('brand_color')
    ->default('#3b82f6')

Field::color('overlay')
    ->alpha()   // enables opacity slider (stores rgba)
```

**Stored as:** hex/rgb/rgba string.

---

### Date

[](#date)

```
Field::date('launch_date')
    ->default('2025-01-01')
    ->min('2024-01-01')
    ->max('2030-12-31')
```

**Stored as:** `YYYY-MM-DD` string.

---

### Time

[](#time)

```
Field::time('open_at')->default('09:00')
```

**Stored as:** `HH:MM` string.

---

### Datetime

[](#datetime)

```
Field::datetime('published_at')
    ->min('2024-01-01T08:00')
    ->max('2030-12-31T18:00')
```

**Stored as:** ISO datetime string.

---

### Date Range

[](#date-range)

```
Field::dateRange('sale_period')
    ->defaultRange('2025-06-01', '2025-06-30')
```

**Stored as:** `{ "start": "2025-06-01", "end": "2025-06-30" }`.

---

### URL

[](#url)

```
Field::url('website')->default('https://example.com')
```

`url` validation rule is automatically appended. **Stored as:** URL string.

---

### Price

[](#price)

```
Field::price('ticket_price')
    ->currency('USD')
    ->decimals(2)
    ->currencies(['USD', 'EUR', 'GBP'])  // restricts the currency selector
```

**Stored as:** `{ "amount": "49.99", "currency": "USD" }`.

**Access:**

```
$field = ui()->section('event')->field('ticket_price');
echo $field->amount();    // 49.99 (float)
echo $field->currency();  // "USD"
```

---

Field Validation
----------------

[](#field-validation)

Every field's `->rules()` definition is enforced server-side on every save. You do not need to write any additional validation logic.

```
public function fields(): array
{
    return [
        Field::text('heading')->rules(['required', 'max:100']),
        Field::text('email')->rules(['required', 'email']),
        Field::url('website'),       // automatically has 'url' rule
        Field::textarea('bio'),      // no rules — any value accepted
    ];
}
```

`SaveSectionRequest` dynamically resolves the section definition from the route and applies each field's rules as `fields.fieldName => [...]`.

### Translatable field validation

[](#translatable-field-validation)

When a field is `->translatable()`, rules are applied per-locale:

```
Field::text('title')
    ->translatable()
    ->rules(['required', 'max:200'])
```

This generates rules `fields.title.en` and `fields.title.ar` (for every locale in `config('ui-manager.locales')`).

### Inline error display

[](#inline-error-display)

The panel automatically shows field-level 422 validation errors below each input — both in `SectionForm` and `RepeatableItemForm`.

---

Repeatable Sections
-------------------

[](#repeatable-sections)

Add `implements Repeatable` to make a section store an ordered list of items instead of a single record. Use it for navigation links, team members, FAQs, testimonials, etc.

```
use AhmedAliraqi\UiManager\Contracts\Repeatable;

class SocialLinks extends Section implements Repeatable
{
    protected string $name = 'social';
    protected string $page = Home::class;

    public function fields(): array
    {
        return [
            Field::text('platform'),
            Field::url('url'),
            Field::image('icon'),
        ];
    }
}
```

The panel automatically switches to full CRUD mode with drag-and-drop reordering.

**Blade:**

```
@foreach(ui()->section('social') as $item)

        {{ $item->field('platform') }}

@endforeach
```

> **Note:** Variable placeholders (`%key%`) are expanded inside repeatable items just like in regular sections.

### List Label Field

[](#list-label-field)

By default the panel shows the first non-empty string value as the label in each item row. Set `$listField` to pin a specific field:

```
class SocialLinks extends Section implements Repeatable
{
    protected string $name      = 'social';
    protected string $page      = Home::class;
    protected string $listField = 'platform';   // shown in the collapsed row header

    public function fields(): array
    {
        return [
            Field::text('platform'),
            Field::url('url'),
            Field::image('icon'),
        ];
    }
}
```

Works with translatable fields too — the panel picks the value for the active page locale.

### Repeatable Section Defaults

[](#repeatable-section-defaults)

Override `default()` on a repeatable section to pre-seed items shown when no DB data exists yet. Once the user saves any items to the database, the `default()` result is ignored.

```
class SocialLinks extends Section implements Repeatable
{
    protected string $name = 'social';
    protected string $page = Home::class;

    public function fields(): array
    {
        return [
            Field::text('platform'),
            Field::url('url'),
        ];
    }

    public function default(): array
    {
        return [
            ['platform' => 'Facebook', 'url' => 'https://facebook.com'],
            ['platform' => 'Twitter',  'url' => 'https://twitter.com'],
        ];
    }
}
```

For sections with **translatable fields**, use locale-keyed arrays inside each item:

```
public function default(): array
{
    return [
        ['description' => ['ar' => 'وصف عربي', 'en' => 'English description']],
        ['description' => ['ar' => 'عنصر ثاني', 'en' => 'Second item']],
    ];
}
```

The `make:ui-section --repeatable` command automatically generates a stub `default()` method with example items so you can customise it right away.

---

Multi-language / Translatable Fields
------------------------------------

[](#multi-language--translatable-fields)

### 1 — Configure locales

[](#1--configure-locales)

```
// config/ui-manager.php
'locales'        => ['en', 'ar'],
'default_locale' => 'en',
```

### 2 — Mark fields as translatable

[](#2--mark-fields-as-translatable)

```
public function fields(): array
{
    return [
        Field::text('title')->translatable()->label('Page Title'),
        Field::textarea('body')->translatable(),
        Field::text('cta')->label('Button text'),  // NOT translatable
    ];
}
```

### Locale-keyed defaults

[](#locale-keyed-defaults)

Pass a locale-keyed array to `->default()` to pre-fill each locale independently:

```
Field::text('title')
    ->translatable()
    ->default(['en' => 'Welcome', 'ar' => 'أهلاً'])

Field::textarea('bio')
    ->translatable()
    ->default(['en' => 'About us', 'ar' => 'من نحن'])
```

A plain string default is also valid — it is applied to the `default_locale` only, leaving other locales empty:

```
Field::text('title')
    ->translatable()
    ->default('Welcome')   // only pre-fills 'en' (the default_locale)
```

### 3 — Stored format

[](#3--stored-format)

```
{
  "title": { "en": "Welcome", "ar": "أهلاً" },
  "body":  { "en": "Hello world", "ar": "مرحبا بالعالم" },
  "cta":   "Get started"
}
```

### 4 — Blade usage

[](#4--blade-usage)

```
// Current app locale (e.g. 'en'):
ui()->section('hero')->field('title')        // → "Welcome"

// Explicit locale:
ui()->section('hero')->field('title:ar')     // → "أهلاً"

// Switch app locale:
app()->setLocale('ar');
ui()->section('hero')->field('title')        // → "أهلاً"
```

**Fallback order:** requested locale → `default_locale` → first non-empty stored locale → `''`.

---

Variables System
----------------

[](#variables-system)

Any field value stored in the database can reference other values using `%key%` placeholders. They are resolved lazily when `getString()` is called — never at storage time. **Every field supports placeholders automatically** — no special flag is needed to use them.

### Marking a field as a variable source

[](#marking-a-field-as-a-variable-source)

Use `->hasVariable()` to **export** a field's value as a reusable variable that other fields (or templates) can reference:

```
public function fields(): array
{
    return [
        Field::text('phone')->hasVariable(),   // exposes %general.phone% for use anywhere
        Field::image('logo')->hasVariable(),    // exposes %general.logo:url%, %general.logo:name%
        Field::textarea('bio'),                 // normal field — no export, but still resolves %placeholders% in its value
    ];
}
```

`->hasVariable()` controls two things in the panel:

- A **copy button** appears next to the field label showing the variable placeholder(s) for that field
- The field is listed in the **Variable Browser** panel so editors can discover and copy it

### Using variables in field values

[](#using-variables-in-field-values)

Any field value can contain `%placeholder%` syntax — **no extra configuration needed**. The substitution happens when the value is read, not when it is saved:

```
{{-- phone is marked hasVariable(), other fields can reference it --}}
{{ ui('general')->field('phone') }}   {{-- +966 50 000 0000 --}}

{{-- A URL field storing a WhatsApp deep-link with a phone placeholder --}}
{{ ui('social')->field('whatsapp_link') }}   {{-- https://wa.me/+966500000000 --}}
```

```
// Section definition
Field::text('phone')->hasVariable(),          // source — exported as %general.phone%
Field::url('whatsapp_link'),                  // stores "https://wa.me/%general.phone%"
                                              // resolved automatically at read time
```

### Syntax

[](#syntax)

```
%app.name%                           built-in
%general.phone%                      any section.field marked hasVariable()
%header.logo:url%                    image/file — returns the file URL
%header.logo:name%                   image/file — original filename
%header.logo:size%                   file — file size in bytes
%event.date:format(Y-m-d)%           date/datetime — formatted string
%event.starts_at:format(Y-m-d H:i)% datetime — formatted string
%promo.period:start%                 date_range — start date
%promo.period:end%                   date_range — end date
%product.price%                      price — raw stored value
%product.price:currency%             price — currency code

```

### Variable picker in the panel

[](#variable-picker-in-the-panel)

When `->hasVariable()` is set on a field, a **copy button** appears next to the field label:

Formats availableUI1 formatSingle copy button showing the placeholderMultiple formatsDropdown button listing all available formatsClicking any format copies its placeholder to the clipboard.

The **variable autocomplete** is available in every text, textarea, and URL input — type `%` to open a filtered dropdown of all known variables.

### Per-field format table

[](#per-field-format-table)

Field typeAvailable placeholders`text` / `textarea` / `editor` / `select` / `color` / `url``%section.field%``image``%section.field:url%`, `%section.field:name%``file``%section.field:url%`, `%section.field:name%`, `%section.field:size%``date``%section.field%`, `%section.field:format(Y-m-d)%``datetime``%section.field%`, `%section.field:format(Y-m-d H:i)%``date_range``%section.field:start%`, `%section.field:end%``price``%section.field%`, `%section.field:currency%`### Built-in variables

[](#built-in-variables)

VariableResolves to`%app.name%``config('app.name')``%app.url%``config('app.url')``%app.env%``config('app.env')`### Custom variables

[](#custom-variables)

Register in a service provider:

```
use AhmedAliraqi\UiManager\Variables\VariableRegistry;

public function boot(VariableRegistry $registry): void
{
    $registry->value('site.year', (string) now()->year);
    $registry->register('auth.user', fn () => auth()->user()?->name ?? 'Guest');
    $registry->register('mail.support', fn () => config('mail.from.address'));
}
```

---

Blade Usage
-----------

[](#blade-usage)

### Non-repeatable sections

[](#non-repeatable-sections)

```
{{-- Render a field (auto-cast to string) --}}
{{ ui()->section('banner')->field('title') }}

{{-- Shorthand — equivalent --}}
{{ ui('banner')->field('title') }}

{{-- HTML field (editor) — unescaped --}}
{!! ui('banner')->field('description') !!}

{{-- Image URL --}}

{{-- Check for empty value --}}
@if(!ui('banner')->field('title')->isEmpty())
    {{ ui('banner')->field('title') }}
@endif

{{-- Explicit locale --}}
{{ ui('banner')->field('title:ar') }}

{{-- Price fields --}}
{{ ui('event')->field('price')->amount() }} {{ ui('event')->field('price')->currency() }}
```

### Repeatable sections

[](#repeatable-sections-1)

```
@foreach(ui()->section('social') as $item)
    {{ $item->field('platform') }}
@endforeach

{{-- Count items --}}
{{ ui()->section('social')->count() }}

{{-- Check if empty --}}
@if(!ui()->section('social')->isEmpty())
    ...
@endif

{{-- Get first item --}}
{{ ui()->section('social')->first()?->field('platform') }}
```

### Blade directives

[](#blade-directives)

```
@uiField('banner', 'title')
```

---

Embedded Panel
--------------

[](#embedded-panel)

The `UiManagerPanel` component lets you drop the full pages-and-sections editor into **any existing dashboard** (AdminLTE, Filament, etc.). It is self-contained: it fetches its own data, manages its own state, and uses hash-based navigation so the host page URL never changes.

### Hash navigation

[](#hash-navigation)

The panel encodes its state in the URL hash, leaving the host page's path and query string untouched.

HashEffect*(none)*Opens first page, first section`#uim:home`Opens the **home** page, first section`#uim:home:banner`Opens the **home** page, **banner** sectionClicking Back/Forward in the browser moves through hash states as expected. The hash is written with `history.replaceState`, so it never creates history entries of its own.

---

### Option 1 — Blade directive (for any Blade view in your host dashboard)

[](#option-1--blade-directive-for-any-blade-view-in-your-host-dashboard)

After running `php artisan ui-manager:install`, include the panel anywhere with a single directive:

```
{{-- resources/views/admin/ui-manager.blade.php --}}
@extends('adminlte::page')

@section('content')

        @uiManagerPanel

@endsection
```

Pass overrides to merge on top of the package config:

```
@uiManagerPanel(['locales' => ['en', 'ar'], 'defaultLocale' => 'ar'])
```

The directive outputs `` tags (Bootstrap + Tailwind CSS, scoped), a mount ``, and `` tags — all resolved from the published manifest. **No manual asset registration needed.**

> **Prerequisite** — publish assets if you haven't already:
>
> ```
> php artisan vendor:publish --tag=ui-manager-assets
> ```

---

### Option 2 — Auto-mount via data attribute (raw HTML)

[](#option-2--auto-mount-via-data-attribute-raw-html)

Include the panel bundle and CSS in any HTML page, then add the mount element:

```

    {{-- Published asset URL — resolve from public/vendor/ui-manager/manifest.json --}}

```

Pass a config override via the `data-config` attribute (JSON):

```

```

> Use `@uiManagerPanel` (Option 1) whenever possible — the Blade directive resolves the hashed filenames automatically.

---

### How it works

[](#how-it-works)

```
Host page (AdminLTE, Filament, custom Blade)
└── @uiManagerPanel  ──────────────────────────────────────────────┐
    │  Bootstrap CSS (scoped under .uim-bs — no leakage)     │
    │  Tailwind CSS (form components)                         │
    │                                    │
    │   ┌──────────────────────────────────────────────────────┐    │
    │   │  Pages   [Home ●] [About] [Contact]                  │    │
    │   ├──────────────┬───────────────────────────────────────┤    │
    │   │  Sections    │  Edit: Banner                         │    │
    │   │  > Banner ●  │  Title (EN) [__________________]      │    │
    │   │    Featured  │  Title (AR) [__________________]      │    │
    │   │    Most Loved│  Image      [📷 upload]                │    │
    │   │              │                         [Save]         │    │
    │   └──────────────┴───────────────────────────────────────┘    │
    │  panel bundle                                         │
    └───────────────────────────────────────────────────────────────┘

```

The Bootstrap CSS is fully isolated under `.uim-bs` — it will **not** override AdminLTE's Bootstrap 4/5 styles or any other framework on the host page.

---

Artisan Commands
----------------

[](#artisan-commands)

### Install

[](#install)

```
php artisan ui-manager:install          # publish config + assets, run migrations
php artisan ui-manager:install --force  # overwrite previously published files
```

### Create a Page

[](#create-a-page)

```
php artisan make:ui-page About
php artisan make:ui-page Marketing/Landing  # creates in App\Ui\Pages\Marketing\
php artisan make:ui-page AboutUs --name=about-us
```

### Create a Section

[](#create-a-section)

```
php artisan make:ui-section Banner
php artisan make:ui-section Banner --page="App\Ui\Pages\Home"
php artisan make:ui-section SocialLinks --page="App\Ui\Pages\Home" --repeatable
php artisan make:ui-section Hero --layout=marketing --force
```

When `--page` is omitted, the command shows an interactive list of all registered pages. If none are registered yet, it falls back to a free-text prompt.

---

Extending the Package
---------------------

[](#extending-the-package)

### Register pages and sections programmatically

[](#register-pages-and-sections-programmatically)

Auto-discovery covers `app/Ui/Pages` and `app/Ui/Sections` by default. To register classes from other locations:

```
use AhmedAliraqi\UiManager\Services\PageRegistry;
use AhmedAliraqi\UiManager\Services\SectionRegistry;

public function boot(PageRegistry $pages, SectionRegistry $sections): void
{
    $pages->register(new MyPage());
    $sections->register(new MySection());

    // Or by class name:
    $pages->registerClass(MyPage::class);
    $sections->registerClass(MySection::class);
}
```

### Custom field types

[](#custom-field-types)

1. **Create the PHP class** in `app/Ui/Fields/RatingField.php`:

```
use AhmedAliraqi\UiManager\Fields\BaseField;

class RatingField extends BaseField
{
    protected int $max = 5;

    public function max(int $max): static
    {
        $this->max = $max;
        return $this;
    }

    public function getType(): string
    {
        return 'rating';
    }

    public function toArray(): array
    {
        return array_merge(parent::toArray(), ['max' => $this->max]);
    }
}
```

2. **Create a Vue component** for the panel.
3. **Register it** in `FieldRenderer.vue` under the `'rating'` type case.
4. **Rebuild assets:** `npm run build` (only needed when modifying the panel frontend).

### Register custom variables

[](#register-custom-variables)

```
use AhmedAliraqi\UiManager\Variables\VariableRegistry;

public function boot(VariableRegistry $registry): void
{
    $registry->value('site.year', (string) now()->year);
    $registry->register('store.name', fn () => Store::first()?->name ?? '');
}
```

Use `%site.year%` or `%store.name%` in any field value.

### Manually flush the section cache

[](#manually-flush-the-section-cache)

```
app(\AhmedAliraqi\UiManager\Services\UiManager::class)->flushCache('home', 'banner');
```

---

Testing
-------

[](#testing)

```
./vendor/bin/phpunit

./vendor/bin/phpunit --filter SectionControllerTest  # single class
./vendor/bin/phpunit --filter test_section_uses_db_value_over_default  # single test
```

### Testing sections in your application

[](#testing-sections-in-your-application)

```
use AhmedAliraqi\UiManager\Models\UiContent;

// Seed a section value:
UiContent::create([
    'layout'  => 'default',
    'page'    => 'home',
    'section' => 'banner',
    'fields'  => ['title' => 'Test Title'],
]);

// Disable cache in test environments:
config(['ui-manager.cache.enabled' => false]);
```

---

Architecture Overview
---------------------

[](#architecture-overview)

```
src/
├── Console/
│   ├── InstallCommand.php
│   ├── MakeUiPageCommand.php
│   └── MakeUiSectionCommand.php
├── Contracts/
│   ├── HasFields.php                interface requiring fields() method
│   ├── Repeatable.php               marker interface for list-type sections
│   └── Renderable.php
├── Core/
│   ├── Page.php                     abstract base for all pages
│   └── Section.php                  abstract base for all sections
├── DTOs/
│   ├── FieldValueData.php           typed accessor: getString(), getUrl(), amount(), …
│   └── SectionData.php
├── Exceptions/
│   └── UiManagerException.php
├── Facades/
│   └── Ui.php
├── Fields/
│   ├── Field.php                    static factory (entry point)
│   ├── BaseField.php                fluent builder base; hasVariable() exports field as a variable
│   ├── TextField.php
│   ├── EditorField.php
│   ├── SelectField.php
│   ├── ImageField.php
│   ├── FileField.php
│   ├── ColorField.php
│   ├── DateField.php
│   ├── TimeField.php
│   ├── DatetimeField.php
│   ├── DateRangeField.php
│   ├── UrlField.php
│   └── PriceField.php
├── Http/
│   ├── Controllers/
│   │   └── Api/
│   │       ├── PageController.php
│   │       ├── SectionController.php    validates reorder IDs; no events
│   │       ├── MediaController.php
│   │       └── VariableController.php
│   └── Requests/
│       └── SaveSectionRequest.php       dynamic field-level + per-locale rules
├── Models/
│   ├── UiContent.php               single table for all section data
│   ├── UiMediaFile.php             Spatie media owner model
│   └── UiMedia.php                 alias
├── Services/
│   ├── UiManager.php               main entry point — ui() helper target
│   ├── PageRegistry.php
│   ├── SectionRegistry.php
│   ├── VariableParser.php          %key:modifier()% replacement with depth guard
│   └── MediaUploadService.php
├── Support/
│   ├── ClassDiscovery.php
│   ├── SectionView.php
│   ├── RepeatableSectionView.php   IteratorAggregate — foreach-able
│   ├── SectionItemView.php
│   └── helpers.php                 ui() global helper
├── Variables/
│   └── VariableRegistry.php        resolvers + modifier handlers (format, start, end, …)
└── UiManagerServiceProvider.php

```

### Data flow

[](#data-flow)

```
PHP Section class (fields() definition)
        ↓
Panel reads via  GET /ui-manager/api/pages/{page}/sections/{section}
        ↓
User edits and submits
        ↓
SaveSectionRequest validates (field rules + per-locale for translatable)
        ↓
SectionController saves → ui_contents.fields (JSON)
        ↓
Cache flushed
        ↓
Blade calls  ui()->section('name')->field('key')
        ↓
UiManager reads from cache (or DB if cold)
        ↓
Returns FieldValueData · variables resolved on getString() / getUrl()

```

### Database tables

[](#database-tables)

TablePurpose`ui_contents`All section field data; `sort_order IS NULL` = single record, `NOT NULL` = repeatable item`ui_media_files`Thin owner model for Spatie Media Library`media`Spatie's standard media table (bundled migration)---

License
-------

[](#license)

MIT © Ahmed Fathy

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance83

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

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 ~1 days

Total

5

Last Release

87d ago

Major Versions

v1.2.0 → v2.0.02026-04-28

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/23261109?v=4)[Ahmed Fathy](/maintainers/ahmed-aliraqi)[@ahmed-aliraqi](https://github.com/ahmed-aliraqi)

---

Top Contributors

[![ahmed-aliraqi](https://avatars.githubusercontent.com/u/23261109?v=4)](https://github.com/ahmed-aliraqi "ahmed-aliraqi (22 commits)")

---

Tags

laraveluicmssectionsfieldscontent management

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ahmed-aliraqi-ui-manager/health.svg)

```
[![Health](https://phpackages.com/badges/ahmed-aliraqi-ui-manager/health.svg)](https://phpackages.com/packages/ahmed-aliraqi-ui-manager)
```

###  Alternatives

[nasirkhan/laravel-starter

A CMS like modular Laravel starter project.

1.4k2.7k](/packages/nasirkhan-laravel-starter)[slimani/filament-media-manager

A media manager plugin for Filament.

126.9k](/packages/slimani-filament-media-manager)[tapp/filament-form-builder

User facing form builder using Filament components

132.4k5](/packages/tapp-filament-form-builder)

PHPackages © 2026

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