Stilling's Blog /

Masking IDs in Filament using Sqids

2024-05-01

I recently began working on my first Filament v3 project and needed to hide the IDs for the records. For this task, I opted to use Sqids.

To begin, install sqids/sqids by running the following command:

$ composer require sqids/sqids

Models

To make Filament display Sqids in links instead of model IDs, let's create a trait:

use Sqids\Sqids;

/**
 * @property-read string $sqid
 */
trait ModelMasksRecordId {
	public static function getSqids(): Sqids {
		return new Sqids(
			alphabet: defined('self::SQID_ALPHABET') ? self::SQID_ALPHABET : Sqids::DEFAULT_ALPHABET,
			minLength: defined('self::SQID_MIN_LENGTH') ? self::SQID_MIN_LENGTH : 6,
		);
	}

	protected function getSqidAttribute(): string {
		return self::getSqids()->encode([$this->getKey()]);
	}

	public function getRouteKey(): string {
		return $this->sqid;
	}
}

This trait offers a static method to obtain a Sqids instance tailored to your model. It includes a $sqid property that returns the encoded ID and implements the getRouteKey() method used by Filament for record referencing.

Simply apply this trait to your models. Optionally, you can configure a custom alphabet and minimum length directly in the model:

class User extends Model {
	use ModelMasksRecordId;

	public const string SQID_ALPHABET = "348abe21dc7069f5";
	public const int SQID_MIN_LENGTH = 10;

	// ...
}

Resources

Filament needs assistance in decoding our Sqids. Let's create another trait for this purpose:

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;

trait ResourceMasksRecordId {
	public static function resolveRecordRouteBinding(int|string $key): ?Model {
		/** @var class-string<ModelMasksRecordId> $model */
		$model = self::$model;
		$key = $model::getSqids()->decode($key)[0] ?? null;

		if (is_null($key)) {
			throw new ModelNotFoundException();
		}

		return parent::resolveRecordRouteBinding($key);
	}
}

We override the resolveRecordRouteBinding method to attempt decoding before passing the ID to the original method. Apply this trait to your resources:

class UserResource extends Resource {
	use ResourceMasksRecordId;

	// ...
}

Multi-tenancy

For multi-tenancy scenarios where you want to conceal tenant IDs, additional steps are required. First, ensure you apply ModelMasksRecordId to your tenant model. Next, create a middleware:

use App\Models\Team;
use Closure;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class ResolveTenantSqid {
	public function handle(Request $request, Closure $next) {
		if ($tenant = $request->route()->parameter("tenant")) {
			$tenant = Team::getSqids()->decode($tenant)[0] ?? null;

			if (is_null($tenant)) {
				throw new ModelNotFoundException();
			}

			$request->route()->setParameter("tenant", $tenant);
		}

		return $next($request);
	}
}

This middleware checks for a tenant parameter and replaces it with the decoded id. Add this middleware to your panel provider:

use Filament\Panel;
use Filament\PanelProvider;

class AppPanelProvider extends PanelProvider {
	public function panel(Panel $panel): Panel {
		return $panel
			// ...
			->middleware([ ResolveTenantSqid::class ], true)
			// ...
		;
	}
}

We must keep our ResolveTenantSqid middleware separate from the original middleware() list, due to the true parameter. This parameter designates the middleware as "persistent", ensuring its addition as a Livewire middleware.

That's all there is to it!