· filament · 5 min read

Playing with Filament - Part 1

I fall in love with FilamentPHP, here is what I did with it.

Indeed! This module is awesome, but it takes time to understand all the possibilities and how to configure every element and aspect.

To practice this module, I’m going to write the “Backend” of my Thai mobile applications.

It’s a win-win project for me as I’ll have a backend to manage the apps, and at the same time, I’m learning complex real-life scenarios with FilamentPHP.

I might also deploy a “web” version in the future for my users.

Project Installation

I’ve used Filament-starter as it sets up a few things that I want in a Filament Project.

  • Media Library (Curator)
  • Settings page with
    • Exception
    • Jobs
    • Activity Log

exception jobs activity-logs

Word

Let’s start with the concept of Word. Here is the first draft (more fields will come). I used

  • Simple Repeater (Cut by syllable)
  • Multiple Images upload
  • Relation Manager (Translations - it was originally a repeater, as seen in the screenshot)

word

If it’s the first time you see the Thai language … well, it’s not using the Roman alphabet :) Thai is a tonal language, and to determine the tone of a syllable, you have to apply a specific set of rules to a syllable.

tonerules

Not funny, right? So instead of memorizing that table, I’ve applied colors to syllables. Every color will tell the tone of that syllable, and when you read a word, it’s easy to adapt your pronunciation.

sentence example

Ok, enough of context :) let’s talk about the Filament part!

Images

Alt text

First, I wanted to be able to add multiple images to a word; sometimes, images are better than a translation :) The default CuratorPicker or FileUpload field was too large, so I’ve used the listDisplay(true) instead, which is more compact.

CuratorPicker::make('image')
  ->label('Images')
  ->acceptedFileTypes(['image/*'])
  ->multiple()
  ->preserveFilenames()
  ->listDisplay(true)
  ->directory('words')
  ->required(),

In the database, it is saved as a list of IDs like this: [8,7], which I suppose to be the IDs of Curator.

I currently have a bug with the Curator module. I don’t know if it’s something I’m doing wrong or a bug in the module?

Cut the Syllable

A word is composed of one to multiple syllables, making it the perfect candidate to be a repeater :)

Also, a syllable will have the following info:

  • The syllable in Thai
  • The associated tone
  • The romanization (Oh, another concept … the Thai writing system…)
 Repeater::make('syllables')
    ->relationship('syllables')
    ->simple(
        Select::make('syllable_id')
            ->relationship('Syllable', 'thai')
            ->searchable()
            ->preload()
            ->createOptionForm([
                TextInput::make('thai')
                    ->required(),
                Select::make('tone')
                    ->options(Tone::class)->required(),
                TextInput::make('romanization')
                    ->required(),
            ])
            ->required(),
    )
    ->orderColumn('order')
    ->grid(3),

To implement this, I’ve created:

  • Syllable table: to store all individual syllables, making them reusable across multiple words.
  • Word_syllables pivot table: to establish associations between syllables and words.

syllable

Note: There’s a crucial relationship with the repeater: “Syllables,” as defined in my “Word” model (App\Models\Word).

public function syllables(): hasMany
{
    return $this->hasMany(WordSyllable::class);
}

A lot of magic happens behind the scenes to create everything correctly, but it just works…

I’ve also added a CreateOptionForm so if the syllable doesn’t exist, we can quickly create a new syllable “on-the-fly” without going to the full-page form.

It would be nice to be able to share that CreateOptionForm and the Syllable Form page.

I opted for a Simple repeater because it only has one field (even if it’s a select behind) and looks visually better than the full repeater. Additionally, I’ve included an “order” (unsigned int) column to easily organize the syllables within the repeater area using drag & drop.

Translations

A language app without a translation field wouldn’t be a language app, right?

translations

In the first version, I used a repeater which is functional but won’t scale well with multiple languages or multiple definitions. Therefore, I’ll change it to a Relation Manager, which will be way better as I could also have some “clickable” pills on top with different language filters.

I’m keeping it here as a reference on how to do it, but the next section will be about rewriting it with the Relation Manager.

A word in Thai can have multiple meanings; well, in fact, in many languages, a word can have different meanings, often depending on the context, its position in a sentence (sometimes even depending on the intonation - Think of “What”).

To represent a translation, I’ve created a new table called word_translations, which includes:

  • word_id
  • language_id
  • value
  • order
  • type (Noun, Verb, etc.) - Which is a PHP Enum
Repeater::make('wordTranslations')
  ->hiddenLabel(true)
  ->relationship('wordTranslations')
  ->orderColumn('order')
  ->collapsed(fn (string $operation): bool => $operation === 'edit')
  ->itemLabel(function (array $state): ?string {
      $availLang = AvailableLanguage::find($state['available_language_id']);
      if (! $availLang) {
          return null;
      }
      return $availLang['name'].' - '.$state['type'].' - '.$state['value'] ?? null;
  })
  ->schema(
      [
          Select::make('available_language_id')
              ->relationship('availableLanguage', 'name')
              ->searchable()
              ->preload()
              ->required()
              ->columnSpan(1),
          Select::make('type')
              ->options(TranslationType::class)->required()
              ->columnSpan(1),

          TextInput::make('value')
              ->required()
              ->columnSpan(3),

      ]
  )->columns(5),
  1. I’ve added an itemLabel to the header of the repeater item, which is a concatenation of different fields inside to quickly identify the translations when the repeater item is collapsed.
  2. If it’s in Edit mode, I want those repeater items to be “collapsed.”

Enums

I’ve created two enums in PHP; if I understand correctly, it’s a relatively new concept in PHP.

namespace App\Enum;

use Filament\Support\Contracts\HasLabel;

enum Tone: string implements HasLabel
{
    case Mid = 'M';
    case High = 'H';
    case Low = 'L';
    case Rising = 'R';
    case Falling = 'F';

    public function getLabel(): ?string
    {
        return $this->name;
    }
}

With Filament, to have a select with my enum’s values, I did the following:

Select::make('tone')
  ->options(Tone::class)->required(),

I’m using the enum in my migration to give a default value as follows:

$table->string('tone')->default(\App\Enum\Tone::Mid);

In the next post, I’ll describe how I implemented the relation manager for translations.

Back to Blog