PHPackages                             ngos/admin-core - 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. [Admin Panels](/categories/admin)
4. /
5. ngos/admin-core

ActiveLibrary[Admin Panels](/categories/admin)

ngos/admin-core
===============

Reusable admin CRUD core (config-driven controllers/services, Route::crud macro, resource generator) for Laravel + AdminLTE 4 / Bootstrap 5.

v1.3.0(today)013↑2900%MITPHPPHP ^8.3CI passing

Since Jun 9Pushed todayCompare

[ Source](https://github.com/ngouyoung/admin-core)[ Packagist](https://packagist.org/packages/ngos/admin-core)[ Docs](https://github.com/ngouyoung/admin-core)[ RSS](/packages/ngos-admin-core/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (13)Versions (18)Used By (0)

ngos/admin-core
===============

[](#ngosadmin-core)

A reusable, config-driven **admin CRUD core** for Laravel 13 + Bootstrap 5, with a custom branded admin theme.

It gives you a thin, conventional CRUD skeleton — abstract `CrudController` + `CrudService`, a `Route::crud()` route macro, and an `admin-core:make` resource generator — so every backend table in your app is built the same way, with permission gating and yajra DataTables wired in.

- **Blade + Bootstrap 5 + jQuery DataTables.** No Livewire. jQuery only for plugins.
- **Config-driven.** Route-name prefix, view-path prefix, permission pattern and pagination all in `config/admin-core.php`.
- **Permission-aware.** Each CRUD action is gated by `permission:{action}-{resource}` (spatie/laravel-permission).

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

[](#requirements)

- PHP ^8.3
- Laravel ^13
- `spatie/laravel-permission` ^8, `yajra/laravel-datatables-oracle` ^13 (pulled in automatically)

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

[](#installation)

```
composer require ngos/admin-core
php artisan admin-core:install
```

`admin-core:install` scaffolds the host-side glue the generated pages depend on (idempotent — safe to re-run, `--force` to overwrite):

PublishedPurpose`config/admin-core.php`route/view/permission/pagination conventions`config/class.php`CSS-class map for tables/buttons/icons`resources/views/backend/layouts/app.blade.php`self-contained CDN starter layout (jQuery, DataTables, Bootstrap 5, SweetAlert2, toastr, CSRF)`resources/views/backend/dashboard.blade.php`minimal dashboard so `admin.dashboard` resolves`routes/Web/Backend/Modules/`auto-loaded folder for generated resource routes`routes/web.php`an `admin` route group + module loader (added once, marked `admin-core:routes`)Then finish the spatie setup:

```
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
```

### Full access module (login + users/roles/permissions)

[](#full-access-module-login--usersrolespermissions)

Want a working authenticated admin out of the box? Pass `--access`:

```
php artisan admin-core:install --access
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
php artisan db:seed --class=Database\\Seeders\\AccessSeeder
```

This additionally scaffolds (all in your `App\` namespace, yours to edit):

- **Auth** — a minimal session `LoginController` + login view + `/login` `/logout` routes; the `admin` route group is wrapped in `auth`.
- **Users / Roles / Permissions** management screens (controllers, services, form requests, Blade views) built on the CRUD core, with role/permission assignment.
- `App\Models\Role` / `App\Models\Permission` (extending spatie), the `HasRoles` trait added to `App\Models\User`, sidebar links, and an `AccessSeeder` that creates an `admin` role with every permission plus an admin user.

Log in at `/login` with **`admin@example.com` / `password`**. (`admin-core:make` auto-grants each new resource's permissions to the `admin` role, so there's nothing to re-seed.)

Prefer a single command? `admin-core:install --access --build --seed` also runs `npm install && npm run build` and migrates + seeds the admin user for you.

Generating a resource
---------------------

[](#generating-a-resource)

```
php artisan admin-core:make Product --migration
```

Generates the model, service, controller, form requests, a route module, the Blade views, and the `list/create/edit/delete-product` permissions. Visit `/admin/products`.

### Generating fields too (`--fields`)

[](#generating-fields-too---fields)

Pass a field list and the generator fills in the **migration columns, `$fillable`, validation rules, form inputs, table headers, and DataTable columns** — a ready-to-use CRUD, no manual edits:

```
php artisan admin-core:make Product --migration --fields="\
  name:string, price:decimal?, description:text?, is_active:boolean, \
  status:enum:draft|published, published_at:date?, category_id:foreign"
```

**Field DSL** — `name:type`, comma-separated:

TypeMigrationForm controlRule`string` (default)`string`text`string,max:255``text``text`textarea`string``integer``integer`number`integer``decimal``decimal(10,2)`number (step)`numeric``boolean``boolean` default 0checkbox`boolean``date` / `datetime``date` / `dateTime`date / datetime-local`date``email``string`email`email``enum:a|b|c``string````in:a,b,c``foreign` (`x_id`)`foreignId()->constrained()`Select2 of related rows`exists:xs,id``image``string` (path)file input + preview`image,max:2048``file``string` (path)file input`file,max:10240``belongsToMany` (`m2m`)pivot tablemulti-Select2`array` + `exists``image`/`file` also generate **upload handling in the service** (store on the `public` disk, delete the old file on update, clean up on delete) and add `enctype="multipart/form-data"` to the form — run `php artisan storage:link` once. `belongsToMany` generates the pivot migration, a `belongsToMany`relation, a multi-select, and `sync()` in the service. Both infer the related model/table from the field name, so generate the related resource first.

**Modifiers** (suffix, any order):

ModifierMeaningWhat it generates`?`nullablenullable column + `nullable` rule`^`uniqueunique index + `unique` rule (ignores self by route key on update)`~`**write-once**settable on create, **locked on update** — fillable + StoreRequest rule, *no* UpdateRequest rule, `readonly` input on edit`@`**system**set by trusted code only — **not** fillable, not validated, not in the form; a `booted()` hook scaffold + nullable column (shown read-only)E.g. `slug:string^`, `published_at:date?`, `sku:string^~` (unique, locked after create).

**Typed system helpers** (imply `@`, auto-filled in the generated `booted()` hook — no TODO to wire up):

TypeColumnAuto-set to`created_by:auth`nullable `users` FK`auth()->id()``code:sku`nullable stringa generated `Str::upper(Str::random(10))` codeE.g. `--fields="name:string, code:sku, created_by:auth"` gives you an auto SKU and an owner stamp with zero hand-editing — neither is user-fillable.

> Security note: `~` and `@` enforce on the **server** (missing update rule / not fillable), not just the readonly input — so a user editing the DOM or POSTing directly still can't change them.

**Foreign keys**: `category_id:foreign` adds a `belongsTo` relation on the model, a Select2 dropdown of the related rows in the form (labelled by the related row's `name`, falling back to `id`), and a related-name column in the table. The related table is inferred (`category_id` → `categories`), so it must already exist — generate the parent resource first.

### App shell (with `--access`)

[](#app-shell-with---access)

The `--access` kit now ships a complete admin shell beyond the access screens:

- **Profile / account** (`/admin/profile`) — edit name/email, change password, upload an avatar.
- **Settings** (`/admin/settings`) — grouped key-value app settings with a `Setting::get('key')` helper (cached), gated by the `manage-settings` permission. Seeded with `app_name`, `support_email`, etc.
- **Dashboard** — stat-card widgets (Users / Roles / Permissions / Group Permissions counts).
- **Auto-sidebar** — `admin-core:make` injects the new resource's nav link automatically (idempotent), so you never hand-edit the sidebar.
- **Show / detail view** — every resource gets a read-only `show` page + a View button in the table.

### Every list comes with export &amp; bulk delete

[](#every-list-comes-with-export--bulk-delete)

Generated index screens ship two things out of the box:

- **Export** — an `Export` button streams the table to CSV (`export` route, gated by `list-*`).
- **Bulk delete** — a select-all checkbox column + a "Delete selected" button that soft/hard-deletes the chosen rows in one request (`bulkDelete` route, gated by `delete-*`).

Both live on the base `CrudController` (`export()` / `bulkDelete()`), plus a single DataTables search box (server-side via yajra), so they apply to every resource.

### Drag-to-reorder (`--sortable`)

[](#drag-to-reorder---sortable)

```
php artisan admin-core:make Category --sortable --migration --fields="name:string"
```

Adds a `sort` column and a **Sort** toggle button on the index that reveals a **drag-and-drop panel**(reusing the bundled nestable plugin) — the DataTable stays put. Dragging a row posts the new order to a `reorder` route, which persists each row's `sort` position via `CrudService::reorder()`. Best paired with the `--access` kit (which bundles the nestable JS).

### Audit trail (`--audit`)

[](#audit-trail---audit)

```
php artisan admin-core:make Product --audit --migration --fields="name:string"
```

Adds the package's `LogsActivity` trait to the model, recording every create/update/delete in `activity_logs` (the actor, the subject, and the changed attributes — sensitive fields like `password`are filtered out). The `activity_logs` table migration is published by `admin-core:install`; the `--access` kit adds a read-only **Activity Log** viewer (gated by `list-activity`). Set `'generator' => ['audit' => true]` to audit every generated resource, or add the trait to any model:

```
use Ngos\AdminCore\Concerns\LogsActivity;

class Order extends Model { use LogsActivity; }
```

### Soft deletes &amp; extras

[](#soft-deletes--extras)

Every `admin-core:make` also generates a **Factory** (field-aware fake data), a **Seeder**, and a permission-mapped **Policy**. Add `--soft-deletes` for a trash workflow:

```
php artisan admin-core:make Product --soft-deletes --migration --fields="name:string, price:decimal?"
```

It adds the `SoftDeletes` trait + `deleted_at` column, a **Trash** button on the index, and a trash screen with **Restore** / **Delete permanently** (routes `trash` / `restore` / `forceDelete`, backed by `trashedQuery()` / `restore()` / `forceDelete()` on the base service).

### Non-enumerable URLs — the hybrid key strategy (`--uuid`)

[](#non-enumerable-urls--the-hybrid-key-strategy---uuid)

`--uuid` gives a resource a **public UUID** for its URLs while keeping a fast **bigint primary key**:

```
php artisan admin-core:make Product --uuid --migration --fields="name:string, category_id:foreign"
```

It generates:

- `$table->id();` — the bigint primary key (all **foreign keys and joins use this** → lean indexes that never bloat)
- `$table->uuid('uuid')->unique();` — the **public** key used in URLs/APIs (`/admin/products/019eadac-…`, non-enumerable)
- `foreignId('category_id')->constrained()` — bigint FK (not `foreignUuid`)
- a model using the package's `HasPublicUuid` trait, which auto-fills the uuid and sets `getRouteKeyName() => 'uuid'`

So you get **non-guessable URLs without the index/join cost of uuid primary keys** — the best default for a system that may grow. The base `CrudService` resolves every action by the model's route key, so edit/show/update/delete/bulk-delete/reorder all use the uuid automatically; plain `id` models (no `--uuid`) keep using `id` unchanged.

To make **every** generated resource hybrid, set `'generator' => ['uuid' => true]` in `config/admin-core.php`(override per-resource with `--no-uuid`). The `--access` module (users/roles/permissions/group-permissions) ships hybrid too. Use a plain model? Add `Ngos\AdminCore\Concerns\HasPublicUuid` + a `uuid` column to any model.

> Omitting `--fields` gives the default single `name` column (backward-compatible). The generated routes are gated by `permission:*` middleware. Either assign the new permissions to a role and wrap the `admin-core:routes` group in `['auth', ...]`, or set `permission.enabled => false`in `config/admin-core.php` to browse without auth while developing.

Lifecycle commands
------------------

[](#lifecycle-commands)

```
php artisan admin-core:version                  # show the installed package version
php artisan admin-core:uninstall                # un-wire (remove the route/middleware blocks + User trait)
php artisan admin-core:uninstall --purge        # also delete the files it published
php artisan admin-core:reinstall [--access]     # purge + reinstall (clean re-scaffold)
```

Everything `install` injects is wrapped in `// >>> admin-core:* … //
