insidieux/task-bundle
A Symfony bundle for defining, organizing, and executing application tasks. It integrates task management into Symfony projects, helping you structure jobs and run them consistently within your existing configuration and workflow. Useful for recurring maintenance, background work, or custom operatio...
1. Install and register
composer require insidieux/task-bundle:^0.2.6
Take 0.2.6 or newer — that patch fixes concurrent updates to the task_queue row, which is exactly the failure mode you'll hit as soon as you poll status while a task is being written. Release cadence on this package is slow, so run a --dry-run first and read the require block before Composer mutates your lock file:
composer require insidieux/task-bundle:^0.2.6 --dry-run -v
Register the bundle the usual way. On Symfony 4/5 with Flex:
// config/bundles.php
return [
// ...
Insidieux\TaskBundle\InsidieuxTaskBundle::class => ['all' => true],
];
On a legacy Symfony 3 kernel, add it to registerBundles() instead.
2. Look here first
bin/console debug:container --tag=kernel.event_subscriber | grep -i task — see what the bundle wires up.
bin/console debug:container | grep -i task — find the manager/runner/storage service IDs; these are what you'll inject. The 0.2.6 changelog confirms one of them writes to a task_queue table, so a persistence-backed storage service exists.
Check the schema before writing any code. Look in vendor/insidieux/task-bundle/ for a Doctrine entity/mapping or SQL for task_queue. Then diff it against your database:
bin/console doctrine:schema:update --dump-sql | grep -i task_queue
If the table isn't there, either a migration ships with the bundle or you own creating it. Decide which, in writing, before it bites you in staging.
bin/console debug:config insidieux_task (or config:dump-reference insidieux_task) — confirm the actual configuration root name and available keys before writing YAML. The extension alias is derived from the bundle name, so verify rather than assume.
The package's own Resources/config/*.xml|yaml and DependencyInjection/Configuration.php in vendor/insidieux/task-bundle/ — with zero stars and zero dependents, vendor source is your real documentation.
3. First use case
The bundle's value is turning an existing bin/console command into a background job you can kick off from a request and poll. Start there:
// src/Controller/ReportController.php
public function export(TaskManagerInterface $tasks): Response
{
$task = $tasks->create('app:reports:export', ['--since' => '2024-01-01']);
$tasks->run($task); // async: returns immediately, process keeps running
return $this->json(['id' => $task->getId()]);
}
Then a status endpoint the browser polls every couple of seconds. Don't build anything more elaborate until that round-trip works end to end — and confirm the status change actually lands in task_queue (query it directly after the first run) rather than trusting the API surface.
Command-as-task is the dominant pattern. Keep the real work inside a Command class that is idempotent and safe to re-run. The bundle should be a thin launcher, not a home for business logic — that way the same code runs interactively (bin/console app:reports:export) during development and in the background in production.
class ExportCommand extends Command
{
protected static $defaultName = 'app:reports:export';
protected function execute(InputInterface $input, OutputInterface $output): int
{
// write results to a deterministic path, e.g. var/exports/{id}.csv
// so the status endpoint can hand back a download URL
return Command::SUCCESS;
}
}
Async vs. sync. Prefer async only for work that exceeds a few seconds or must not block a request. For anything under ~2s, just call the command directly — a background process adds a queue table, a cleanup job, and failure modes you don't need.
Treat the task_queue row as shared mutable state. 0.2.6 exists because two writers can update the same row. Keep your own status writes narrow and atomic — a single UPDATE ... WHERE id = ? AND status = ? — instead of read-modify-write in PHP, and route writes through the bundle's storage service rather than flushing the entity yourself. Anything you add that touches that row is now your concurrency problem.
Lifecycle events for cross-cutting concerns. Listen for start/finish/fail events and centralize logging, metrics, and notification there rather than sprinkling it through controllers:
class TaskListener implements EventSubscriberInterface
{
public function onFinished(TaskFinishedEvent $event): void
{
$this->logger->info('task.finished', ['id' => $event->getTask()->getId()]);
$this->mailer->sendFinishedNotification($event->getTask());
}
}
Wire it with kernel.event_subscriber (or #[AsEventListener] on newer Symfony). Confirm the exact event class names via bin/console debug:event-dispatcher | grep -i task — the string constants are what you actually subscribe to.
Status polling contract. Expose a small JSON endpoint returning {status, startedAt, finishedAt, exitCode, outputUrl}, read from task_queue rather than from in-process state. Map the bundle's internal statuses onto a narrow vocabulary your frontend understands (queued|running|done|failed) so an upgrade or a swap to Symfony Messenger doesn't break the UI.
Retention. A queue table that gets a row per job grows forever. Add a pruning command on a cron (DELETE FROM task_queue WHERE finished_at < :cutoff) and keep finished rows long enough to debug failures — a week is usually right. Do it before the table is large, not after.
Composition with Messenger. If you're on Symfony 4.4+, consider using Messenger for the async dispatch and keeping this bundle only for process supervision and status tracking. They overlap; pick one as the source of truth for "is it running."
Testing. Assert on your command in isolation with CommandTester. For the launcher path, swap the process/runner service in a test container with a fake so your controller tests don't fork real processes — and point the storage at a test table you truncate between tests. Never let the test suite spawn background jobs.
task_queue row, which shows up as tasks stuck in running or reverting to queued. If you're pinned below it, upgrade before you debug anything else.^4.4|^5.0. On Symfony 6+, either stay on the old framework, fork and widen the constraints, or replace it — a 0-star, 0-dependent package will not get upstream fixes beyond patches like this one. Vendor the fork under a namespace you control if you go that route.PATH, HOME, and env vars differ, so a command that works in bin/console may fail as a spawned process. Set the working directory and env explicitly, and prefer absolute paths to binaries.Process timeout will kill long exports; set null deliberately and add a heartbeat or max-runtime guard. On failure paths, make sure a SIGKILL/stop() actually reaps the child, otherwise you accumulate zombie PHP processes — and rows that never leave running.task_queue, so polling works across nodes. The remaining traps are read replicas lagging behind a just-written status and any caching layer in front of the status endpoint — both will make a finished task look stuck.memory_limit on chatty commands. Redirect output to a file and store the path, rather than holding the string or writing it into the table.var/, logs, and the DB user. The web user and the CLI user often differ; a process spawned from FPM can't write to files your console user created. Same for the Doctrine connection if the worker runs under a different env.debug:container for the actual service IDs, debug:config for keys, debug:event-dispatcher for event names, and Monolog at debug for the task channel. Add $process->getErrorOutput() to your logs on non-zero exit, and SELECT * FROM task_queue ORDER BY id DESC LIMIT 5 when the UI says "running" and nothing is happening — silent failures are the standard symptom."insidieux/task-bundle": "0.2.*" rather than ^0.2, and run composer outdated in CI. With no maintainers and no dependents, treat it as an internal dependency you own — read every changelog line, since patches like 0.2.6 land without a migration path or deprecation notice. Keep an extraction plan: the surface is small enough that replacing it with a thin symfony/process wrapper plus a status table is a day's work.How can I help you explore Laravel packages today?