For Developers

Plugin Development

7 min read

This guide covers how to develop plugins for TallCMS. Plugins extend the CMS with new functionality including custom blocks, routes, admin pages, and more.

Plugin Development Guide

Quick Start

1. Create Plugin Directory Structure

plugins/
└── your-vendor/
    └── your-plugin/
        ├── plugin.json           # Required: Plugin metadata
        ├── src/
        │   ├── Providers/
        │   │   └── YourPluginServiceProvider.php
        │   └── Blocks/           # Optional: Custom blocks
        │       └── ExampleBlock.php
        ├── resources/
        │   └── views/
        │       └── blocks/
        │           └── example.blade.php
        ├── routes/
        │   ├── public.php        # Optional: Public routes
        │   └── web.php           # Optional: Prefixed routes
        └── database/
            └── migrations/       # Optional: Database migrations

2. Create plugin.json

{
    "name": "Your Plugin",
    "slug": "your-plugin",
    "vendor": "your-vendor",
    "version": "1.0.0",
    "description": "A brief description of your plugin",
    "author": "Your Name",
    "namespace": "YourVendor\\YourPlugin",
    "provider": "YourVendor\\YourPlugin\\Providers\\YourPluginServiceProvider",
    "compatibility": {
        "php": "^8.2",
        "tallcms": "^1.0"
    }
}

3. Create Service Provider

<?php

namespace YourVendor\YourPlugin\Providers;

use Illuminate\Support\ServiceProvider;

class YourPluginServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Register bindings, config, etc.
    }

    public function boot(): void
    {
        // Bootstrap your plugin
        // Load views, register components, etc.
    }
}

Plugin Structure

Required Files

FileDescription
plugin.jsonPlugin metadata and configuration
src/Providers/*ServiceProvider.phpService provider class

Optional Directories

DirectoryPurpose
src/Blocks/Custom content blocks
src/Filament/Filament resources, pages, widgets
resources/views/Blade templates
routes/Route definitions
database/migrations/Database migrations
config/Configuration files

Plugin Configuration

Complete plugin.json Example

{
    "name": "Contact Forms Pro",
    "slug": "contact-forms-pro",
    "vendor": "acme",
    "version": "1.2.0",
    "description": "Advanced contact forms with conditional logic",
    "author": "Acme Inc.",
    "author_url": "https://acme.com",
    "namespace": "Acme\\ContactFormsPro",
    "provider": "Acme\\ContactFormsPro\\Providers\\ContactFormsProServiceProvider",
    "compatibility": {
        "php": "^8.2",
        "tallcms": "^1.0"
    },
    "public_routes": ["/form-submit"],
    "filament_plugin": "Acme\\ContactFormsPro\\Filament\\ContactFormsProPlugin"
}

Configuration Fields

FieldRequiredDescription
nameYesDisplay name
slugYesURL-safe identifier
vendorYesVendor/organization name
versionYesSemantic version
namespaceYesPHP namespace
providerYesService provider class
compatibilityNoPHP/TallCMS version requirements
public_routesNoWhitelisted public routes
filament_pluginNoFilament plugin class

Routes

Public Routes (No Prefix)

Public routes require explicit whitelisting in plugin.json:

{
    "public_routes": ["/form-submit", "/webhook"]
}
// routes/public.php
Route::post('/form-submit', FormController::class)->name('form.submit');

Prefixed Routes (Auto-Prefixed)

Routes in routes/web.php are automatically prefixed with /_plugins/{vendor}/{slug}/:

// routes/web.php
Route::get('/dashboard', DashboardController::class)->name('dashboard');
// Accessible at: /_plugins/acme/contact-forms-pro/dashboard

Custom Blocks

Blocks in src/Blocks/ implementing CustomBlockInterface are auto-discovered:

<?php

namespace Acme\MyPlugin\Blocks;

use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
use TallCms\Cms\Contracts\CustomBlockInterface;
use TallCms\Cms\Filament\Blocks\Concerns\HasBlockMetadata;

class MyBlock extends RichContentCustomBlock implements CustomBlockInterface
{
    use HasBlockMetadata;

    public static function getId(): string
    {
        return 'my-block';
    }

    public static function getLabel(): string
    {
        return 'My Block';
    }

    public static function getBlockName(): string
    {
        return 'my-block';
    }

    public static function getBlockLabel(): string
    {
        return 'My Block';
    }

    public static function getBlockIcon(): string
    {
        return 'heroicon-o-star';
    }

    public static function getBlockSchema(): array
    {
        return [
            // Filament form schema
        ];
    }

    public static function getViewName(): string
    {
        return 'acme-plugin::blocks.my-block';
    }
}

Filament Integration

Creating a Filament Plugin

<?php

namespace Acme\MyPlugin\Filament;

use Filament\Contracts\Plugin;
use Filament\Panel;

class MyPlugin implements Plugin
{
    public function getId(): string
    {
        return 'acme-my-plugin';
    }

    public function register(Panel $panel): void
    {
        $panel
            ->resources([
                // Your resources
            ])
            ->pages([
                // Your pages
            ]);
    }

    public function boot(Panel $panel): void
    {
        // Bootstrap code
    }

    public static function make(): static
    {
        return app(static::class);
    }
}

Reference this in plugin.json:

{
    "filament_plugin": "Acme\\MyPlugin\\Filament\\MyPlugin"
}

Views and Assets

Loading Views

public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../../resources/views', 'acme-plugin');
}

Using Views

return view('acme-plugin::blocks.my-block', $data);

Theme Override Support

Themes can override plugin views at:

themes/{theme}/resources/views/vendor/{view-namespace}/

Migrations

Place migrations in database/migrations/:

// database/migrations/2024_01_01_000000_create_my_table.php
<?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('acme_my_table', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('acme_my_table');
    }
};

Security Guidelines

Service Provider Restrictions

The following are blocked in service providers:

  • Route:: facade calls
  • Direct router access
  • Route aliasing

Route File Restrictions

Blocked in route files:

  • Route::any
  • Route::resource
  • Route::group
  • Router instance variables

Namespace Restrictions

Plugins cannot use these namespaces:

  • App\, Database\, Tests\
  • Illuminate\, Laravel\, Filament\
  • Livewire\, Spatie\

Testing Your Plugin

Directory Structure

plugins/your-vendor/your-plugin/
└── tests/
    ├── Feature/
    │   └── BlockTest.php
    └── Unit/
        └── HelperTest.php

Running Tests

cd plugins/your-vendor/your-plugin
./vendor/bin/phpunit

Distribution

Via Composer

Publish to Packagist:

{
    "name": "acme/tallcms-contact-forms",
    "type": "tallcms-plugin",
    "require": {
        "tallcms/cms": "^1.0"
    }
}

Via ZIP Upload

Package as ZIP and distribute through admin panel upload (if enabled).


Common Pitfalls

"Plugin not loading"Check plugin.json is valid JSON and provider class exists.

"Routes not working"Public routes must be whitelisted in public_routes. Prefixed routes use /_plugins/ path.

"Block not appearing"Implement CustomBlockInterface and use the HasBlockMetadata trait.

"Views not found"Verify view namespace registration in service provider.


Next Steps

Choose Theme