Courses

Creating CRM with Filament 3: Step-By-Step

Creating Customers Resource

It's time to make our first resource - Customer.

In this lesson, we will:

  • Create DB structure for Customers: Model/Migration
  • Create Factories/Seeds for testing data
  • Generate Filament Resource directly from the DB structure
  • Hide the deleted_at column from the table
  • Merge first_name and last_name into one table column

We will have the following fields in our Customer resource:

  • id
  • first_name
  • last_name
  • email
  • phone_number
  • description
  • timestamps
  • soft deletes

Creating Customer Database

Our first step is to create our Database table and Model:

Migration

Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('first_name')->nullable();
$table->string('last_name')->nullable();
$table->string('email')->nullable();
$table->string('phone_number')->nullable();
$table->text('description')->nullable();
$table->timestamps();
$table->softDeletes();
});

As you can see, we have made all of our fields nullable. We don't know if the customer will have all these fields or just some. So, we leave some room for flexibility for our users.

Now, let's create the Model:

app/Models/Customer.php

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
 
class Customer extends Model
{
use SoftDeletes;
use HasFactory;
 
protected $fillable = [
'first_name',
'last_name',
'email',
'phone_number',
'description',
];
}

Since we use the HasFactory trait, we should create a factory for our Customer model. It will be helpful for testing purposes!

php artisan make:factory CustomerFactory

Then, we will add the fields to our factory:

database/factories/CustomerFactory.php

use App\Models\Customer;
use Illuminate\Database\Eloquent\Factories\Factory;
 
/**
* @extends Factory<Customer>
*/
class CustomerFactory extends Factory
{
protected $model = Customer::class;
 
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'first_name' => $this->faker->firstName(),
'last_name' => $this->faker->lastName(),
'email' => $this->faker->unique()->safeEmail(),
'phone_number' => $this->faker->phoneNumber(),
'description' => $this->faker->text(),
];
}
}

Now, we can create a seeder for our Customer model:

database/seeders/DatabaseSeeder.php

use App\Models\Customer;
 
// ...
 
public function run(): void
{
// ...
 
Customer::factory()
->count(10)
->create();
}

We can test our seeder by running:

php artisan migrate:fresh --seed

This command should clear our database, migrate it from scratch, and seed it with our defined seeders. Once it's done, we should be able to see our customers in the database:

This should be enough for us to test our Filament resource. Let's create it!


Creating Customer Resource

To create the base resource, we can use the Filament generator:

php artisan make:filament-resource Customer --generate

After running this command, we should see a few new files in our project:

These files contain all the necessary code to create a resource in Filament, meaning that we can open our browser and visit our Customers page:

.

How cool is that? We just created a model, ran a single command, and Filament generated a resource for us! But let's dive deeper into what we just created and modify some things to make it more useful:


Modifying Customer Resource

Our primary focus should be app/Filament/Resources/CustomerResource.php as this file is responsible for Table and Form generation. Let's take a look at it:

app/Filament/Resources/CustomerResource.php

 
use App\Filament\Resources\CustomerResource\Pages;
use App\Filament\Resources\CustomerResource\RelationManagers;
use App\Models\Customer;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
 
