Tutorial Laravel Filament untuk Pemula

Tutorial Laravel Filament untuk Pemula

Share this post

Tutorial Laravel Filament untuk Pemula

Apa itu Filament?

Filament adalah admin panel yang dibangun khusus untuk Laravel menggunakan TALL stack (Tailwind CSS, Alpine.js, Laravel, dan Livewire). Filament memungkinkan developer membuat interface admin yang modern, responsif, dan powerful dengan sangat cepat.

Mengapa Memilih Filament?

  • Mudah dipelajari: Dokumentasi lengkap dan community support yang baik
  • Produktif: Bisa membuat admin panel dalam hitungan menit
  • Fleksibel: Sangat customizable sesuai kebutuhan
  • Modern: Interface yang clean dan responsive
  • Laravel Native: Terintegrasi sempurna dengan ecosystem Laravel
Laravel Filament Tutorial

Persiapan Awal

1. Membuat Project Laravel Baru

Buat project Laravel 11 baru:

composer create-project --prefer-dist laravel/laravel filament-tutorial cd filament-tutorial

2. Setup Database

Edit file .env:

DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=filament_tutorial DB_USERNAME=root DB_PASSWORD=

Buat database filament_tutorial di MySQL/phpMyAdmin.

3. Install Filament

Install Filament via Composer:

composer require filament/filament:"^3.3" -W

Install Filament panels:

php artisan filament:install --panels

4. Buat User Admin

Buat user admin untuk akses panel:

php artisan make:filament-user

Input:

Membuat Model dan Migration

1. Model Product

Kita akan membuat sistem manajemen produk sederhana. Buat model Product:

php artisan make:model Product -m

Edit migration create_products_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('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('slug')->unique(); $table->text('description')->nullable(); $table->decimal('price', 10, 2); $table->integer('stock')->default(0); $table->string('category'); $table->string('brand')->nullable(); $table->boolean('is_active')->default(true); $table->boolean('is_featured')->default(false); $table->json('images')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('products'); } };

Edit model Product.php:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; class Product extends Model { protected $fillable = [ 'name', 'slug', 'description', 'price', 'stock', 'category', 'brand', 'is_active', 'is_featured', 'images' ]; protected $casts = [ 'price' => 'decimal:2', 'is_active' => 'boolean', 'is_featured' => 'boolean', 'images' => 'array' ]; protected static function boot() { parent::boot(); static::creating(function ($product) { if (!$product->slug) { $product->slug = Str::slug($product->name); } }); } public function getFormattedPriceAttribute(): string { return 'Rp ' . number_format($this->price, 0, ',', '.'); } }

2. Model Category

Buat model Category untuk relasi:

php artisan make:model Category -m

Edit migration create_categories_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('categories', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('slug')->unique(); $table->text('description')->nullable(); $table->string('color')->default('#6366f1'); $table->boolean('is_active')->default(true); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('categories'); } };

Update migration Product untuk menambahkan foreign key:

php artisan make:migration add_category_id_to_products_table
<?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::table('products', function (Blueprint $table) { $table->foreignId('category_id')->nullable()->after('category')->constrained()->nullOnDelete(); }); } public function down(): void { Schema::table('products', function (Blueprint $table) { $table->dropForeign(['category_id']); $table->dropColumn('category_id'); }); } };

Edit model Category.php:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Str; class Category extends Model { protected $fillable = [ 'name', 'slug', 'description', 'color', 'is_active' ]; protected $casts = [ 'is_active' => 'boolean' ]; protected static function boot() { parent::boot(); static::creating(function ($category) { if (!$category->slug) { $category->slug = Str::slug($category->name); } }); } public function products(): HasMany { return $this->hasMany(Product::class); } }

Update model Product untuk relasi:

// Tambahkan di model Product.php use Illuminate\Database\Eloquent\Relations\BelongsTo; public function category(): BelongsTo { return $this->belongsTo(Category::class); }

3. Jalankan Migration

php artisan migrate

Resource dengan --generate

1. Generate Category Resource

Filament menyediakan flag --generate yang secara otomatis membuat form dan table berdasarkan struktur database:

php artisan make:filament-resource Category --generate

