13 KiB
name, description, license, metadata
| name | description | license | metadata | ||
|---|---|---|---|---|---|
| cpad-plugin-development | Use when creating, reviewing, or refactoring cPad plugins. Trigger for plugin module structure, ServiceProvider registration, route layout, admin page headers, setup controllers, DataTables usage, Blade components, translations, migrations, models, and plugin-specific conventions for packages under cPad\Plugins*. | MIT |
|
cPad Plugin Development
Use this skill when building new cPad plugins or updating existing ones. Follow the patterns already used by the plugin you are touching, especially the Blocks module.
Core Principle
Prefer the established cPad plugin conventions over generic Laravel defaults when both are plausible. A plugin should feel native to the admin panel, not like a standalone Laravel package.
If a pattern already exists in the target plugin or a sibling plugin, keep it. Use this skill to standardize the parts that are repeated across plugins, not to invent new architecture.
Plugin Structure
A typical plugin should have this shape:
composer.jsonfor package metadata and dependenciesmanifest.jsonfor plugin metadata, permissions, version, and entry pointServiceProvider.phpfor bootstrappingControllers/for admin actions and setup flowsControllers/Traits/for reusable controller logicModels/for Eloquent models and revision modelsMigrations/for plugin-owned schemaRoutes/routes.phpfor admin routesResources/views/for Blade views, forms, setup screens, and componentsComponents/for Blade components exposed to frontend templatesServices/for business logic that should not live in controllers
Keep plugin code namespaced under cPad\Plugins\<PluginName>\.
Service Provider Rules
Use the service provider to wire the plugin into cPad:
- Load migrations from the plugin
Migrations/directory - Load views with a plugin namespace like
plugin.<slug> - Register Blade components when the plugin exposes reusable frontend markup
- Register the route file from
register() - Add the plugin menu entry in the admin panel if the plugin has a visible admin area
Keep registration logic minimal and push behavior into controllers, services, or views.
Routing Rules
Use a grouped route file with the admin middleware and the cPad webroot prefix.
Preferred shape:
- Middleware:
admin - Prefix:
config('cp.webroot', 'cp') - Route names:
admin.plugin.<slug>.* - Setup routes under a dedicated
setupprefix - Use POST for mutating actions and GET for page rendering
Keep route names predictable so Blade views and JavaScript can reference them consistently.
Admin Header Pattern
Use a shared header partial for plugin pages.
The Blocks module shows a good pattern:
- Extend the admin layout
- Include the plugin header partial inside the content section
- Render a page header component with slots for logo, main route, add route, middle actions, and append actions
- Use
showSaveBtnandhideAddBtnflags where a page needs to alter the header controls - Keep
header.blade.phpas the shared admin header for the plugin, not a page-specific header - Reuse the same header partial in create, edit, layout, setup, and list pages when the navigation/actions are shared
For page wrappers:
create.blade.phpandedit.blade.phpshould usually be thin wrappers- Both should extend the admin layout, include the shared header, and then include the shared form partial
- The only difference is usually the form action route and, for edit pages, the bound model data
- Keep form fields in a shared
form.blade.phpor segmented partials so create/edit stay in sync
Recommended header behavior:
- Keep the header partial small and reusable
- Put navigation actions in the header, not scattered across page bodies
- Expose setup, layout, or diagnostic actions as middle/header buttons when they are part of the plugin workflow
Views and Page Layout
For admin pages:
- Extend
admin::layout.default - Wrap content in
<x-page-layout>or<x-card>where appropriate - Use plugin view namespaces such as
plugin.block::main - Use
@section('header-addon')for page-specific CSS - Use
@section('footer-addon')for page-specific JavaScript
For forms:
- Split large forms into partials like settings, translations, revisions, or layout tabs
- Prefer small includes for repeated form groups
- Keep Blade views focused on markup and binding, not business logic
DataTables Rule
Blocks includes DataTables assets, but the table itself is still rendered server-side. That means the important rule is:
- Do not add DataTables by default just because it exists in the admin theme
- Use it only when you need client-side search, sort, or paging
- If you include the DataTables assets, initialize the table explicitly in the footer script
- If a table is simple, a plain Blade table is usually better and easier to maintain
When DataTables is used:
- Load CSS in
header-addon - Load JS in
footer-addon - Keep markup semantic and compatible with the admin table classes
- Avoid duplicating behavior already handled by server-side filters
Setup Flow
Use a dedicated setup controller for plugin configuration.
Rules:
- Restrict setup actions with
Security::check(...) - Store plugin settings through the cPad config service, usually as JSON
- Keep long-lived configuration separate from page-specific form state
- If the plugin has editable layout fragments such as header or footer HTML, store them as separate config keys
- Provide a setup screen with tabs or sections when the configuration is more than a few fields
The Blocks module pattern is useful here: one config payload for plugin options and a separate layout payload for header/footer HTML.
Model Rules
Follow the existing database shape instead of forcing conventional Laravel naming when the plugin schema is already established.
Common patterns in Blocks:
- Custom primary keys like
block_id - Explicit table names when they do not follow Laravel defaults
fillabledefined for all mass-assigned fields- Cast JSON columns to
array - Cast booleans explicitly
- Use local scopes for repeated filters such as active records or group filters
If the plugin stores language-specific content in JSON, expose a helper method or accessor for the current locale rather than repeating array lookups everywhere.
Migration Rules
Plugin migrations should be easy to read and easy to reverse.
- Use a plugin-specific prefix in migration filenames
- Create plugin-owned tables only
- Add indexes where the plugin filters or sorts frequently
- Include foreign keys for revision tables or parent-child tables
- Keep
down()reversible unless the change is intentionally one-way
For revision-style tables:
- Store the original record ID
- Keep the revision table aligned with the main table’s key fields
- Use cascading delete only when revision history should disappear with the source record
Controller Rules
Keep controllers thin.
- Put data prep in private helpers or services
- Use traits only when the same logic is reused across controller actions
- Validate input before touching the database
- Use transactions for multi-step writes or revision snapshots
- Return redirects for admin form submissions and arrays/JSON only for AJAX endpoints
Blocks shows a practical controller split:
- CRUD and page rendering in the main controller
- setup logic in a dedicated controller
- editor integration in a separate controller
- shared mutation logic in a trait
Revisions and Auditing
If the plugin needs change history:
- Snapshot the previous record before updating
- Normalize data before comparing it so cosmetic ordering does not create false diffs
- Store timestamps on the revision record
- Keep the diff logic in one place
Frontend Components
For reusable frontend output:
- Expose a Blade component with a small constructor API
- Keep the component read-only
- Resolve content by keycode or slug and optional store/language context
- Return raw HTML only when the plugin intentionally manages HTML content
If debug mode is useful, gate it behind a config or environment check and keep the debug overlay unobtrusive.
Blade and Content Safety
Be careful with encoded content.
- Decode stored HTML only at the boundary where it needs to be displayed or edited
- Avoid double-encoding content when round-tripping through the editor
- Preserve raw HTML when the plugin is designed to store HTML fragments
- If content is user-authored, validate and sanitize deliberately instead of assuming Blade escaping will handle it
Naming Conventions
Use predictable names:
- View namespace:
plugin.<slug>::... - Route name prefix:
admin.plugin.<slug>. - Config keys:
plugin.<slug>.config,plugin.<slug>.layout - Plugin class namespace:
cPad\Plugins\<PluginName>\... - Admin menu label and plugin title should match the human-facing module name
What to Check Before Writing a New Plugin
Before starting a new cPad plugin, confirm:
- What the plugin’s main content model is
- Whether it needs revisions or history
- Whether it needs setup/configuration screens
- Whether it needs header/footer HTML injection
- Whether it needs store, language, or site scoping
- Whether it needs a reusable Blade component
- Whether the admin list can stay server-rendered or needs DataTables
Output Expectations
When asked to create a new plugin, produce:
- A clear folder structure
- A service provider that registers the plugin cleanly
- Routes grouped by admin prefix
- Admin pages with a shared header and consistent layout
- A setup screen if the plugin has configuration
- Model and migration conventions that match the plugin’s schema
Blocks Module Notes
The Blocks plugin is the canonical example in this repo for:
- A custom admin header partial
- A setup controller with config persistence
- Translation tabs and editor-based content entry
- Optional store scoping
- Revision tracking
- A Blade component for frontend rendering
Use it as the reference implementation when a future plugin has similar needs.
Starter Templates
Use these as the default starting point for a new plugin and then adapt them to the plugin's domain.
Service Provider
<?php
namespace cPad\Plugins\Example;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Klevze\ControlPanel\Framework\Core\Menu;
class ServiceProvider extends LaravelServiceProvider
{
public function boot(Menu $menu): void
{
$this->loadMigrationsFrom(__DIR__ . '/Migrations');
$this->loadViewsFrom(__DIR__ . '/Resources/views', 'plugin.example');
Blade::component('example', \cPad\Plugins\Example\Components\ExampleComponent::class);
$menu->addItem(trans('admin.CONTENT'), trans('admin.EXAMPLE'), 'fa fa-th-large', 'admin.plugin.example.main');
}
public function register(): void
{
require __DIR__ . '/Routes/routes.php';
}
}
Routes
<?php
use Illuminate\Support\Facades\Route;
use cPad\Plugins\Example\Controllers\ExampleController;
use cPad\Plugins\Example\Controllers\SetupController;
Route::group(['middleware' => 'admin', 'as' => 'admin.plugin.example.', 'prefix' => config('cp.webroot', 'cp') . '/content/example'], function () {
Route::get('/', [ExampleController::class, 'main'])->name('main');
Route::get('create', [ExampleController::class, 'create'])->name('create');
Route::get('edit/{id}', [ExampleController::class, 'edit'])->name('edit');
Route::post('insert', [ExampleController::class, 'insert'])->name('insert');
Route::post('update/{id}', [ExampleController::class, 'update'])->name('update');
Route::group(['as' => 'setup.', 'prefix' => 'setup'], function () {
Route::get('/', [SetupController::class, 'setup'])->name('main');
Route::post('update', [SetupController::class, 'update'])->name('update');
});
});
Shared Header
<x-page-header :sticky="true">
<x-slot name="logo">
{{ config('cp.admin_path') }}/images/icons/example.png
</x-slot>
<x-slot name="mainRoute">
{{ route('admin.plugin.example.main') }}
</x-slot>
@unless(isset($hideAddBtn))
<x-slot name="addRoute">
{{ route('admin.plugin.example.create') }}
</x-slot>
@endunless
@if(isset($showSaveBtn))
<x-slot name="append">
<button type="submit" class="btn btn-flat bg-indigo saveForm">
<i class="fa fa-save fa-fw"></i>
{{ trans('admin.SAVE') }}
</button>
</x-slot>
@endif
<x-slot name="title">
{{ trans('admin.EXAMPLE') }}
</x-slot>
</x-page-header>
Create and Edit Wrappers
@extends('admin::layout.default')
@section('content')
@include('plugin.example::header', ['showSaveBtn' => true, 'hideAddBtn' => true])
@include('plugin.example::form', ['urlRoute' => route('admin.plugin.example.insert')])
@endsection
@extends('admin::layout.default')
@section('content')
@include('plugin.example::header', ['showSaveBtn' => true, 'hideAddBtn' => true])
@include('plugin.example::form', ['urlRoute' => route('admin.plugin.example.update', $table->id)])
@endsection