Files
aritmija/.agents/skills/cpad-plugin-development/SKILL.md
2026-05-13 17:11:09 +02:00

367 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: cpad-plugin-development
description: '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\*.'
license: MIT
metadata:
author: copilot
---
# 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.json` for package metadata and dependencies
- `manifest.json` for plugin metadata, permissions, version, and entry point
- `ServiceProvider.php` for bootstrapping
- `Controllers/` for admin actions and setup flows
- `Controllers/Traits/` for reusable controller logic
- `Models/` for Eloquent models and revision models
- `Migrations/` for plugin-owned schema
- `Routes/routes.php` for admin routes
- `Resources/views/` for Blade views, forms, setup screens, and components
- `Components/` for Blade components exposed to frontend templates
- `Services/` 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 `setup` prefix
- 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 `showSaveBtn` and `hideAddBtn` flags where a page needs to alter the header controls
- Keep `header.blade.php` as 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.php` and `edit.blade.php` should 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.php` or 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
- `fillable` defined 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 tables 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 plugins 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 plugins 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
<?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
<?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
```blade
<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
```blade
@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
```
```blade
@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
```