Perintah ini akan:

  • Menganalisis struktur table categories
  • Membuat form fields otomatis
  • Membuat table columns otomatis
  • Setup basic CRUD operations

2. Generate Product Resource

php artisan make:filament-resource Product --generate --view

Flag --view akan menambahkan halaman view detail produk.

Mari kita lihat hasil generate dan kemudian modifikasi sesuai kebutuhan.

Modifikasi Category Resource

Edit file app/Filament/Resources/CategoryResource.php:

<?php namespace App\Filament\Resources; use App\Filament\Resources\CategoryResource\Pages; use App\Models\Category; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; class CategoryResource extends Resource { protected static ?string $model = Category::class; protected static ?string $navigationIcon = 'heroicon-o-tag'; protected static ?string $navigationLabel = 'Categories'; protected static ?string $modelLabel = 'Category'; protected static ?string $pluralModelLabel = 'Categories'; protected static ?int $navigationSort = 1; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\Section::make('Category Information') ->description('Enter the category details below') ->schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(fn (string $operation, $state, Forms\Set $set) => $operation === 'create' ? $set('slug', \Illuminate\Support\Str::slug($state)) : null ) ->helperText('The name of the category'), Forms\Components\TextInput::make('slug') ->disabled() ->dehydrated() ->required() ->maxLength(255) ->unique(Category::class, 'slug', ignoreRecord: true) ->helperText('Auto-generated from name'), Forms\Components\ColorPicker::make('color') ->default('#6366f1') ->helperText('Choose a color for this category'), Forms\Components\Toggle::make('is_active') ->default(true) ->helperText('Enable or disable this category'), ]) ->columns(2), Forms\Components\Section::make('Additional Information') ->schema([ Forms\Components\Textarea::make('description') ->maxLength(1000) ->rows(3) ->helperText('Optional description for this category'), ]) ->collapsed() ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name') ->searchable() ->sortable() ->weight('bold'), Tables\Columns\TextColumn::make('slug') ->searchable() ->sortable() ->copyable() ->copyMessage('Slug copied!') ->badge() ->color('gray'), Tables\Columns\ColorColumn::make('color') ->label('Color'), Tables\Columns\TextColumn::make('products_count') ->counts('products') ->label('Products') ->badge() ->color('success'), Tables\Columns\IconColumn::make('is_active') ->boolean() ->label('Active') ->sortable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ Tables\Filters\TernaryFilter::make('is_active') ->label('Active Status') ->boolean() ->trueLabel('Only Active Categories') ->falseLabel('Only Inactive Categories') ->native(false), Tables\Filters\Filter::make('has_products') ->query(fn ($query) => $query->has('products')) ->label('Has Products'), ]) ->actions([ Tables\Actions\ActionGroup::make([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ]) ->label('Actions') ->icon('heroicon-m-ellipsis-vertical') ->size('sm') ->color('gray') ->button() ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), Tables\Actions\BulkAction::make('activate') ->label('Activate Selected') ->icon('heroicon-o-check-circle') ->color('success') ->action(fn ($records) => $records->each->update(['is_active' => true])) ->requiresConfirmation(), Tables\Actions\BulkAction::make('deactivate') ->label('Deactivate Selected') ->icon('heroicon-o-x-circle') ->color('danger') ->action(fn ($records) => $records->each->update(['is_active' => false])) ->requiresConfirmation(), ]), ]) ->emptyStateActions([ Tables\Actions\CreateAction::make(), ]) ->defaultSort('name', 'asc'); } public static function getPages(): array { return [ 'index' => Pages\ListCategories::route('/'), 'create' => Pages\CreateCategory::route('/create'), 'edit' => Pages\EditCategory::route('/{record}/edit'), ]; } public static function getNavigationBadge(): ?string { return static::getModel()::count(); } }

Modifikasi Product Resource

Edit file app/Filament/Resources/ProductResource.php:

