Linkeeper - Lesson 02 - Start playing with Filament
Tags: laravel, filament, php, tutorial
Published: 2024-04-10 16:15:15

Ok, now your application is set.

We can dive in something more concrete.

{%- # TOC start (generated with https://github.com/derlin/bitdowntoc) -%}

- [an user can signin, login, logout and manage his account](#an-user-can-signin-login-logout-and-manage-his-account)
- [a link has a name, a status (published or not) and obviously a link](#a-link-has-a-name-a-status-published-or-not-and-obviously-a-link)
   * [Let's think about table schema](#lets-think-about-table-schema)
   * [Let's migrate](#lets-migrate)
   * [Define model and relationship](#define-model-and-relationship)
   * [We need fake datas to plays with](#we-need-fake-datas-to-plays-with)
      + [The factories](#the-factories)
      + [The seeder](#the-seeder)

{%- # TOC end -%}

What we need to manage our links.

Remember what I said in the first part : 

> - an user can signin, login, logout and manage his account
- a user can see a list of their links
- an user can add, edit or delete a link
- an user can have a shareable link to share his links with the world
- a link has a name, a status (published or not) and obviously a link. May be we will add some options later.


## an user can signin, login, logout and manage his account

This is done with Filament. In fact, no completely, we can login, logout but not register or even manage our own account.


![Signin view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6qm92bcpnywky1s4al9.png)

But, you will see it's very easy to add register and profile page for users.

In `app/Providers/Filament/AdminPanelProvider.php` 

![Path to AdminPanelProvider.php](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lh2ru1xnud0vunw8tq38.png)

you have to add only 2 lines under login() function call to implement both functionnalities : 

```php
class AdminPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->default()
            ->id('admin')
            ->path('admin')
            ->login()
            // Lesson 2 : here we add registration and profile page
            ->registration() // registration
            ->profile()      // profile management
            ->colors([
                'primary' => Color::Amber,
            ])
            ->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,
            ]);
    }
}
```

Go back to your login page : `http://linkeeper.com/admin/login` and you will see


![Signin page with register link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3274bw47b1gvcs59fm5.png)

that the link for signup has been added.

If you login, you will see under your menu the new sub menu 'Profile'


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wvvd09d3qj7sr2q59ukj.png)

It allows you to edit your nickname,  your e-mail address and change your password.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mw5hvzh55vbyfsbc52m3.png)

So, after this very cool step we can check the first functionnality : 

> ✅ an user can signin, login, logout and manage his account
- an user can add, edit or delete a link
- an user can have a shareable link to share his links with the world
- a link has a name, a status (published or not) and obviously a link. May be we will add some options later.

## a link has a name, a status (published or not) and obviously a link

I know it is not in the order of the list, but first it's me writing, second I need to define table `links` and its fields before an user can add link, right ?

So let me do this ! 🤣

### Let's think about table schema


Ok, what will be the fields in our table `links` ?

Of course an `id`, an `url`, a short `description`, a `published` status (published or not), a `user_id` foreign key and finally the classicals `created_at` and `updated_at`.

The `user_id` is to define the relationship between a user and a link.

In fact, we can resume this relation by :
- a user can have 0, 1 or many links
- a link (unique by its `id`) belongs to one and only one user

This is why we use a one-to-many relationship.

What do you thing, is something missing ? Tell me in the comment if you think I forgot something.

Ok, now we have to prepare the migration for `links` table.

```bash
php artisan make:migration create_links_table
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/we2824mexb8q2fr00bkt.png)

Now we have to edit this migration to add fields to the table. So, let's edit the migration file

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('links', function (Blueprint $table) {
            $table->id();
            $table->string('url');
            $table->string('description', 255)->nullable();
            $table->boolean('published')->default(false);
            $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('links');
    }
};

```

### Let's migrate

Ok, it's time to migrate. For that, we can use (again) Artisan.

```php
php artisan migrate
```


Remember if you make something wrong or you want to modify immediately the schema, you can rollback your migration with :

```php
php artisan migrate:rollback
```

And if you want to see the status of your migrations, you can use : 

```php
php artisan migrate:status
```
Look at this screenshot captured before running the last migration.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xf2v2tzlf2ba3l9yba3t.png)

We can see two things : 
- first the migration for the `links` table was not run already
- second the three first migrations was launched in the same sequence : the sequence number [1]

### Define model and relationship

The next step is to define the `Link` model and adapt `User` model.

We will use again Artisan : 

```bash
php artisan make:model Link
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gfdmp8zwp3xvv04xmebs.png)

Now we have to tell the model that a link belongs to an user.

Add this function to the model class `Link`: 

```php
    public function user()
    {
        return $this->belongsTo(User::class);
    }
```

To finish with the model, we have to update the model `User` by adding it a function `links` 

```php
    public function links()
    {
        return $this->hasMany(Link::class);
    }
``` 


### We need fake datas to plays with

After defining our database schema, let's do something very important.

Create fake datas to play with. Please do not use production datas from your database during development.
For many reasons such as ethics, confidentiality and security. We are professionals, so let's act accordingly.

#### The factories

By default Laravel comes with `User` factory, so we just need to create a `Link` factory.

```bash
php artisan make:factory Link
```

In the generated `definition` function we put this code : 

```php
    public function definition(): array
    {
        return [
            'url' => fake()->url(),
            'description' => fake()->sentence(5),
            'published' => fake()->boolean(80),
        ];
    }
```

Factories use Faker to generate fake datas but very close to real datas.

In this code, we generate :
- an url
- a description based on a sentence of 5 words. In fact, as I let the second parameter of `sentence` method by default (true), sentence will generate sentences with approximately 5 words. It could be four, five, six or even seven words.
- true or false for the `published` field with a minimum true percentage of 80%


#### The seeder

Laravel comes with `database/seeders/DatabaseSeeder.php` 

![Path to DatabaseSeeder.php](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dv63g7jmzuqb9jofvh20.png)

We have to edit it to use our factory.

Here it is the code : 

```php
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        User::factory()->create([
            'name' => 'Happytodev',
            'email' => 'happytodev@gmail.com',
            'email_verified_at' => now(),
            'password' => Hash::make('Password*'),
            'remember_token' => Str::random(10),
        ]);

        User::factory()
            ->count(10)
            ->create()
            ->each(function ($user) {
                Link::factory()
                    ->count(rand(2, 10))
                    ->create(['user_id' => $user->id]);
            });
    }
```

Some explanations :

- First I create my own user to connect easily each time with my own email
- Second part does this
   - create 10 fake users
   - for every user create between 2 and 10 links, during the creation we told that the `user_id` fiels must be filled with the current created `user->id`.

And now, the magic could begin.

Let's go back in your terminal, and launch simply this command : 

```bash
php artisan db:seed
```


![Seeding database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vtpnnyw793fuibqibihi.png)

Go into your DB viewer, and check the tables `users` and `links`.

You should see something like that : 

- `users` table
![Users table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwcdnbm1hs3tn3x5r6qy.png)

- `links` table

![Links table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/agfg85t03wym6rl5w8i4.png)

If you want to check everything works fine, we can go a walk in Tinker.

```bash
php artisan tinker
```

Tinker will let us to play with our datas and check the relationship is currently working.

```
User::find(6)->links()->get(['id', 'url', 'published', 'user_id']);
```

Here, we ask to find every link for the user with id = 6. We ask too to get only interesting fields.



![Tinker query results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oatqw2v70x4nqhq8o16j.png)

Seeing these results, we can improve immediately something.

What if, we only want the published links ?

So you can do it like this : 

```
> User::find(6)->links()->where('published', 1)->get(['id', 'url', 'published', 'user_id']);
```

But there is a better way to do this and don't repeat yourself later. We can use Scopes.

A scope can add a bunch of query to your query. Scopes can be global or local. I let you consult [the documentation](https://laravel.com/docs/11.x/eloquent#query-scopes) to see more.

Here we will use a local scope.

Go to your `link` model and add this function : 

```php
    /**
     * Scope a query to only include published links.
     */
    public function scopePublished(Builder $query): void
    {
        $query->where('published', 1);
    }
```

Now, if in Tinker we use the following query : 

```bash
User::find(6)->links()->published()->get(['id', 'url', 'published', 'user_id']);
```

we will obtain only the published links from user with id = 6.


![Tinker query using scope](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c2gxinb0av40mmo3fpoe.png)
 
And voilà, we can cross this point in our list :

> ✅ an user can signin, login, logout and manage his account
> - a user can see a list of their links
> - an user can add, edit or delete a link
> - an user can have a shareable link to share his links with the world

> ✅ a link has a name, a status (published or not) and obviously a link. May be we will add some options later.

Well, we'll stop here for today.

In this longer chapter, we saw introduction to : 

- how to add functionnality to login page in Filament
- models
- factory
- seeder
- Tinker
- scopes

As usual, I wait for your comments below.

See you.