Plugin Structure
Introduction
Plugins in Botble CMS follow a modular architecture that allows for easy extension of the core functionality. Each plugin is a self-contained module with its own controllers, models, views, and assets.
All plugins are registered to Composer autoloader manually. Each plugin requires a plugin.json
file to provide all needed information for auto-loading.
Directory Structure
When you create a new plugin using the php artisan cms:plugin:create
command, it generates the following directory structure:
platform/plugins/foo/
├── config/
│ └── permissions.php
├── database/
│ ├── migrations/
│ └── seeders/
├── helpers/
├── resources/
│ ├── assets/
│ ├── lang/
│ └── views/
├── routes/
│ └── web.php
├── src/
│ ├── Forms/
│ ├── Http/
│ │ ├── Controllers/
│ │ └── Requests/
│ ├── Models/
│ ├── Providers/
│ │ └── FooServiceProvider.php
│ ├── Repositories/
│ └── Plugin.php
└── plugin.json
Key Components
plugin.json
This file contains metadata about your plugin, including its name, namespace, provider, author, and description. It's required for every plugin and serves as the plugin's manifest.
Example:
{
"name": "Foo",
"namespace": "Botble\\Foo\\",
"provider": "Botble\\Foo\\Providers\\FooServiceProvider",
"author": "Your Name",
"url": "https://yourwebsite.com",
"version": "1.0",
"description": "A simple foo plugin for Botble CMS",
"minimum_core_version": "7.3.0"
}
The fields in the plugin.json file are:
name
: The display name of your pluginnamespace
: The PHP namespace of your plugin (must end with a double backslash)provider
: The fully qualified class name of your plugin's service providerauthor
: The name of the plugin authorurl
: The website URL of the plugin or authorversion
: The version number of the plugindescription
: A brief description of what the plugin doesminimum_core_version
: The minimum version of Botble CMS required to run this plugin
Plugin.php
This file handles the plugin lifecycle events: activation, deactivation, and removal. It implements the PluginInterface
or extends PluginOperationAbstract
.
Example:
<?php
namespace Botble\Foo;
use Botble\PluginManagement\Abstracts\PluginOperationAbstract;
use Illuminate\Support\Facades\Schema;
use Botble\Setting\Facades\Setting;
class Plugin extends PluginOperationAbstract
{
public static function activate(): void
{
// Logic to run when the plugin is activated
// For example, you might want to set default settings
Setting::set('foo_items_per_page', 10)->save();
}
public static function deactivate(): void
{
// Logic to run when the plugin is deactivated
}
public static function remove(): void
{
// Clean up when the plugin is removed
Schema::dropIfExists('foo_items');
Schema::dropIfExists('foo_categories');
// Remove plugin settings
Setting::delete(['foo_items_per_page', 'foo_display_author']);
}
}
The Plugin class has three main methods:
activate()
: Called when the plugin is activated. Use this to initialize any settings or data your plugin needs.deactivate()
: Called when the plugin is deactivated but not removed. Use this to temporarily disable functionality.remove()
: Called when the plugin is completely removed. Use this to clean up database tables, settings, and any other data created by your plugin.
Configuration Files
permissions.php
Each plugin should have a configuration for permissions. Permissions are defined in code so we need to specify them in this file.
Example:
return [
[
'name' => 'Foo',
'flag' => 'foo.index',
],
[
'name' => 'Create',
'flag' => 'foo.create',
'parent_flag' => 'foo.index',
],
[
'name' => 'Edit',
'flag' => 'foo.edit',
'parent_flag' => 'foo.index',
],
[
'name' => 'Delete',
'flag' => 'foo.destroy',
'parent_flag' => 'foo.index',
],
];
The permissions configuration consists of an array of permission definitions:
name
: The display name of the permissionflag
: The unique identifier for the permission, typically following the formatplugin.action
parent_flag
: Optional parent permission flag for hierarchical permissions
Database
Migrations
After generating a plugin, it will create a first migration file in the database/migrations
directory. You should modify this file before activating the plugin. When activating a plugin, its migrations will run automatically.
Example:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
public function up(): void
{
Schema::create('foo_items', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->string('description', 400)->nullable();
$table->longText('content')->nullable();
$table->string('status', 60)->default('published');
$table->integer('user_id')->unsigned();
$table->string('image', 255)->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('foo_items');
}
};
Migrations are used to create and modify database tables. Each migration file should:
- Have a descriptive name that indicates what it does
- Include both
up()
anddown()
methods - Use the Schema builder to create or modify tables
- Follow Laravel's migration conventions
Seeders
Seeders are used to populate your database with test data. They are located in the database/seeders
directory.
Example:
<?php
namespace Botble\Foo\Database\Seeders;
use Botble\Base\Supports\BaseSeeder;
use Botble\Foo\Models\Item;
class FooSeeder extends BaseSeeder
{
public function run(): void
{
Item::truncate();
for ($i = 1; $i <= 20; $i++) {
Item::create([
'name' => 'Sample Item ' . $i,
'description' => 'This is a sample item',
'content' => 'Detailed content for sample item ' . $i,
'status' => 'published',
'user_id' => 1,
]);
}
}
}
Helpers
Helpers are utility functions and constants that can be used throughout your plugin.
constants.php
This file defines all PHP constants for your plugin. It must have a constant for its screen name.
Example:
if (!defined('FOO_MODULE_SCREEN_NAME')) {
define('FOO_MODULE_SCREEN_NAME', 'foo');
}
The module screen name constant is used to identify your plugin in various parts of the system, such as when registering custom fields or hooks.
helpers.php
This file contains utility functions specific to your plugin.
Example:
if (!function_exists('foo_get_items')) {
/**
* Get a list of items
*
* @param int $limit
* @param array $with
* @return \Illuminate\Database\Eloquent\Collection
*/
function foo_get_items(int $limit = 10, array $with = [])
{
return app(ItemInterface::class)->advancedGet([
'condition' => [
'status' => 'published',
],
'order_by' => [
'created_at' => 'DESC',
],
'take' => $limit,
'with' => $with,
]);
}
}
Helper functions should:
- Be wrapped in a
!function_exists()
check to avoid conflicts - Have descriptive names prefixed with your plugin name
- Include proper PHPDoc comments
- Use dependency injection through the service container when possible
Resources
The resources directory contains assets, language files, and views for your plugin.
Language Files
Language files contain translations for your plugin's text. They are located in the resources/lang/{locale}
directory.
Example (resources/lang/en/foo.php
):
return [
'name' => 'Foo',
'create' => 'New Item',
'edit' => 'Edit Item',
'items' => [
'name' => 'Items',
'create' => 'Create new item',
'edit' => 'Edit item',
'list' => 'List items',
'menu' => 'Items',
'settings' => 'Settings',
'form' => [
'name' => 'Name',
'description' => 'Description',
'content' => 'Content',
'image' => 'Image',
'status' => 'Status',
],
],
];
To support multiple languages, create additional language files in the appropriate directories. For example, for Vietnamese support, create resources/lang/vi/foo.php
.
Views
Views contain the HTML for your plugin's pages. They are located in the resources/views
directory.
Example (resources/views/create.blade.php
):
{!! Form::open(['route' => 'foo.create']) !!}
<div class="row">
<div class="col-md-9">
<div class="card">
<div class="card-body">
<div class="form-group mb-3">
<label for="name">{{ trans('core/base::forms.name') }}</label>
<input type="text" class="form-control" name="name" id="name" value="{{ old('name') }}" placeholder="{{ trans('core/base::forms.name_placeholder') }}">
</div>
<div class="form-group mb-3">
<label for="description">{{ trans('core/base::forms.description') }}</label>
<textarea class="form-control" rows="4" name="description" id="description" placeholder="{{ trans('core/base::forms.description_placeholder') }}">{{ old('description') }}</textarea>
</div>
<div class="form-group mb-3">
<label for="content">{{ trans('core/base::forms.content') }}</label>
{!! Form::customEditor('content', old('content')) !!}
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<div class="form-group mb-3">
<label for="status">{{ trans('core/base::tables.status') }}</label>
{!! Form::customSelect('status', \Botble\Base\Enums\BaseStatusEnum::labels(), old('status')) !!}
</div>
<div class="form-group mb-3">
<label>{{ trans('core/base::forms.image') }}</label>
{!! Form::mediaImage('image', old('image')) !!}
</div>
</div>
</div>
<div class="mt-3">
<button class="btn btn-primary" type="submit">{{ trans('core/base::forms.create') }}</button>
<button class="btn btn-danger" type="reset">{{ trans('core/base::forms.reset') }}</button>
</div>
</div>
</div>
{!! Form::close() !!}
Assets
Assets include CSS, JavaScript, and image files for your plugin. They are located in the resources/assets
directory.
Example structure:
resources/assets/
├── css/
│ └── foo.css
├── js/
│ └── foo.js
└── images/
└── logo.png
To publish these assets to the public directory, add the following to your service provider's boot()
method:
$this->publishes([
__DIR__ . '/../../resources/assets' => public_path('vendor/foo'),
], 'foo-assets');
Routes
Routes define the URLs for your plugin's pages. They are located in the routes
directory.
Web Routes
Web routes are defined in routes/web.php
and are used for browser-accessible pages.
Example:
<?php
use Botble\Base\Facades\BaseHelper;
use Illuminate\Support\Facades\Route;
use Botble\Foo\Http\Controllers\FooController;
Route::group(['namespace' => 'Botble\Foo\Http\Controllers', 'middleware' => ['web', 'core']], function () {
Route::group(['prefix' => BaseHelper::getAdminPrefix(), 'middleware' => 'auth'], function () {
Route::group(['prefix' => 'foo', 'as' => 'foo.'], function () {
Route::resource('', 'ItemController')->parameters(['' => 'item']);
Route::delete('items/destroy', [
'as' => 'deletes',
'uses' => 'ItemController@deletes',
'permission' => 'foo.destroy',
]);
Route::get('settings', [
'as' => 'settings',
'uses' => 'FooController@getSettings',
'permission' => 'foo.settings',
]);
Route::post('settings', [
'as' => 'settings.update',
'uses' => 'FooController@postSettings',
'permission' => 'foo.settings',
]);
});
});
// Public routes (no auth required)
if (defined('THEME_MODULE_SCREEN_NAME')) {
Route::group([
'prefix' => 'foo',
'as' => 'public.foo.',
], function () {
Route::get('', [
'as' => 'index',
'uses' => 'PublicController@index',
]);
Route::get('{slug}', [
'as' => 'detail',
'uses' => 'PublicController@detail',
]);
});
}
});
API Routes
API routes are defined in routes/api.php
and are used for API endpoints.
Example:
<?php
use Illuminate\Support\Facades\Route;
Route::group([
'prefix' => 'api/v1',
'namespace' => 'Botble\Foo\Http\Controllers\API',
'middleware' => ['api'],
], function () {
Route::get('items', [
'as' => 'api.foo.index',
'uses' => 'ItemController@index',
]);
Route::get('items/{id}', [
'as' => 'api.foo.show',
'uses' => 'ItemController@show',
]);
});
Routes should be organized by:
- Authentication requirements (auth vs. public)
- Purpose (admin vs. public)
- HTTP method (GET, POST, etc.)
- Resource type (following RESTful conventions)
Source Code
The src
directory contains the PHP source code for your plugin, organized into subdirectories by purpose.
Models
Models represent database tables and define relationships between entities. They are located in the src/Models
directory.
Example (src/Models/Item.php
):
<?php
namespace Botble\Foo\Models;
use Botble\Base\Casts\SafeContent;
use Botble\Base\Enums\BaseStatusEnum;
use Botble\Base\Models\BaseModel;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Item extends BaseModel
{
protected $table = 'foo_items';
protected $fillable = [
'name',
'description',
'content',
'status',
'user_id',
'image',
];
protected $casts = [
'status' => BaseStatusEnum::class,
'name' => SafeContent::class,
'description' => SafeContent::class,
];
public function user(): BelongsTo
{
return $this->belongsTo(\Botble\ACL\Models\User::class, 'user_id');
}
}
Controllers
Controllers handle HTTP requests and return responses. They are located in the src/Http/Controllers
directory.
Example (src/Http/Controllers/ItemController.php
):
<?php
namespace Botble\Foo\Http\Controllers;
use Botble\Base\Events\CreatedContentEvent;
use Botble\Base\Events\DeletedContentEvent;
use Botble\Base\Events\UpdatedContentEvent;
use Botble\Base\Http\Controllers\BaseController;
use Botble\Base\Http\Responses\BaseHttpResponse;
use Botble\Foo\Forms\ItemForm;
use Botble\Foo\Http\Requests\ItemRequest;
use Botble\Foo\Models\Item;
use Botble\Foo\Tables\ItemTable;
use Exception;
use Illuminate\Http\Request;
class ItemController extends BaseController
{
public function index(ItemTable $table)
{
$this->pageTitle(trans('plugins/foo::items.name'));
return $table->renderTable();
}
public function create()
{
$this->pageTitle(trans('plugins/foo::items.create'));
return ItemForm::create()->renderForm();
}
public function store(ItemRequest $request, BaseHttpResponse $response)
{
$item = Item::query()->create($request->validated());
event(new CreatedContentEvent(FOO_MODULE_SCREEN_NAME, $request, $item));
return $response
->setPreviousUrl(route('foo.index'))
->setNextUrl(route('foo.edit', $item->id))
->setMessage(trans('core/base::notices.create_success_message'));
}
public function edit(Item $item)
{
$this->pageTitle(trans('plugins/foo::items.edit') . ' "' . $item->name . '"');
return ItemForm::createFromModel($item)->renderForm();
}
public function update(Item $item, ItemRequest $request, BaseHttpResponse $response)
{
$item->update($request->validated());
event(new UpdatedContentEvent(FOO_MODULE_SCREEN_NAME, $request, $item));
return $response
->setPreviousUrl(route('foo.index'))
->setMessage(trans('core/base::notices.update_success_message'));
}
public function destroy(Item $item, Request $request, BaseHttpResponse $response)
{
try {
$item->delete();
event(new DeletedContentEvent(FOO_MODULE_SCREEN_NAME, $request, $item));
return $response->setMessage(trans('core/base::notices.delete_success_message'));
} catch (Exception $exception) {
return $response
->setError()
->setMessage($exception->getMessage());
}
}
}
Requests
Requests handle validation of form submissions. They are located in the src/Http/Requests
directory.
Example (src/Http/Requests/ItemRequest.php
):
<?php
namespace Botble\Foo\Http\Requests;
use Botble\Base\Enums\BaseStatusEnum;
use Botble\Support\Http\Requests\Request;
use Illuminate\Validation\Rule;
class ItemRequest extends Request
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:400',
'content' => 'nullable|string',
'status' => Rule::in(BaseStatusEnum::values()),
'image' => 'nullable|string|max:255',
];
}
public function attributes(): array
{
return [
'name' => trans('plugins/foo::items.form.name'),
'description' => trans('plugins/foo::items.form.description'),
'content' => trans('plugins/foo::items.form.content'),
'status' => trans('core/base::tables.status'),
'image' => trans('core/base::forms.image'),
];
}
}
Forms
Forms handle the creation and editing of models. They are located in the src/Forms
directory.
Example (src/Forms/ItemForm.php
):
<?php
namespace Botble\Foo\Forms;
use Botble\Base\Enums\BaseStatusEnum;
use Botble\Base\Forms\FieldOptions\TextFieldOption;
use Botble\Base\Forms\FieldOptions\TextareaFieldOption;
use Botble\Base\Forms\FieldOptions\SelectFieldOption;
use Botble\Base\Forms\FieldOptions\MediaImageFieldOption;
use Botble\Base\Forms\Fields\TextField;
use Botble\Base\Forms\Fields\TextareaField;
use Botble\Base\Forms\Fields\SelectField;
use Botble\Base\Forms\Fields\MediaImageField;
use Botble\Base\Forms\Fields\EditorField;
use Botble\Base\Forms\FormAbstract;
use Botble\Foo\Http\Requests\ItemRequest;
use Botble\Foo\Models\Item;
class ItemForm extends FormAbstract
{
public function buildForm(): void
{
$this
->setupModel(new Item())
->setValidatorClass(ItemRequest::class)
->withCustomFields()
->add('name', TextField::class, TextFieldOption::make()
->label(trans('core/base::forms.name'))
->placeholder(trans('core/base::forms.name_placeholder'))
->required()
)
->add('description', TextareaField::class, TextareaFieldOption::make()
->label(trans('core/base::forms.description'))
->placeholder(trans('core/base::forms.description_placeholder'))
->rows(4)
)
->add('content', EditorField::class, [
'label' => trans('core/base::forms.content'),
'placeholder' => trans('core/base::forms.content_placeholder'),
'rows' => 4,
])
->add('status', SelectField::class, SelectFieldOption::make()
->label(trans('core/base::tables.status'))
->choices(BaseStatusEnum::labels())
)
->add('image', MediaImageField::class, MediaImageFieldOption::make()
->label(trans('core/base::forms.image'))
)
->setBreakFieldPoint('status');
}
}
Tables
Tables handle the display of data in the admin panel. They are located in the src/Tables
directory.
Example (src/Tables/ItemTable.php
):
<?php
namespace Botble\Foo\Tables;
use Botble\Base\Enums\BaseStatusEnum;
use Botble\Foo\Models\Item;
use Botble\Table\Abstracts\TableAbstract;
use Botble\Table\Actions\DeleteAction;
use Botble\Table\Actions\EditAction;
use Botble\Table\BulkActions\DeleteBulkAction;
use Botble\Table\Columns\CreatedAtColumn;
use Botble\Table\Columns\IdColumn;
use Botble\Table\Columns\NameColumn;
use Botble\Table\Columns\StatusColumn;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
class ItemTable extends TableAbstract
{
public function setup(): void
{
$this
->model(Item::class)
->addActions([
EditAction::make()
->route('foo.edit'),
DeleteAction::make()
->route('foo.destroy'),
]);
}
public function columns(): array
{
return [
IdColumn::make(),
NameColumn::make()->route('foo.edit'),
CreatedAtColumn::make(),
StatusColumn::make(),
];
}
public function buttons(): array
{
return $this->addCreateButton(route('foo.create'));
}
public function bulkActions(): array
{
return [
DeleteBulkAction::make()->permission('foo.destroy'),
];
}
public function getFilters(): array
{
return [
'name' => [
'title' => trans('core/base::tables.name'),
'type' => 'text',
'validate' => 'required|string|max:120',
],
'status' => [
'title' => trans('core/base::tables.status'),
'type' => 'select',
'choices' => BaseStatusEnum::labels(),
'validate' => 'required|in:' . implode(',', BaseStatusEnum::values()),
],
'created_at' => [
'title' => trans('core/base::tables.created_at'),
'type' => 'date',
],
];
}
}
Repositories
Repositories handle data access and business logic. They are located in the src/Repositories
directory.
Example interface (src/Repositories/Interfaces/ItemInterface.php
):
<?php
namespace Botble\Foo\Repositories\Interfaces;
use Botble\Support\Repositories\Interfaces\RepositoryInterface;
interface ItemInterface extends RepositoryInterface
{
}
Example implementation (src/Repositories/Eloquent/ItemRepository.php
):
<?php
namespace Botble\Foo\Repositories\Eloquent;
use Botble\Support\Repositories\Eloquent\RepositoriesAbstract;
use Botble\Foo\Repositories\Interfaces\ItemInterface;
class ItemRepository extends RepositoriesAbstract implements ItemInterface
{
}
Service Provider
The service provider is the main entry point of your plugin. It registers routes, views, translations, and other resources. A plugin must have this file.
Example:
<?php
namespace Botble\Foo\Providers;
use Botble\Base\Facades\DashboardMenu;
use Botble\Base\Traits\LoadAndPublishDataTrait;
use Botble\Foo\Models\Item;
use Botble\Foo\Repositories\Interfaces\ItemInterface;
use Botble\Foo\Repositories\Eloquent\ItemRepository;
use Illuminate\Support\ServiceProvider;
class FooServiceProvider extends ServiceProvider
{
use LoadAndPublishDataTrait;
public function register(): void
{
// Register bindings for repositories
$this->app->bind(ItemInterface::class, function () {
return new ItemRepository(new Item());
});
// Load helpers
$this->app->make('files')->requireOnce(__DIR__ . '/../../helpers/constants.php');
$this->app->make('files')->requireOnce(__DIR__ . '/../../helpers/helpers.php');
}
public function boot(): void
{
$this
->setNamespace('plugins/foo') // Set namespace for views, translations, etc.
->loadHelpers() // Load all helpers in the helpers folder
->loadAndPublishConfigurations(['permissions']) // Load and publish config files
->loadMigrations() // Load migrations
->loadAndPublishTranslations() // Load and publish translations
->loadAndPublishViews() // Load and publish views
->loadRoutes(['web']); // Load routes with web middleware
// Register menu items in the admin dashboard
DashboardMenu::default()->beforeRetrieving(function (): void {
DashboardMenu::make()
->registerItem([
'id' => 'cms-plugins-foo', // Unique ID for the menu item
'priority' => 5, // Position in the menu
'parent_id' => null, // Parent ID for submenu items
'name' => 'plugins/foo::foo.name', // Translation key for the menu name
'icon' => 'ti ti-box', // Icon for the menu item
'url' => route('foo.index'), // URL for the menu item
'permissions' => ['foo.index'], // Required permissions to see this menu
]);
});
// Add additional hooks, filters, or actions here
$this->app->booted(function () {
// Code to run after the application is fully booted
});
}
}
The service provider has two main methods:
register()
: Used to bind implementations to the service container. This is where you register repositories, services, and load helpers.boot()
: Called after all other service providers have been registered. This is where you load and publish assets, register routes, and add menu items.
The LoadAndPublishDataTrait
provides several helpful methods:
setNamespace()
: Sets the namespace for views, translations, and other assetsloadHelpers()
: Loads all PHP files from the helpers directoryloadAndPublishConfigurations()
: Loads and publishes configuration filesloadMigrations()
: Loads database migrationsloadAndPublishTranslations()
: Loads and publishes translation filesloadAndPublishViews()
: Loads and publishes view filesloadRoutes()
: Loads route files
Best Practices
Naming Conventions
- Plugin Name: Use PascalCase for plugin names (e.g.,
Foo
,Ecommerce
) - Database Tables: Prefix tables with your plugin name (e.g.,
foo_items
,foo_categories
) - Routes: Use kebab-case for route names (e.g.,
foo.items.create
) - Translation Keys: Use dot notation and lowercase (e.g.,
plugins/foo::items.create
) - Constants: Use SCREAMING_SNAKE_CASE for constants (e.g.,
FOO_MODULE_SCREEN_NAME
) - Classes: Use PascalCase for class names (e.g.,
ItemController
,FooServiceProvider
) - Methods: Use camelCase for method names (e.g.,
getItems()
,createItem()
) - Variables: Use snake_case for variables (e.g.,
$item_count
,$user_id
)
Code Organization
- Keep your code organized in appropriate directories
- Use namespaces consistently
- Follow PSR-12 coding standards
- Use type hints and return types in PHP 8.2+
- Add proper PHPDoc comments to all classes and methods
Security
- Always validate user input using Request classes
- Use the permission system to restrict access to features
- Sanitize output using the
SafeContent
cast - Use Laravel's built-in protection against CSRF, XSS, and SQL injection
Performance
- Use eager loading to avoid N+1 query problems
- Cache frequently accessed data
- Optimize database queries
- Use indexes on database columns that are frequently queried
Advanced Plugin Development
Creating Custom Database Seeders
Seeders are useful for populating your database with test data or default content. To create a custom seeder for your plugin:
- Create a seeder class in the
database/seeders
directory - Register the seeder in your plugin's service provider
Example seeder registration in your service provider:
if (app()->environment() !== 'production') {
$this->app->register(
\Botble\Foo\Database\Seeders\FooSeederProvider::class
);
}
Creating Commands
You can add custom Artisan commands to your plugin by creating command classes in the src/Commands
directory.
Example command class (src/Commands/GenerateFooCommand.php
):
<?php
namespace Botble\Foo\Commands;
use Illuminate\Console\Command;
class GenerateFooCommand extends Command
{
protected $signature = 'foo:generate {name : The name of the item}';
protected $description = 'Generate a new foo item';
public function handle(): int
{
$name = $this->argument('name');
// Your command logic here
$this->info("Generated new foo item: {$name}");
return self::SUCCESS;
}
}
Register the command in your service provider:
if ($this->app->runningInConsole()) {
$this->commands([
\Botble\Foo\Commands\GenerateFooCommand::class,
]);
}
Creating Events and Listeners
Events and listeners allow you to decouple various aspects of your application. To add events to your plugin:
- Create event classes in the
src/Events
directory - Create listener classes in the
src/Listeners
directory - Register them in an event service provider
Example event (src/Events/FooCreated.php
):
<?php
namespace Botble\Foo\Events;
use Botble\Foo\Models\Item;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class FooCreated
{
use Dispatchable, SerializesModels;
public function __construct(public Item $item)
{
}
}
Example listener (src/Listeners/SendFooCreatedNotification.php
):
<?php
namespace Botble\Foo\Listeners;
use Botble\Foo\Events\FooCreated;
class SendFooCreatedNotification
{
public function handle(FooCreated $event): void
{
// Send notification logic here
}
}
Register in an event service provider (src/Providers/EventServiceProvider.php
):
<?php
namespace Botble\Foo\Providers;
use Botble\Foo\Events\FooCreated;
use Botble\Foo\Listeners\SendFooCreatedNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
FooCreated::class => [
SendFooCreatedNotification::class,
],
];
}
Then register this provider in your main service provider:
public function register(): void
{
$this->app->register(EventServiceProvider::class);
}
Creating Middleware
Middleware provides a convenient mechanism for filtering HTTP requests entering your application. To add middleware to your plugin:
- Create middleware classes in the
src/Http/Middleware
directory - Register them in your service provider
Example middleware (src/Http/Middleware/CheckFooPermission.php
):
<?php
namespace Botble\Foo\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CheckFooPermission
{
public function handle(Request $request, Closure $next)
{
if (!Auth::user()->hasPermission('foo.index')) {
return redirect()->route('dashboard.index');
}
return $next($request);
}
}
Register in your service provider:
protected function registerMiddlewares(): void
{
$this->app['router']->aliasMiddleware('foo.permission', \Botble\Foo\Http\Middleware\CheckFooPermission::class);
}
public function boot(): void
{
// ...
$this->registerMiddlewares();
}
Creating Facades
Facades provide a static interface to classes that are available in the application's service container. To create a facade for your plugin:
- Create a facade class in the
src/Facades
directory - Register the underlying class in your service provider
Example facade (src/Facades/FooHelper.php
):
<?php
namespace Botble\Foo\Facades;
use Botble\Foo\Supports\FooHelper as FooHelperSupport;
use Illuminate\Support\Facades\Facade;
/**
* @method static array getItems(array $args = [])
*
* @see \Botble\Foo\Supports\FooHelper
*/
class FooHelper extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'foo-helper';
}
}
Implementation class (src/Supports/FooHelper.php
):
<?php
namespace Botble\Foo\Supports;
use Botble\Foo\Repositories\Interfaces\ItemInterface;
class FooHelper
{
public function __construct(protected ItemInterface $itemRepository)
{
}
public function getItems(array $args = []): array
{
return $this->itemRepository->advancedGet([
'condition' => [
'status' => 'published',
],
'order_by' => [
'created_at' => 'DESC',
],
'take' => $args['limit'] ?? 10,
'with' => $args['with'] ?? [],
]);
}
}
Register in your service provider:
public function register(): void
{
$this->app->bind('foo-helper', function () {
return new FooHelper(
app(ItemInterface::class)
);
});
}
Frequently Asked Questions
How do I add a settings page for my plugin?
To add a settings page:
- Create a controller method to handle the settings form
- Register a route for the settings page
- Add the settings page to the CMS settings menu
Example controller methods:
public function getSettings()
{
$this->pageTitle(trans('plugins/foo::foo.settings'));
return view('plugins/foo::settings', [
'itemsPerPage' => setting('foo_items_per_page', 10),
'displayAuthor' => setting('foo_display_author', true),
]);
}
public function postSettings(Request $request, BaseHttpResponse $response)
{
setting([
'foo_items_per_page' => $request->input('foo_items_per_page'),
'foo_display_author' => $request->input('foo_display_author'),
])->save();
return $response
->setMessage(trans('core/base::notices.update_success_message'));
}
Add to settings menu in your service provider:
if (defined('SETTING_MODULE_SCREEN_NAME')) {
add_filter('cms_settings_pages', function ($pages) {
return array_merge($pages, [
'foo' => [
'name' => 'plugins/foo::foo.settings',
'icon' => 'ti ti-box',
'view' => 'plugins/foo::settings',
'route' => 'foo.settings',
],
]);
});
}
How do I add custom validation rules?
To add custom validation rules:
- Create a rule class in the
src/Rules
directory - Use the rule in your request classes
Example rule (src/Rules/UniqueItemSlug.php
):
<?php
namespace Botble\Foo\Rules;
use Botble\Foo\Repositories\Interfaces\ItemInterface;
use Illuminate\Contracts\Validation\Rule;
class UniqueItemSlug implements Rule
{
protected int $itemId;
public function __construct(int $itemId = 0)
{
$this->itemId = $itemId;
}
public function passes($attribute, $value): bool
{
$itemRepository = app(ItemInterface::class);
$item = $itemRepository->getFirstBy(['slug' => $value]);
if (!$item) {
return true;
}
if ($item->id === $this->itemId) {
return true;
}
return false;
}
public function message(): string
{
return trans('plugins/foo::validation.slug_unique');
}
}
Usage in a request class:
public function rules(): array
{
return [
'slug' => ['required', 'string', 'max:255', new UniqueItemSlug($this->route('item'))],
];
}
How do I add a widget to the dashboard?
To add a widget to the dashboard:
- Create a widget class in the
src/Widgets
directory - Register the widget in your service provider
Example widget (src/Widgets/RecentItemsWidget.php
):
<?php
namespace Botble\Foo\Widgets;
use Botble\Base\Widgets\Card;
use Botble\Foo\Repositories\Interfaces\ItemInterface;
class RecentItemsWidget extends Card
{
public function __construct(protected ItemInterface $itemRepository)
{
parent::__construct();
$this->setTitle(trans('plugins/foo::foo.recent_items'));
$this->setIcon('ti ti-box');
$this->setColor('#f3c200');
$this->setRoute(route('foo.index'));
$this->setPermission('foo.index');
}
public function getStats(): int
{
return $this->itemRepository->count(['status' => 'published']);
}
}
Register in your service provider:
public function boot(): void
{
// ...
add_filter(DASHBOARD_FILTER_ADMIN_LIST, function ($widgets) {
return (new RecentItemsWidget(app(ItemInterface::class)))
->setColumn('col-md-3 col-sm-6')
->init($widgets);
}, 9, 1);
}
How do I add a custom filter to a table?
To add a custom filter to a table:
- Override the
getFilters()
method in your table class - Add your custom filter logic
Example custom filter:
public function getFilters(): array
{
return [
'category_id' => [
'title' => trans('plugins/foo::foo.category'),
'type' => 'select',
'choices' => app(CategoryInterface::class)->pluck('name', 'id'),
'validate' => 'required|integer',
],
'status' => [
'title' => trans('core/base::tables.status'),
'type' => 'select',
'choices' => BaseStatusEnum::labels(),
'validate' => 'required|in:' . implode(',', BaseStatusEnum::values()),
],
'created_at' => [
'title' => trans('core/base::tables.created_at'),
'type' => 'date',
],
];
}
public function applyFilterCondition($query, string $key, string $operator, ?string $value)
{
switch ($key) {
case 'category_id':
if (!$value) {
break;
}
return $query->whereHas('categories', function ($query) use ($value) {
return $query->where('foo_item_categories.category_id', $value);
});
default:
return parent::applyFilterCondition($query, $key, $operator, $value);
}
}
How do I add a custom action button to a table?
To add a custom action button to a table:
- Create a custom action class in the
src/Tables/Actions
directory - Add the action to your table's
addActions()
method
Example custom action (src/Tables/Actions/DuplicateAction.php
):
<?php
namespace Botble\Foo\Tables\Actions;
use Botble\Table\Actions\Action;
class DuplicateAction extends Action
{
public static function make(string $name = 'duplicate'): static
{
return parent::make($name)
->label(trans('plugins/foo::foo.duplicate'))
->color('primary')
->icon('ti ti-copy')
->attributes([
'data-action' => 'duplicate',
]);
}
}
Add to your table class:
public function setup(): void
{
$this
->model(Item::class)
->addActions([
EditAction::make()
->route('foo.edit'),
DuplicateAction::make()
->route('foo.duplicate'),
DeleteAction::make()
->route('foo.destroy'),
]);
}
How do I add a relationship between my plugin's models?
To add relationships between models:
- Define the relationship methods in your model classes
- Set up the necessary foreign keys in your migrations
Example one-to-many relationship:
In your migration:
Schema::create('foo_categories', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->string('slug', 255)->unique();
$table->string('status', 60)->default('published');
$table->timestamps();
});
Schema::create('foo_items', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->string('description', 400)->nullable();
$table->longText('content')->nullable();
$table->string('status', 60)->default('published');
$table->foreignId('category_id')->nullable()->references('id')->on('foo_categories')->onDelete('set null');
$table->timestamps();
});
In your Category model:
public function items(): HasMany
{
return $this->hasMany(Item::class, 'category_id');
}
In your Item model:
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
How do I add SEO metadata to my plugin's content?
To add SEO metadata to your plugin's content:
- Use the SeoHelper facade in your controllers
- Add SEO fields to your forms
Example in a controller:
public function show($slug)
{
$item = $this->itemRepository->getFirstBy(['slug' => $slug, 'status' => 'published']);
if (!$item) {
abort(404);
}
SeoHelper::setTitle($item->name)
->setDescription($item->description)
->setImage(RvMedia::getImageUrl($item->image))
->setUrl($item->url);
return view('plugins/foo::themes.show', compact('item'));
}
Adding SEO fields to a form:
->add('seo_meta', 'seoMeta', [
'label' => trans('packages/seo-helper::seo-helper.meta_box_header'),
]);
How do I create a plugin that depends on another plugin?
To create a plugin that depends on another plugin:
- Check for the dependency in your plugin's service provider
- Register your plugin's services conditionally
Example dependency check:
public function register(): void
{
if (!is_plugin_active('other-plugin')) {
return;
}
// Register your plugin's services here
$this->app->bind(ItemInterface::class, function () {
return new ItemRepository(new Item());
});
}
How do I add a custom shortcode?
To add a custom shortcode:
- Register the shortcode in your service provider
- Create a view for the shortcode
Example shortcode registration:
Shortcode::register('foo-items', trans('plugins/foo::shortcodes.foo_items.name'), trans('plugins/foo::shortcodes.foo_items.description'), function ($shortcode) {
$limit = $shortcode->limit ?: 6;
$category = $shortcode->category;
$items = app(ItemInterface::class)->advancedGet([
'condition' => [
'status' => BaseStatusEnum::PUBLISHED,
],
'with' => ['slugable'],
'order_by' => [
'created_at' => 'DESC',
],
'take' => $limit,
'withCount' => ['comments'],
]);
return view('plugins/foo::shortcodes.items', compact('items', 'shortcode'))->render();
}, [
'name' => [
'title' => trans('plugins/foo::shortcodes.foo_items.name'),
'type' => 'text',
'tab' => trans('core/base::forms.content'),
],
'limit' => [
'title' => trans('core/base::forms.limit'),
'type' => 'number',
'default_value' => 6,
'tab' => trans('core/base::forms.content'),
],
'category' => [
'title' => trans('plugins/foo::shortcodes.foo_items.category'),
'type' => 'customSelect',
'source' => route('foo.categories.list'),
'tab' => trans('core/base::forms.content'),
],
]);
How do I add a custom widget area?
To add a custom widget area:
- Register the widget area in your service provider
- Create a view for the widget area
Example widget area registration:
if (is_plugin_active('widget')) {
app(WidgetInterface::class)->registerSidebar([
'id' => 'foo_sidebar',
'name' => trans('plugins/foo::foo.widgets.sidebar_name'),
'description' => trans('plugins/foo::foo.widgets.sidebar_description'),
]);
}
To display the widget area in your theme:
{!! dynamic_sidebar('foo_sidebar') !!}
How do I add custom meta boxes to my plugin's forms?
To add custom meta boxes:
- Add the meta box fields to your form class
- Create a migration for the meta data table
- Add the meta data relationship to your model
Example meta box in form:
->add('metadata', 'custom_html', [
'label' => trans('plugins/foo::foo.metadata'),
'wrapper' => [
'class' => 'form-group col-md-12',
],
'html' => '<div class="row"><div class="col-md-6">',
])
->add('meta_title', 'text', [
'label' => trans('core/base::forms.meta_title'),
'wrapper' => [
'class' => 'form-group col-md-6',
],
'value' => $this->getModel()->meta_title,
])
->add('meta_description', 'textarea', [
'label' => trans('core/base::forms.meta_description'),
'wrapper' => [
'class' => 'form-group col-md-6',
],
'value' => $this->getModel()->meta_description,
'attributes' => [
'rows' => 3,
],
])
->add('meta_keywords', 'text', [
'label' => trans('core/base::forms.meta_keywords'),
'wrapper' => [
'class' => 'form-group col-md-6',
],
'value' => $this->getModel()->meta_keywords,
])
->add('metadata_end', 'custom_html', [
'html' => '</div></div>',
])
In your model, add the meta attributes:
/**
* @var array
*/
protected $fillable = [
'name',
'description',
'content',
'status',
'category_id',
'image',
'meta_title',
'meta_description',
'meta_keywords',
];
How do I add custom CSS and JavaScript to my plugin?
To add custom CSS and JavaScript:
- Place your assets in the
public/vendor/your-plugin
directory - Register and load your assets in your service provider
Example in service provider:
public function boot(): void
{
// ...
$this->app->booted(function () {
add_action(BASE_ACTION_ENQUEUE_SCRIPTS, [$this, 'registerAssets'], 99);
});
}
public function registerAssets(): void
{
Assets::addStylesDirectly([
'vendor/core/plugins/foo/css/foo.css',
])
->addScriptsDirectly([
'vendor/core/plugins/foo/js/foo.js',
]);
}
To publish your assets during plugin installation:
$this->publishes([
__DIR__.'/../../resources/assets' => resource_path('vendor/core/plugins/foo'),
__DIR__.'/../../public' => public_path('vendor/core/plugins/foo'),
], 'public');
Conclusion
Following this plugin structure and the best practices outlined in this document will help you create well-organized, maintainable, and secure plugins for Botble CMS. The modular architecture of Botble CMS makes it easy to extend and customize, and by adhering to these conventions, your plugins will integrate seamlessly with the core system.