<?php namespace App\Filament\Resources; use App\Filament\Resources\ProductResource\Pages; use App\Models\Product; use App\Models\Category; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; use Filament\Support\Enums\FontWeight; use Illuminate\Database\Eloquent\Builder; class ProductResource extends Resource { protected static ?string $model = Product::class; protected static ?string $navigationIcon = 'heroicon-o-cube'; protected static ?string $navigationLabel = 'Products'; protected static ?int $navigationSort = 2; protected static ?string $recordTitleAttribute = 'name'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\Group::make() ->schema([ Forms\Components\Section::make('Product Information') ->schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(fn (string $operation, $state, Forms\Set $set) => $operation === 'create' ? $set('slug', \Illuminate\Support\Str::slug($state)) : null ), Forms\Components\TextInput::make('slug') ->disabled() ->dehydrated() ->required() ->maxLength(255) ->unique(Product::class, 'slug', ignoreRecord: true), Forms\Components\Textarea::make('description') ->maxLength(1000) ->rows(4) ->columnSpanFull(), ]) ->columns(2), Forms\Components\Section::make('Pricing & Inventory') ->schema([ Forms\Components\TextInput::make('price') ->required() ->numeric() ->prefix('Rp') ->minValue(0) ->step(0.01), Forms\Components\TextInput::make('stock') ->required() ->numeric() ->default(0) ->minValue(0) ->step(1), ]) ->columns(2), ]) ->columnSpan(['lg' => 2]), Forms\Components\Group::make() ->schema([ Forms\Components\Section::make('Associations') ->schema([ Forms\Components\Select::make('category_id') ->relationship('category', 'name') ->required() ->searchable() ->preload() ->createOptionForm([ Forms\Components\TextInput::make('name') ->required(), Forms\Components\ColorPicker::make('color') ->default('#6366f1'), Forms\Components\Toggle::make('is_active') ->default(true), ]), Forms\Components\TextInput::make('brand') ->maxLength(255), ]), Forms\Components\Section::make('Status') ->schema([ Forms\Components\Toggle::make('is_active') ->default(true) ->helperText('Enable/disable this product'), Forms\Components\Toggle::make('is_featured') ->helperText('Feature this product on homepage'), ]), ]) ->columnSpan(['lg' => 1]), ]) ->columns(3); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name') ->searchable() ->sortable() ->weight(FontWeight::Bold) ->description(fn (Product $record): string => $record->brand ?? 'No brand'), Tables\Columns\TextColumn::make('category.name') ->badge() ->searchable() ->sortable() ->color(fn ($record) => $record->category?->color ?? 'gray'), Tables\Columns\TextColumn::make('price') ->money('IDR') ->sortable() ->color('success') ->weight(FontWeight::Bold), Tables\Columns\TextColumn::make('stock') ->badge() ->sortable() ->color(fn (int $state): string => match (true) { $state === 0 => 'danger', $state < 10 => 'warning', default => 'success', }), Tables\Columns\IconColumn::make('is_featured') ->boolean() ->sortable() ->toggleable() ->label('Featured'), Tables\Columns\IconColumn::make('is_active') ->boolean() ->sortable() ->toggleable() ->label('Active'), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ Tables\Filters\SelectFilter::make('category') ->relationship('category', 'name') ->searchable() ->preload() ->multiple(), Tables\Filters\Filter::make('price') ->form([ Forms\Components\TextInput::make('price_from') ->numeric() ->prefix('Rp'), Forms\Components\TextInput::make('price_to') ->numeric() ->prefix('Rp'), ]) ->query(function (Builder $query, array $data): Builder { return $query ->when( $data['price_from'], fn (Builder $query, $price): Builder => $query->where('price', '>=', $price), ) ->when( $data['price_to'], fn (Builder $query, $price): Builder => $query->where('price', '<=', $price), ); }), Tables\Filters\TernaryFilter::make('is_featured') ->label('Featured') ->boolean() ->trueLabel('Only Featured') ->falseLabel('Not Featured') ->native(false), Tables\Filters\TernaryFilter::make('is_active') ->label('Active') ->boolean() ->trueLabel('Only Active') ->falseLabel('Only Inactive') ->native(false), Tables\Filters\Filter::make('low_stock') ->query(fn (Builder $query): Builder => $query->where('stock', '<', 10)) ->label('Low Stock'), ]) ->actions([ Tables\Actions\ActionGroup::make([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), Tables\Actions\Action::make('duplicate') ->icon('heroicon-o-document-duplicate') ->color('warning') ->action(function (Product $record) { $newProduct = $record->replicate(); $newProduct->name = $record->name . ' (Copy)'; $newProduct->slug = null; $newProduct->save(); }) ->successNotificationTitle('Product duplicated successfully'), Tables\Actions\DeleteAction::make(), ]) ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), Tables\Actions\BulkAction::make('feature') ->label('Mark as Featured') ->icon('heroicon-o-star') ->color('warning') ->action(fn ($records) => $records->each->update(['is_featured' => true])), Tables\Actions\BulkAction::make('unfeature') ->label('Remove from Featured') ->icon('heroicon-o-star') ->color('gray') ->action(fn ($records) => $records->each->update(['is_featured' => false])), ]), ]) ->emptyStateActions([ Tables\Actions\CreateAction::make(), ]) ->defaultSort('created_at', 'desc'); } public static function getPages(): array { return [ 'index' => Pages\ListProducts::route('/'), 'create' => Pages\CreateProduct::route('/create'), 'view' => Pages\ViewProduct::route('/{record}'), 'edit' => Pages\EditProduct::route('/{record}/edit'), ]; } public static function getNavigationBadge(): ?string { return static::getModel()::count(); } public static function getGloballySearchableAttributes(): array { return ['name', 'brand', 'category.name']; } }