class CustomerResource extends Resource
{
 
// ...
 
public static function form(Form $form): Form
{
// This is where we define what fields we want to have in our form
return $form
->schema([
Forms\Components\TextInput::make('first_name')
->maxLength(255),
Forms\Components\TextInput::make('last_name')
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->maxLength(255),
Forms\Components\TextInput::make('phone_number')
->tel()
->maxLength(255),
Forms\Components\Textarea::make('description')
->maxLength(65535)
->columnSpanFull(),
]);
}
 
public static function table(Table $table): Table
{
// This is where we define our table columns, filters, actions, and any other table-related things
return $table
->columns([
Tables\Columns\TextColumn::make('first_name')
->searchable(),
Tables\Columns\TextColumn::make('last_name')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable(),
Tables\Columns\TextColumn::make('phone_number')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('deleted_at')
->dateTime()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
// ...
}

Table Modifications

But how can we modify this? Well, let's start with something simple - our Customers table has a deleted_at column that we want to hide:

To hide it, we can borrow some code from the created_at column like so:

app/Filament/Resources/CustomerResource.php

 
use App\Filament\Resources\CustomerResource\Pages;
use App\Filament\Resources\CustomerResource\RelationManagers;
use App\Models\Customer;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
 
class CustomerResource extends Resource
{
 
// ...
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('first_name')
->searchable(),
Tables\Columns\TextColumn::make('last_name')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable(),
Tables\Columns\TextColumn::make('phone_number')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
// ...
}

Now, if we refresh our page, we should see that the column is hidden:

But that's not all! We still want a Full Name column in our table, not just the first_name and last_name columns. To do that, we need to create a new column, Name and remove the first_name and last_name columns:

app/Filament/Resources/CustomerResource.php

// ...
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('first_name')
// We are setting the column label to "Name"
->label('Name')
// This function allows us to format the column value
// In this case, we are concatenating first_name and last_name
->formatStateUsing(function ($record) {
return $record->first_name . ' ' . $record->last_name;
})
// This function allows us to inform Filament that this column is searchable
// And also define in which columns the search should be performed
// In this case - first_name and last_name columns
->searchable(['first_name', 'last_name']),
Tables\Columns\TextColumn::make('first_name')
->searchable(),
Tables\Columns\TextColumn::make('last_name')
->searchable(),
// ...
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
// ...

And this is how our table looks now:

And the search also works if we search for a last name:

Or even if we search for a full name:

Form Modifications

Next, let's check that our form is working as expected, too. Open the form and try to create a new customer:

As you can see, even if we enter a valid phone number - we still get an error. This is because we have a validation rule for our phone number field:

app/Filament/Resources/CustomerResource.php

public static function form(Form $form): Form
{
return $form
->schema([
// ...
Forms\Components\TextInput::make('phone_number')
// This is the validation rule that causes the error. We will remove it for now
->tel()
->maxLength(255),
// ...
]);
}

Now, once the rule is removed, we can create a new customer:

As you can see, we have successfully created a new customer!

This is it! You have successfully created your first resource in Filament! Next, we will create a new resource - Lead Sources.

avatar

Why removing the validation rule for phone. Does this means that we allow user to input any data without validation? I guess the phone validation is important from my perspective.

👍 1
avatar

Phone validation comes with it's own set of rules and formatting. It does not really match other common formats, so we removed it and left for everyone to decide on it :)

avatar

Okay, can we explore validation rules with filament in upcoming course on filament. Thanks

avatar

Will take a look at what we can do here!

Most likely we will have a section on custom rule creation to fit fields like phone number in

avatar

Great!

avatar

I have a suggestion for the phone validation

'phone' => 'required|regex:/^([0-9\s-+()]*)$/|min:10',

'mobile' => 'regex:/^([0-9\s-+()]*)$/|min:10|nullable',

This is what I normaly use.

avatar

Phone validation is tricky, as each country might have different rules for formatting. In any case, if it works - it works!

avatar

As of Filament 3.0.94 the error with the phone number is no more I am not getting the error as of today 11/12/2023 doing a restart with github

avatar

please add the command for making a migration and a model in the begining of the leson. php artisan make:model Customer -m

for new people coming to the laravel this can help

👍 3
avatar

Hi, not sure if the migration/model making command will help here a lot. We assume that people who learn filament already know at least the basics of Laravel.

Will think about it, but no promises that it will be updated

avatar
WILLY CHANDRA NEGARA

I agree with this, as for me who is paying 26$ per month i want everything to be clear.

avatar

Hi, what is the best way to extend a model with created_by, edited_by and deleted_by. I think it is important to know who edited the record. Thanks for the help.

avatar

Hello Thomas,

This is Model Activity logs . The spatie/laravel-activitylog package provides easy to use functions to log the activities of the users of your app. It can also automatically log model events. All activity will be stored in the activity_log table.

Check this plugin here: https://spatie.be/docs/laravel-activitylog/v4/introduction

avatar

Hi, thanks for your replay. I would like to extend my database with user_id from the auth-user who changed the record in created_by, edited_by and delted_by

avatar

Check the package database structure.

avatar

Hello, I used this code in the model and it is working well by creating und editing the record, but not by deleting (softdelete).

Sorry, I don't know I can formating the code well

public static function booted(): void

{

User::creating(function (User $user) {

	$user->created_by = auth()->id();

}	

User::deleting(function (User $user) {	

	$user->deleted_by = auth()->id();		

} }

avatar

Hi, I would suggest you to use the spatie activity logs package as it tracks all those actions automatically and more! But if you don't want to do that, you have to remember that deleting works differently. It does not call the ->save() as you would think, so you have to manually add:

$user->save()

After you have set the ID.

avatar

Good morning Modestas, $user->save() works. Thanks for the support and thanks for the recommendation. I will take a look at the package