Examples
Practical examples of using Filament Twist to build real-world applications.
Basic Blog Addon
Here's a complete example of creating a blog management addon.
1. Create the Addon Structure
bash
php artisan twist:make BlogManagement --group=Content
2. Define the Addon
php
<?php
// app/Addons/Content/BlogManagement/twist.php
use Twist\Addons\AddonRegistrar;
use App\Addons\Content\BlogManagement\BlogManagementAddon;
AddonRegistrar::register(
name: 'blog-management',
path: BlogManagementAddon::class,
panels: ['admin', 'editor']
);
3. Create the Addon Class
php
<?php
// app/Addons/Content/BlogManagement/BlogManagementAddon.php
namespace App\Addons\Content\BlogManagement;
use Twist\Base\BaseAddon;
use Twist\Contracts\HasMigration;
use Twist\Contracts\HasRouteApi;
use Filament\Panel;
class BlogManagementAddon extends BaseAddon implements HasMigration, HasRouteApi
{
public function getId(): string
{
return 'blog-management';
}
public function boot(Panel $panel): void
{
$panel->resources([
Resources\PostResource::class,
Resources\CategoryResource::class,
Resources\TagResource::class,
]);
$panel->pages([
Pages\BlogDashboard::class,
]);
}
public function pathMigrations(): string
{
return __DIR__ . '/Database/Migrations';
}
public function pathRouteApi(): string
{
return __DIR__ . '/Routes/api.php';
}
}
4. Create Models
php
<?php
// app/Addons/Content/BlogManagement/Models/Post.php
namespace App\Addons\Content\BlogManagement\Models;
use Twist\Base\BaseModel;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Post extends BaseModel
{
protected $fillable = [
'title',
'slug',
'content',
'excerpt',
'featured_image',
'status',
'published_at',
'category_id',
'author_id',
];
protected $casts = [
'published_at' => 'datetime',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
public function author(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'author_id');
}
}
5. Create Filament Resources
php
<?php
// app/Addons/Content/BlogManagement/Resources/PostResource.php
namespace App\Addons\Content\BlogManagement\Resources;
use Filament\Forms;
use Filament\Tables;
use Filament\Resources\Resource;
use App\Addons\Content\BlogManagement\Models\Post;
class PostResource extends Resource
{
protected static ?string $model = Post::class;
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static ?string $navigationGroup = 'Blog';
public static function form(Forms\Form $form): Forms\Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->required()
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) =>
$set('slug', \Illuminate\Support\Str::slug($state))
),
Forms\Components\TextInput::make('slug')
->required()
->unique(Post::class, 'slug', ignoreRecord: true),
Forms\Components\Select::make('category_id')
->relationship('category', 'name')
->required(),
Forms\Components\Select::make('tags')
->relationship('tags', 'name')
->multiple()
->preload(),
Forms\Components\Textarea::make('excerpt')
->rows(3),
Forms\Components\RichEditor::make('content')
->required()
->columnSpanFull(),
Forms\Components\FileUpload::make('featured_image')
->image()
->directory('blog'),
Forms\Components\Select::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
])
->default('draft'),
Forms\Components\DateTimePicker::make('published_at'),
]);
}
public static function table(Tables\Table $table): Tables\Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('category.name')
->sortable(),
Tables\Columns\TextColumn::make('status')
->badge()
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'published' => 'success',
'archived' => 'warning',
}),
Tables\Columns\TextColumn::make('published_at')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('author.name')
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
]),
Tables\Filters\SelectFilter::make('category')
->relationship('category', 'name'),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListPosts::route('/'),
'create' => Pages\CreatePost::route('/create'),
'edit' => Pages\EditPost::route('/{record}/edit'),
];
}
}
6. Create API Routes
php
<?php
// app/Addons/Content/BlogManagement/Routes/api.php
use Illuminate\Support\Facades\Route;
use App\Addons\Content\BlogManagement\Controllers\Api\PostController;
Route::prefix('blog')->group(function () {
Route::get('posts', [PostController::class, 'index']);
Route::get('posts/{post}', [PostController::class, 'show']);
Route::get('categories', [PostController::class, 'categories']);
Route::get('tags', [PostController::class, 'tags']);
});
7. Setup and Run
bash
# Register the addon
php artisan twist:setup
# Run migrations
php artisan twist:migrate
# Access your blog management at /admin
Multi-Tenant SaaS Application
Example of building a multi-tenant SaaS application with customer portals.
1. Tenant Model
php
<?php
// app/Models/Tenant.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
protected $fillable = [
'name',
'slug',
'domain',
'database',
'plan',
'is_active',
'settings',
];
protected $casts = [
'is_active' => 'boolean',
'settings' => 'json',
];
public function users()
{
return $this->hasMany(User::class);
}
}
2. Tenant Service
php
<?php
// app/Services/TenantService.php
namespace App\Services;
use App\Models\Tenant;
use Twist\Services\Tenancy\MigrateTenancyService;
use Illuminate\Support\Facades\DB;
class TenantService
{
public function createTenant(array $data): Tenant
{
$tenant = Tenant::create([
'name' => $data['name'],
'slug' => \Illuminate\Support\Str::slug($data['name']),
'domain' => $data['domain'] ?? null,
'plan' => $data['plan'] ?? 'basic',
'is_active' => true,
]);
// Create tenant database
$this->createTenantDatabase($tenant);
// Run tenant migrations
MigrateTenancyService::make()->migrateAddons($tenant);
return $tenant;
}
protected function createTenantDatabase(Tenant $tenant): void
{
$dbName = 'tenant_' . $tenant->id;
DB::statement("CREATE DATABASE IF NOT EXISTS {$dbName}");
$tenant->update(['database' => $dbName]);
}
}
3. Tenant Middleware
php
<?php
// app/Http/Middleware/IdentifyTenant.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Models\Tenant;
use Twist\Facades\Tenancy;
use Twist\Tenancy\DTO\TenantDTO;
class IdentifyTenant
{
public function handle(Request $request, Closure $next)
{
$tenant = $this->resolveTenant($request);
if ($tenant) {
$tenantDTO = new TenantDTO(
id: $tenant->id,
database: $tenant->database,
attributes: $tenant->toArray()
);
Tenancy::initialize($tenantDTO);
}
$response = $next($request);
Tenancy::end();
return $response;
}
protected function resolveTenant(Request $request): ?Tenant
{
// Resolve by subdomain
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
if ($subdomain !== 'www' && $subdomain !== config('app.domain')) {
return Tenant::where('slug', $subdomain)
->where('is_active', true)
->first();
}
// Resolve by custom domain
return Tenant::where('domain', $host)
->where('is_active', true)
->first();
}
}
4. Customer Panel Provider
php
<?php
// app/Providers/CustomerPanelProvider.php
namespace App\Providers;
use Twist\Support\TwistPanelProvider;
use Twist\Classes\TwistClass;
use Filament\Panel;
class CustomerPanelProvider extends TwistPanelProvider
{
public function twist(TwistClass $twist): void
{
$twist
->setPath('dashboard')
->setColor('#059669')
->setPrefixTable('customer_')
->setMiddleware(\App\Http\Middleware\IdentifyTenant::class)
->setMiddleware(\App\Http\Middleware\CustomerAuth::class);
}
public function panel(Panel $panel): Panel
{
return parent::panel($panel)
->authGuard('customer')
->login(\App\Filament\Customer\Pages\Login::class)
->registration(\App\Filament\Customer\Pages\Register::class)
->brandName(fn () => auth('customer')->user()?->tenant?->name ?? 'Customer Portal');
}
}
E-commerce Platform
Example of building an e-commerce platform with multiple addons.
1. Product Management Addon
php
<?php
// app/Addons/Ecommerce/ProductManagement/ProductManagementAddon.php
namespace App\Addons\Ecommerce\ProductManagement;
use Twist\Base\BaseAddon;
use Twist\Contracts\HasMigration;
use Twist\Contracts\HasRouteApi;
use Twist\Contracts\HasHooks;
use Filament\Panel;
class ProductManagementAddon extends BaseAddon implements
HasMigration,
HasRouteApi,
HasHooks
{
public function boot(Panel $panel): void
{
$panel->resources([
Resources\ProductResource::class,
Resources\CategoryResource::class,
Resources\BrandResource::class,
]);
}
public function pathMigrations(): string
{
return __DIR__ . '/Database/Migrations';
}
public function pathRouteApi(): string
{
return __DIR__ . '/Routes/api.php';
}
public function hooks(): void
{
add_action('product.created', [$this, 'onProductCreated']);
add_action('product.updated', [$this, 'onProductUpdated']);
}
public function onProductCreated($product)
{
// Index in search engine
\App\Jobs\IndexProductJob::dispatch($product);
}
public function onProductUpdated($product)
{
// Clear cache
cache()->forget("product.{$product->id}");
}
}
2. Order Management Addon
php
<?php
// app/Addons/Ecommerce/OrderManagement/OrderManagementAddon.php
namespace App\Addons\Ecommerce\OrderManagement;
use Twist\Base\BaseAddon;
use Twist\Contracts\HasMigration;
use Twist\Contracts\HasDispatcher;
use Filament\Panel;
class OrderManagementAddon extends BaseAddon implements
HasMigration,
HasDispatcher
{
public function boot(Panel $panel): void
{
$panel->resources([
Resources\OrderResource::class,
Resources\OrderItemResource::class,
]);
$panel->widgets([
Widgets\OrderStatsWidget::class,
Widgets\RecentOrdersWidget::class,
]);
}
public function pathMigrations(): string
{
return __DIR__ . '/Database/Migrations';
}
public function pathDispatchers(): string
{
return __DIR__ . '/Dispatchers';
}
}
3. Setup Multiple Addons
bash
# Create addons
php artisan twist:make ProductManagement --group=Ecommerce
php artisan twist:make OrderManagement --group=Ecommerce
php artisan twist:make PaymentGateway --group=Ecommerce
php artisan twist:make InventoryManagement --group=Ecommerce
# Register all addons
php artisan twist:setup
# Run migrations
php artisan twist:migrate
Advanced Multi-Panel Setup
Example of a complex application with multiple panels and roles.
1. Panel Configuration
php
<?php
// config/twist.php
return [
'panels' => [
'super-admin', // Super admin panel
'admin', // Regular admin panel
'manager', // Manager panel
'customer', // Customer portal
'api', // API-only panel
],
];
2. Super Admin Panel
php
<?php
// app/Providers/SuperAdminPanelProvider.php
class SuperAdminPanelProvider extends TwistPanelProvider
{
public function twist(TwistClass $twist): void
{
$twist
->setPath('super-admin')
->setColor('#dc2626')
->setDomain('secure.example.com')
->setPrefixTable('sa_')
->setMiddleware(\App\Http\Middleware\SuperAdminOnly::class);
}
}
3. Manager Panel
php
<?php
// app/Providers/ManagerPanelProvider.php
class ManagerPanelProvider extends TwistPanelProvider
{
public function twist(TwistClass $twist): void
{
$twist
->setPath('manager')
->setColor('#0ea5e9')
->setPrefixTable('mgr_')
->setMiddleware(\App\Http\Middleware\ManagerOnly::class);
}
public function panel(Panel $panel): Panel
{
return parent::panel($panel)
->authGuard('manager')
->navigationGroups([
'Sales' => NavigationGroup::make()->icon('heroicon-o-chart-bar'),
'Reports' => NavigationGroup::make()->icon('heroicon-o-document-chart-bar'),
]);
}
}
4. API-Only Panel
php
<?php
// app/Providers/ApiPanelProvider.php
class ApiPanelProvider extends TwistPanelProvider
{
public function twist(TwistClass $twist): void
{
$twist
->setPath('api')
->disloadSetupAddons() // API doesn't need UI addons
->setMiddleware(\App\Http\Middleware\ApiAuthentication::class);
}
public function panel(Panel $panel): Panel
{
// API panel doesn't need UI components
return $panel
->id('api')
->path('api');
}
}
Development Workflow
1. Local Development Setup
bash
#!/bin/bash
# scripts/dev-setup.sh
# Install dependencies
composer install
npm install
# Setup environment
cp .env.example .env
php artisan key:generate
# Setup database
php artisan migrate
# Setup Twist
php artisan twist:setup
php artisan twist:migrate
# Create test tenant
php artisan tinker --execute="
\App\Services\TenantService::make()->createTenant([
'name' => 'Test Company',
'domain' => 'test.localhost',
'plan' => 'premium'
]);
"
# Start development servers
php artisan serve &
npm run dev
2. Testing Addons
php
<?php
// tests/Feature/AddonTest.php
namespace Tests\Feature;
use Tests\TestCase;
use Twist\Facades\Twist;
class AddonTest extends TestCase
{
public function test_addon_loads_correctly()
{
$this->artisan('twist:setup');
$addons = Twist::getAddons();
$this->assertGreaterThan(0, count($addons));
$this->assertTrue(Twist::hasAddon('blog-management'));
}
public function test_addon_migrations_run()
{
$this->artisan('twist:migrate');
$this->assertDatabaseHasTable('posts');
$this->assertDatabaseHasTable('categories');
}
}