Membuat Widget untuk Dashboard

1. Stats Overview Widget

php artisan make:filament-widget StatsOverview --stats

Edit app/Filament/Widgets/StatsOverview.php:

<?php namespace App\Filament\Widgets; use App\Models\Product; use App\Models\Category; use Filament\Widgets\StatsOverviewWidget as BaseWidget; use Filament\Widgets\StatsOverviewWidget\Stat; class StatsOverview extends BaseWidget { protected static ?int $sort = 0; protected function getStats(): array { $totalProducts = Product::count(); $activeProducts = Product::where('is_active', true)->count(); $totalCategories = Category::count(); $lowStockProducts = Product::where('stock', '<', 10)->count(); $featuredProducts = Product::where('is_featured', true)->count(); $totalValue = Product::sum('price'); return [ Stat::make('Total Products', $totalProducts) ->description('All products in database') ->descriptionIcon('heroicon-m-cube') ->color('primary') ->extraAttributes([ 'class' => 'cursor-pointer', ]), Stat::make('Active Products', $activeProducts) ->description($totalProducts > 0 ? round(($activeProducts / $totalProducts) * 100, 1) . '% of total' : '0%') ->descriptionIcon('heroicon-m-eye') ->color('success'), Stat::make('Categories', $totalCategories) ->description('Product categories') ->descriptionIcon('heroicon-m-tag') ->color('warning'), Stat::make('Low Stock Items', $lowStockProducts) ->description('Products with stock < 10') ->descriptionIcon($lowStockProducts > 0 ? 'heroicon-m-exclamation-triangle' : 'heroicon-m-check-circle') ->color($lowStockProducts > 0 ? 'danger' : 'success'), Stat::make('Featured Products', $featuredProducts) ->description('Products marked as featured') ->descriptionIcon('heroicon-m-star') ->color('info'), Stat::make('Total Inventory Value', 'Rp ' . number_format($totalValue, 0, ',', '.')) ->description('Sum of all product prices') ->descriptionIcon('heroicon-m-currency-dollar') ->color('success'), ]; } }

2. Latest Products Widget

php artisan make:filament-widget LatestProducts

Edit app/Filament/Widgets/LatestProducts.php:

<?php namespace App\Filament\Widgets; use App\Models\Product; use Filament\Tables; use Filament\Tables\Table; use Filament\Widgets\TableWidget as BaseWidget; class LatestProducts extends BaseWidget { protected static ?int $sort = 2; protected int | string | array $columnSpan = 'full'; public function table(Table $table): Table { return $table ->query(Product::query()->latest()->limit(5)) ->heading('Latest Products') ->columns([ Tables\Columns\TextColumn::make('name') ->weight('bold') ->searchable(), Tables\Columns\TextColumn::make('category.name') ->badge() ->color(fn ($record) => $record->category?->color ?? 'gray'), Tables\Columns\TextColumn::make('price') ->money('IDR') ->color('success') ->weight('bold'), Tables\Columns\TextColumn::make('stock') ->badge() ->color(fn (int $state): string => match (true) { $state === 0 => 'danger', $state < 10 => 'warning', default => 'success', }), Tables\Columns\IconColumn::make('is_active') ->boolean(), Tables\Columns\TextColumn::make('created_at') ->since() ->dateTimeTooltip(), ]) ->actions([ Tables\Actions\Action::make('view') ->url(fn (Product $record): string => ProductResource::getUrl('edit', ['record' => $record])) ->icon('heroicon-o-eye'), ]); } }

