Custom Rules in Filament

Imagine you're building out an admin panel with Filament, and you are leveraging the power and flexibility of its KeyValue component to store JSON data directly in a database column. It's a great way to handle dynamic attributes or configurations. But what happens when you need to enforce specific constraints on the structure of that JSON, not just its validity as a whole? For example, you might need to ensure that both the keys and values within that JSON object fall within certain length limits.

This isn't something the KeyValue component handles out-of-the-box. The standard validation rules apply to the entire JSON string, not its individual parts. In this article, we'll walk through a practical solution: creating a custom Laravel validation rule and seamlessly integrating it with your Filament form. You'll learn how to gain fine-grained control over the validity of your user-provided KeyValue data, ensuring data integrity and consistency. We'll focus on implementing length restrictions, but the principles can be extended to other custom validation logic.

Building a Custom Laravel Validation Rule

We'll create a rule specifically designed for our KeyValue scenario described above. The goal is to check both the key and value lengths within each entry of the JSON object.

php artisan make:rule JsonKeyValueLengthRule
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JsonKeyValueLengthRule implements ValidationRule
{
    protected int $minLength;
    protected int $maxLength;
    /**
     * Create a new rule instance.
     */
    public function __construct(int $minLength = 3, int $maxLength = 30)
    {
        $this->minLength = $minLength;
        $this->maxLength = $maxLength;
    }

    /**
     * Run the validation rule.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @param  \Closure  $fail
     * @return void
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!is_array($value)) {
            $fail("The :attribute must be a valid JSON object."); // Basic type check
            return;
        }

        foreach ($value as $key => $val) {
            if (!is_string($key) || !is_string($val)) { //Type check each Key and Value
                $fail("All keys and values in :attribute must be strings.");
                return;
            }

            $keyLength = strlen($key);
            $valueLength = strlen($val);

            if ($keyLength < $this->minLength || $keyLength > $this->maxLength) {
                $fail("The key '{$key}' must be between {$this->minLength} and {$this->maxLength} characters.");
                return; // Stop on first error for this key-value pair
            }

            if ($valueLength < $this->minLength || $valueLength > $this->maxLength) {
                $fail("The value for key '{$key}' must be between {$this->minLength} and {$this->maxLength} characters.");
                return; // Stop on first error for this key-value pair
            }
        }
    }
}
Databases in VS Code? Get DevDb

Let's break down this code. After generating the rule class using the artisan command, we added a constructor, to make the class more flexible, allowing set the min and max length of characters from where it's called, if not defined, defaults to 3 and 30. The core logic resides in the validate method. It receives the attribute name, the value (our JSON data), and a $fail closure, crucial to signal validation problems.

Inside validate, we first perform a basic check to ensure the input is an array (since Filament's KeyValue component stores data as an associative array). Then, we iterate through each key-value pair. We added type checking also. Finally, we check the lengths of both the key and the value against our defined minimum (3) and maximum (30) limits. If a violation is found, we use the $fail closure to register a custom error message. It's important to note that you could use the trans() helper, if you are working with multiple languages.

Integrating the Custom Rule with Filament's KeyValue Component

Now that we have our custom rule, integrating it into our Filament form is pretty straightforward. We'll use the rules() method provided by the KeyValue component.

use Filament\Forms\Components\KeyValue;
use App\Rules\JsonKeyValueLengthRule;

KeyValue::make('description')
    ->label('Feature Description')
    ->keyLabel('Feature Name')
    ->valueLabel('Description')
    ->addActionLabel('Add Feature')
    ->rules([new JsonKeyValueLengthRule()]) // Applies our custom rule
    ->default([]),

That's it! We've simply added our JsonKeyValueLengthRule to the rules array. Filament will now automatically apply this rule whenever the form is submitted. If any key or value violates the length constraints, the user will receive the custom error message we defined in our rule.

We can even modify the Min and Max values, as we defined in the constructor:

use Filament\Forms\Components\KeyValue;
use App\Rules\JsonKeyValueLengthRule;

KeyValue::make('description')
    ->label('Feature Description')
    ->keyLabel('Feature Name')
    ->valueLabel('Description')
    ->addActionLabel('Add Feature')
    ->rules([new JsonKeyValueLengthRule(5, 50)]) // Apply our custom rule. The Keys and Values should have more than 5 characters and not be greater than 50.
    ->default([]),

Adding More Robust Validation (Optional)

While the length check is a good start, you might want to add further validation. For instance, you might want to enforce specific character sets for keys (e.g., only alphanumeric characters and underscores). You could easily extend the JsonKeyValueLengthRule to include such checks, or create separate, more specialized rules. I personally find it's best to create focused, single-responsibility rules as it makes debugging much easier down the line.

For example, adding a regex check for allowed key characters:

// Inside JsonKeyValueLengthRule::validate()
if (!preg_match('/^[a-zA-Z0-9_]+$/', $key)) {
    $fail("The key '{$key}' contains invalid characters. Only alphanumeric characters and underscores are allowed.");
    return;
}

Conclusion

By combining Filament's flexible KeyValue component with Laravel's powerful custom validation rules feature, we have complete control over the data structure stored in our JSON column. We've enforced key and value length constraints, ensuring data integrity and preventing unexpected issues. This approach is not only effective but also maintainable and extensible. You can easily adapt the JsonKeyValueLengthRule to implement other validation logic for your specific application requirements. This pattern of creating custom validation rules is a valuable tool in any Laravel developer's toolkit, and it integrates seamlessly with Filament's form builder. You should also consider looking into Laravel's validation rule objects and closures for even more advanced validation scenarios.

Wanna chat about what you just read, or anything at all? Click here to tweet at me on 𝕏