interwal-net/filament-visual-cron-builder
A Filament plugin that adds a visual cron expression builder for Laravel scheduling, letting you create and preview cron schedules through an intuitive UI instead of writing expressions by hand.
Requirements: Filament v4 (not v3), plus whatever Laravel/PHP floor the composer.json declares. This is a v1.0.0 — read composer.json and CHANGELOG.md before adopting; treat the public API as provisional until a 1.1/2.0 lands.
Install and look at these first:
composer require <vendor>/<package>
README.md — exact namespace and field options.src/ — the three moving parts: the CronBuilder field, CronExpressionBuilder (compose / parse / humanReadable), and the ValidCronExpression rule. These are the whole surface area.CHANGELOG.md — v1.0.0 baseline; watch it for option renames in point releases.Data model: store the expression as a plain string column. Nothing exotic is required.
Schema::table('reports', function (Blueprint $table) {
$table->string('cron')->nullable(); // e.g. "*/15 9-17 * * 1-5"
});
First use case — swap a text input in a Resource form:
use <Vendor>\<Package>\Fields\CronBuilder; // confirm namespace in README
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
CronBuilder::make('cron')
->label('Schedule')
->required(),
]);
}
If you already have a TextInput::make('cron') for schedules, this is a genuine drop-in: same state path, same string value, no migration. Ship it behind a feature flag or on one low-risk resource first to see how the 5-column UI behaves with your real expressions.
Form field as the primary integration. The field binds to a string column and renders five columns (minute, hour, day-of-month, month, day-of-week) with Every / Specific / Range / Step modes, plus a live human-readable preview and next-run time. That preview is the reason to use this over a text input — non-technical admins can build 0 9 * * 1-5 without help.
Display side — humanize in tables and infolists. Keep the raw string in the DB and render it read-only:
TextColumn::make('cron')
->formatStateUsing(fn (string $state) => CronExpressionBuilder::humanReadable($state))
->tooltip(fn ($record) => $record->cron),
Validate outside the form too. The rule is standalone, so FormRequests, imports, and API endpoints get the same guarantees:
'cron' => ['required', 'string', new ValidCronExpression()],
Compose expressions programmatically for seeders, defaults, or "schedule presets" in an admin action:
$cron = CronExpressionBuilder::compose([
'minute' => '*/15',
'hour' => '9-17',
// ...
]);
Round-trip a stored expression back into builder state for edit forms — parse() gives you the parts, which is useful if you ever want presets, bulk-copy, or a custom summary panel outside Filament.
Wire it to the scheduler. The output is a standard cron string, so use it directly where you'd otherwise hardcode:
$schedule->command('reports:run')
->cron($report->cron)
->withoutOverlapping();
Anything dynamic like this needs the schedule to be rebuilt whenever the record changes — Schedule::command(...) in a service provider reading from the DB is the usual pattern.
Pair with a "next run" column for operator confidence. If the package exposes a next-run computation on CronExpressionBuilder, cache it; if not, compute it in your own accessor with cron/cron-expression (which the package almost certainly depends on).
Testing. Assert the field accepts and rejects values at the rule level, and use Filament's form testing for the field:
livewire(CreateReport::class)
->fillForm(['cron' => '0 9 * * 1-5'])
->call('create')
->assertHasNoFormErrors();
livewire(CreateReport::class)
->fillForm(['cron' => 'not a cron'])
->call('create')
->assertHasFormErrors(['cron']);
@daily, @weekly). Don't assume a scheduler-style alias survives the field's parser. If you accept macros from elsewhere (API, config), normalize them to 5-field form before they hit the builder.null cron on an edit form often surfaces as an empty or half-filled preview. Decide the contract early: either ->required() or a ->default('0 0 * * *'), and guard the human-readable rendering with a null check.0 9 * * 1-5 and 0 09 * * 1-5 and different whitespace are the same schedule. If you dedupe, diff, or match schedules, compare parsed parts, not raw strings.->showPreview(), ->showNextRun(), etc.) can move in a minor. "^1.0" with a lockfile is fine; floating dev-main is not.CronBuilder and override the view/render hooks for branding or to trim columns; wrap CronExpressionBuilder in your own service if you need caching or custom phrasing, so a package upgrade doesn't ripple through your codebase.$livewire->form->getState()) to catch dehydration surprises; if an expression is valid but rejected, test the same string against ValidCronExpression in isolation to tell a rule problem from a field-state problem.How can I help you explore Laravel packages today?