3. Product Chart Widget

php artisan make:filament-widget ProductChart --chart

Edit app/Filament/Widgets/ProductChart.php:

<?php namespace App\Filament\Widgets; use App\Models\Category; use Filament\Widgets\ChartWidget; class ProductChart extends ChartWidget { protected static ?string $heading = 'Products by Category'; protected static ?int $sort = 1; protected function getData(): array { $categories = Category::withCount('products')->get(); return [ 'datasets' => [ [ 'label' => 'Products per Category', 'data' => $categories->pluck('products_count')->toArray(), 'backgroundColor' => $categories->pluck('color')->toArray(), 'borderColor' => $categories->pluck('color')->toArray(), ], ], 'labels' => $categories->pluck('name')->toArray(), ]; } protected function getType(): string { return 'doughnut'; } protected function getOptions(): array { return [ 'plugins' => [ 'legend' => [ 'display' => true, ], ], ]; } }

Customisasi Dashboard

1. Custom Dashboard Page

php artisan make:filament-page Dashboard --type=custom

Edit app/Filament/Pages/Dashboard.php:

<?php namespace App\Filament\Pages; use Filament\Pages\Dashboard as BaseDashboard; class Dashboard extends BaseDashboard { protected static ?string $navigationIcon = 'heroicon-o-home'; protected static string $routePath = '/'; public function getColumns(): int | string | array { return [ 'sm' => 1, 'md' => 2, 'lg' => 3, 'xl' => 4, ]; } public function getWidgets(): array { return [ \App\Filament\Widgets\StatsOverview::class, \App\Filament\Widgets\ProductChart::class, \App\Filament\Widgets\LatestProducts::class, ]; } }

2. Menambahkan Navigation Groups

Edit file app/Providers/Filament/AdminPanelProvider.php:

