Filament Laravel
This filament is something to be recon, I got used to Voyager but this is the next level.
This playlist is pretty straightforward, I understood every bit of it.
laravel new project --jet
# update env for database
composer require filament/filament
php artisan migrate
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\\Permission\\PermissionServiceProvider"
php artisan make:migration add_is_admin_users
Schema::table('users', function(Blueprint $table) {
$table->boolean('is_admin')->after('name')->default(0);
});
use Spatie\\Permission\\Traits\\HasRoles;
class User extends Authenticatable {
use HasRoles;
protected $fillable = [
'is_admin',
];
}
php artisan make:seeder RolesAndPermissionSeeder
# get this to rolesAndPermissionSeeder
<https://gist.github.com/madindo/95e9c7bacd744d27a31760dd0cc31683>
php artisan make:filament-resource Permission --simple
---
use Spatie\Permission\Models\Permission;
protected static ?string $navigationGroup = 'Admin Management';
public static function form(Form $form): Form
{
return $form
->schema([
Card::make()
->schema([
TextInput::make('name')
->unique(ignoreRecord: true)
->required()
])
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')->sortable(),
TextColumn::make('name')->sortable()->searchable(),
TextColumn::make('created_at')
->dateTime('d-M-Y')
->sortable()
->searchable()
])
->filters([
//
])
->actions([
Tables\\Actions\\EditAction::make(),
Tables\\Actions\\DeleteAction::make(),
])
->bulkActions([
Tables\\Actions\\DeleteBulkAction::make(),
]);
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
php artisan make:filament-relation-manager RoleResource permissions name
User Resource w/ soft delete
composer require doctrine/dbal --dev
php artisan make:filament-resource User --generate
// to make list of checkbox relationship
CheckboxList::make('roles')
->relationship('roles', 'name')
->columns(2)
->helperText('Only Choose One!')
->required()
// password changing
Forms\\Components\\TextInput::make('password')
->password()
->maxLength(255)
->dehydrateStateUsing(
static fn(null|string $state):
null|string => filled($state) ? Hash::make($state) : null,
)->required(
static fn(Page $livewire):
string => $livewire instanceOf CreateUser,
)->dehydrated(
static fn(null|string $state):
bool => filled($state),
)->label(
static fn(Page $livewire):
string => $livewire instanceOf EditUser ? 'New Password' : "Password"
),
Customizing user menu
php artisan make:provider FilamentServiceProvider
#add to config app.php
App\\Providers\\FilamentServiceProvider::class,
#boot()
Filament::serving(function() {
if (!empty(auth()->user()) && auth()->user()->is_admin == 1 && auth()->user()->hasAnyRole('super-admin', 'admin', 'moderator')) {
Filament::registerUserMenuItems([
UserMenuItem::make()
->label('Manage Users')
->url(UserResource::getUrl())
->icon('heroicon-s-users'),
UserMenuItem::make()
->label('Manage Roles')
->url(RoleResource::getUrl())
->icon('heroicon-s-cog'),
UserMenuItem::make()
->label('Manage Permission')
->url(PermissionResource::getUrl())
->icon('heroicon-s-key')
]);
}
});
// to hide menu sidebar UserResource.php
protected static bool $shouldRegisterNavigation = false;
Authorization with policies
# User.php add
implements FilamentUser
public function canAccessFilament(): bool
{
return $this->is_admin;
}
php artisan make:policy PermissionPolicy --model=Permission
public function viewAny(User $user)
{
return $user->hasAnyRole(['super-admin', 'admin', 'moderator']);
}
# app/Providers/AuthServiceProvider.php
use App\\Policies\\PermissionPolicy;
protected $policies = [
\\Spatie\\Permission\\Models\\Permission::class => PermissionPolicy::class,
];
Dashboard
php artisan make:filament-widget StatsOverview --stats-overview
protected function getCards(): array
{
return [
Card::make('Unique views', '192.1k'),
Card::make('Bounce rate', '21%'),
Card::make('Average time on page', '3:12'),
];
}
php artisan make:filament-widget BlogPostsChart --chart
protected function getData(): array
{
$users = User::select('created_at')->get()->groupBy(function($users) {
return Carbon::parse($users->created_at)->format('F');
});
$quantities = [];
foreach ($users as $user => $value) {
array_push($quantities, $value->count());
}
return [
'datasets' => [
[
'label' => 'Users Joined',
'data' => $quantities,
'backgroundColor' => [
'rgba(255,99,132,0.2',
'rgba(255,99,132,0.2',
'rgba(255,99,132,0.2',
'rgba(255,99,132,0.2',
]
],
],
'labels' => $users->keys(),
];
}
Custom theme
npm install tippy.js --save-dev
#tailwind.config.js
const colors = require('tailwindcss/colors')
colors: {
danger: colors.rose,
primary: colors.blue,
success: colors.green,
warning: colors.yellow,
},
#vite.config.js
'resources/css/filament.css',
#create new file in resources/css/filament.css
@import '../../vendor/filament/filament/resources/css/app.css';
# open filamentServiceProvider in serving function
Filament::registerViteTheme('resources/css/filament.css');
Make only admin accessible
V2
I saw a few tricks to get this done but this is my way, it's simpler.
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(0);
});
// then in user model
use Filament\Models\Contracts\FilamentUser;
... implements FilamentUser
public function canAccessFilament(): bool
{
return $this->is_admin;
}
V3
<?php
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser
{
// ...
public function canAccessPanel(Panel $panel): bool
{
return str_ends_with($this->email, '@yourdomain.com') && $this->hasVerifiedEmail();
}
}
Import data
I have tried with some plugins but with a new project, the package doesn't work, here's another approach.
First get the package first
composer require maatwebsite/excel
Create the import
php artisan make:import ImportCampaign --model=Campaign
<?php
namespace App\Imports;
use App\Models\Campaign;
use Maatwebsite\Excel\Concerns\ToModel;
class ImportCampaign implements ToModel
{
/**
* @param array $row
*
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
return new Campaign([
'client_id' => $row[0] ?? '',
'date' => $row[1] ?? '',
'name' => $row[2] ?? '',
]);
}
}
Add this to Page/List{model} if User ListUser
// Pages/List{model}
public function getHeader(): ?View {
$data = Actions\CreateAction::make();
return view('filament.custom.upload-file', compact('data'));
}
public function save() {
if ($this->file != '') {
Excel::import(new ImportCampaign, $this->file);
}
}
Look at the get header it return a view, make like this below where wire:submit="save" (on top save())
<div>
<x-filament::breadcrumbs :breadcrumbs="[
'/admin/campaigns' => 'Campaign',
'' => 'List',
]" />
<div class="flex justify-between mt-1">
<h1 class="font-bold text-3xl">Campaign</h1>
<div>{{ $data }}</div>
</div>
</div>
<div>
<form wire:submit="save" class="w-full max-w-sm flex mt-2">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="fileInput">
Upload File
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="fileInput" type="file" wire:model='file'>
</div>
<div class="item-center justify-between mt-3 pl-5">
<br>
<button class="text-black font-bold border rounded px-6 py-2" type="submit">
Upload
</button>
</div>
</form>
</div>
Export data using native Export from Filament 3
php artisan make:notifications-table // to enable notification panel
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate
add ->databaseNotifications() to AdminPanelProvider
php artisan make:filament-exporter Order --generate // it will create a file
make sure php artisan queue:work is working
Custom Pages
In my case, I need to change the way how the form is saved.
php artisan make:filament-page FormResidenceClaim --type=custom
use Filament\Forms\Contracts\HasForms;
use InteractsWithForms;
class FormResidenceClaim extends Page implements HasForms
{
use InteractsWithForms, InteractsWithRecord;
public function mount(int | string $record): void
{
$this->record = $this->resolveRecord($record);
//custom edits
$this->form->fill($this->record->toArray());
}
public function form(Form $form): Form
{
return $form
->schema([
//
])
->statePath('claim');
}
protected function getFormActions(): array
{
return [
Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save'),
];
}
public function save(): void
{
try {
$data = $this->form->getState();
!$data ? Model::create($residenceUser) : $this->record->residenceUser->update($residenceUser);
} catch (Halt $exception) {
return;
}
Notification::make()
->success()
->title(__('filament-panels::resources/pages/edit-record.notifications.saved.title'))
->send();
}
}
Plugins
Settings
composer require reworck/filament-settings
# appServiceProvider - add more in values in here
\Reworck\FilamentSettings\FilamentSettings::setFormFields([
\Filament\Forms\Components\TextInput::make('title'),
\Filament\Forms\Components\Textarea::make('description'),
]);
# in User.php add function below, create permission and add to user if have any
public function canManageSettings(): bool
{
return $this->can('manage.settings');
}