<?php namespace App\Providers\Filament; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Pages; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; use Filament\Widgets; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; class AdminPanelProvider extends PanelProvider { public function panel(Panel $panel): Panel { return $panel ->default() ->id('admin') ->path('admin') ->login() ->colors([ 'primary' => Color::Blue, ]) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->pages([ Pages\Dashboard::class, ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ // Widgets\AccountWidget::class, // Widgets\FilamentInfoWidget::class, ]) ->middleware([ EncryptCookies::class, AddQueuedCookiesToResponse::class, StartSession::class, AuthenticateSession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, DisableBladeIconComponents::class, DispatchServingFilamentEvent::class, ]) ->authMiddleware([ Authenticate::class, ]) ->navigationGroups([ 'Product Management', 'Analytics', 'System', ]) ->sidebarCollapsibleOnDesktop() ->brandName('My Admin Panel') ->favicon('/favicon.ico'); } }

3. Update Resource dengan Navigation Groups

Update CategoryResource.php:

// Tambahkan di CategoryResource protected static ?string $navigationGroup = 'Product Management';

Update ProductResource.php:

// Tambahkan di ProductResource protected static ?string $navigationGroup = 'Product Management';

Seeder untuk Data Sample

1. Category Seeder

php artisan make:seeder CategorySeeder

Edit database/seeders/CategorySeeder.php:

<?php namespace Database\Seeders; use Illuminate\Database\Seeder; use App\Models\Category; class CategorySeeder extends Seeder { public function run(): void { $categories = [ [ 'name' => 'Electronics', 'description' => 'Electronic devices and gadgets', 'color' => '#3B82F6', 'is_active' => true, ], [ 'name' => 'Clothing', 'description' => 'Fashion and apparel', 'color' => '#EF4444', 'is_active' => true, ], [ 'name' => 'Books', 'description' => 'Books and educational materials', 'color' => '#10B981', 'is_active' => true, ], [ 'name' => 'Sports', 'description' => 'Sports equipment and accessories', 'color' => '#F59E0B', 'is_active' => true, ], [ 'name' => 'Home & Garden', 'description' => 'Home improvement and garden supplies', 'color' => '#8B5CF6', 'is_active' => true, ], ]; foreach ($categories as $category) { Category::create($category); } } }

2. Product Seeder

php artisan make:seeder ProductSeeder

Edit database/seeders/ProductSeeder.php:

<?php namespace Database\Seeders; use Illuminate\Database\Seeder; use App\Models\Product; use App\Models\Category; class ProductSeeder extends Seeder { public function run(): void { $electronics = Category::where('name', 'Electronics')->first(); $clothing = Category::where('name', 'Clothing')->first(); $books = Category::where('name', 'Books')->first(); $sports = Category::where('name', 'Sports')->first(); $home = Category::where('name', 'Home & Garden')->first(); $products = [ // Electronics [ 'name' => 'iPhone 15 Pro', 'description' => 'Latest iPhone with advanced features', 'price' => 15000000, 'stock' => 25, 'category_id' => $electronics->id, 'category' => 'Electronics', 'brand' => 'Apple', 'is_active' => true, 'is_featured' => true, ], [ 'name' => 'Samsung Galaxy S24', 'description' => 'Premium Android smartphone', 'price' => 12000000, 'stock' => 30, 'category_id' => $electronics->id, 'category' => 'Electronics', 'brand' => 'Samsung', 'is_active' => true, 'is_featured' => true, ], [ 'name' => 'MacBook Air M3', 'description' => 'Lightweight laptop with M3 chip', 'price' => 20000000, 'stock' => 15, 'category_id' => $electronics->id, 'category' => 'Electronics', 'brand' => 'Apple', 'is_active' => true, 'is_featured' => false, ], [ 'name' => 'AirPods Pro', 'description' => 'Wireless earbuds with noise cancellation', 'price' => 3500000, 'stock' => 5, // Low stock 'category_id' => $electronics->id, 'category' => 'Electronics', 'brand' => 'Apple', 'is_active' => true, 'is_featured' => true, ], // Clothing [ 'name' => 'Nike Air Force 1', 'description' => 'Classic white sneakers', 'price' => 1500000, 'stock' => 50, 'category_id' => $clothing->id, 'category' => 'Clothing', 'brand' => 'Nike', 'is_active' => true, 'is_featured' => false, ], [ 'name' => 'Uniqlo Basic T-Shirt', 'description' => 'Comfortable cotton t-shirt', 'price' => 150000, 'stock' => 100, 'category_id' => $clothing->id, 'category' => 'Clothing', 'brand' => 'Uniqlo', 'is_active' => true, 'is_featured' => false, ], [ 'name' => 'Levi\'s 501 Jeans', 'description' => 'Classic denim jeans', 'price' => 800000, 'stock' => 35, 'category_id' => $clothing->id, 'category' => 'Clothing', 'brand' => 'Levi\'s', 'is_active' => true, 'is_featured' => true, ], // Books [ 'name' => 'Laravel: Up & Running', 'description' => 'Comprehensive guide to Laravel development', 'price' => 500000, 'stock' => 20, 'category_id' => $books->id, 'category' => 'Books', 'brand' => 'O\'Reilly', 'is_active' => true, 'is_featured' => true, ], [ 'name' => 'Clean Code', 'description' => 'A handbook of agile software craftsmanship', 'price' => 600000, 'stock' => 15, 'category_id' => $books->id, 'category' => 'Books', 'brand' => 'Prentice Hall', 'is_active' => true, 'is_featured' => false, ], // Sports [ 'name' => 'Wilson Tennis Racket', 'description' => 'Professional tennis racket', 'price' => 2500000, 'stock' => 8, // Low stock 'category_id' => $sports->id, 'category' => 'Sports', 'brand' => 'Wilson', 'is_active' => true, 'is_featured' => false, ], [ 'name' => 'Yoga Mat Premium', 'description' => 'High-quality yoga mat for exercise', 'price' => 350000, 'stock' => 25, 'category_id' => $sports->id, 'category' => 'Sports', 'brand' => 'Manduka', 'is_active' => true, 'is_featured' => false, ], // Home & Garden [ 'name' => 'IKEA Office Chair', 'description' => 'Ergonomic office chair', 'price' => 1200000, 'stock' => 12, 'category_id' => $home->id, 'category' => 'Home & Garden', 'brand' => 'IKEA', 'is_active' => true, 'is_featured' => false, ], [ 'name' => 'LED Desk Lamp', 'description' => 'Adjustable LED desk lamp', 'price' => 250000, 'stock' => 0, // Out of stock 'category_id' => $home->id, 'category' => 'Home & Garden', 'brand' => 'Philips', 'is_active' => false, // Inactive due to no stock 'is_featured' => false, ], ]; foreach ($products as $product) { Product::create($product); } } }

3. Update DatabaseSeeder

Edit database/seeders/DatabaseSeeder.php:

<?php namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { $this->call([ CategorySeeder::class, ProductSeeder::class, ]); } }

Jalankan seeder:

php artisan db:seed

Fitur Advanced

Tambahkan global search di ProductResource:

// Sudah ada di ProductResource di atas public static function getGloballySearchableAttributes(): array { return ['name', 'brand', 'category.name']; } public static function getGlobalSearchResultDetails(Model $record): array { return [ 'Category' => $record->category?->name, 'Brand' => $record->brand, 'Price' => $record->formatted_price, ]; } public static function getGlobalSearchResultActions(Model $record): array { return [ Action::make('edit') ->iconButton() ->icon('heroicon-o-pencil-square') ->url(static::getUrl('edit', ['record' => $record])), ]; }

2. Custom Actions

Tambahkan custom action untuk duplicate product di ProductResource:

// Sudah ada di table actions di atas Tables\Actions\Action::make('duplicate') ->icon('heroicon-o-document-duplicate') ->color('warning') ->action(function (Product $record) { $newProduct = $record->replicate(); $newProduct->name = $record->name . ' (Copy)'; $newProduct->slug = null; $newProduct->save(); }) ->successNotificationTitle('Product duplicated successfully'),

3. Custom Bulk Actions

Tambahkan bulk actions untuk mengubah status:

// Sudah ada di bulkActions di atas Tables\Actions\BulkAction::make('feature') ->label('Mark as Featured') ->icon('heroicon-o-star') ->color('warning') ->action(fn ($records) => $records->each->update(['is_featured' => true])), Tables\Actions\BulkAction::make('unfeature') ->label('Remove from Featured') ->icon('heroicon-o-star') ->color('gray') ->action(fn ($records) => $records->each->update(['is_featured' => false])),

4. Advanced Filters

Tambahkan filter berdasarkan range harga:

// Sudah ada di filters di atas Tables\Filters\Filter::make('price') ->form([ Forms\Components\TextInput::make('price_from') ->numeric() ->prefix('Rp'), Forms\Components\TextInput::make('price_to') ->numeric() ->prefix('Rp'), ]) ->query(function (Builder $query, array $data): Builder { return $query ->when( $data['price_from'], fn (Builder $query, $price): Builder => $query->where('price', '>=', $price), ) ->when( $data['price_to'], fn (Builder $query, $price): Builder => $query->where('price', '<=', $price), ); }),

Customisasi Appearance

1. Custom Theme Colors

Edit app/Providers/Filament/AdminPanelProvider.php:

->colors([ 'primary' => Color::Blue, 'gray' => Color::Slate, 'success' => Color::Green, 'warning' => Color::Amber, 'danger' => Color::Red, 'info' => Color::Sky, ])
->brandLogo(asset('images/logo.png')) ->brandLogoHeight('2rem') ->brandName('My Store Admin')

3. Dark Mode

->darkMode(false) // Disable dark mode // atau ->darkMode(true) // Force dark mode

Menjalankan Aplikasi

1. Build Assets

npm install npm run build

2. Start Server

php artisan serve

3. Akses Admin Panel

Buka browser dan akses http://localhost:8000/admin

Login dengan:

Fitur-Fitur Penting yang Dipelajari

1. Resource Management

  • Generate Resource: Menggunakan --generate flag
  • CRUD Operations: Create, Read, Update, Delete
  • Form Components: TextInput, Select, Toggle, dll
  • Table Columns: TextColumn, BadgeColumn, IconColumn, dll
  • Relationships: BelongsTo, HasMany

2. Search & Filter

  • Searchable: Kolom yang bisa dicari
  • Sortable: Kolom yang bisa diurutkan
  • Filters: SelectFilter, TernaryFilter, Custom Filter
  • Global Search: Search across resources

3. Actions & Bulk Actions

  • Row Actions: View, Edit, Delete, Custom
  • Bulk Actions: Multiple record operations
  • Custom Actions: Business logic specific actions

4. Dashboard & Widgets

  • Stats Widget: Statistik overview
  • Chart Widget: Visualisasi data
  • Table Widget: Tabel data latest
  • Custom Dashboard: Layout dan widget management

5. Advanced Features

  • Validation: Form validation
  • Notifications: Success/error messages
  • Navigation: Groups, badges, icons
  • Theming: Colors, branding
  • Seeders: Sample data

Tips & Best Practices

1. Performance

// Gunakan eager loading public static function getEloquentQuery(): Builder { return parent::getEloquentQuery()->with(['category']); } // Pagination protected static ?string $recordsPerPageSelectOption = '25,50,100';

2. Security

// Batasi akses berdasarkan user public static function getEloquentQuery(): Builder { return parent::getEloquentQuery()->where('user_id', auth()->id()); }

3. UX/UI

// Gunakan helper text Forms\Components\TextInput::make('slug') ->helperText('Auto-generated from name'), // Confirmations untuk actions berbahaya ->requiresConfirmation() ->modalHeading('Delete product') ->modalDescription('Are you sure you want to delete this product?')

4. Data Integrity

// Soft deletes use Illuminate\Database\Eloquent\SoftDeletes; class Product extends Model { use SoftDeletes; } // Validasi unique Forms\Components\TextInput::make('slug') ->unique(Product::class, 'slug', ignoreRecord: true)

Troubleshooting Common Issues

1. Migration Issues

# Reset dan re-run migrations php artisan migrate:fresh --seed

2. Cache Issues

# Clear semua cache php artisan optimize:clear

3. Permission Issues

# Fix storage permissions php artisan storage:link chmod -R 775 storage bootstrap/cache

4. Asset Issues

# Rebuild assets npm run build

Next Steps untuk Pengembangan

1. Advanced Features

  • File Uploads: Image galleries untuk products
  • Multi-language: Internationalization
  • Exports: PDF, Excel exports
  • Imports: Bulk data import
  • API: REST API dengan Laravel Sanctum

2. Third-party Integrations

  • Payment Gateway: Stripe, PayPal
  • Email: Mail notifications
  • Storage: Amazon S3, Google Cloud
  • Analytics: Google Analytics integration

3. Advanced Widgets

  • Real-time Data: WebSockets dengan Pusher
  • Advanced Charts: Chart.js, ApexCharts
  • Maps: Google Maps integration
  • Calendar: Event management

Kesimpulan

Tutorial ini telah mengcover semua aspek fundamental Filament dari level pemula hingga intermediate:

🎯 Yang Telah Dipelajari:

  • Setup dan instalasi Filament
  • Resource generation dan customization
  • Form dan table management
  • Search, filter, dan sorting
  • Actions dan bulk actions
  • Widget dan dashboard creation
  • Navigation dan theming
  • Data seeding dan best practices

🚀 Skills yang Didapat:

  • Mampu membuat admin panel modern dengan cepat
  • Memahami konsep Resource, Widget, dan Dashboard
  • Menguasai customization form dan table
  • Implementasi search, filter, dan actions
  • Best practices untuk performance dan security

Filament adalah tool yang sangat powerful untuk membuat admin panel Laravel. Dengan foundation yang solid dari tutorial ini, kalian sudah bisa mengembangkan aplikasi admin yang complex dan professional.


Selamat mencoba! Jangan ragu untuk eksperimen dan mengembangkan fitur-fitur tambahan sesuai kebutuhan project kalian.

Resources untuk lanjutan: