neophp / queue-package
Simple database-backed job queue for NeoPHP, processed via a batch worker command
Requires
- php: >=8.5
README
A simple, database-backed job queue for NeoPHP. Push jobs, process them in batches via a worker command triggered by a system cron — no long-running process, no external dependency like Redis or a message broker.
Structure
queue-package/
├── composer.json
├── README.md
├── src/
│ ├── NeoQueuePackage.php
│ ├── Command/
│ │ ├── QueueWorkCommand.php
│ │ ├── QueueRetryCommand.php
│ │ └── MakeJobCommand.php
│ ├── Service/
│ │ └── QueueManager.php
│ └── Interface/
│ └── JobInterface.php
└── database/
├── Entity/
│ └── QueueJob.php
├── Repository/
│ └── QueueJobRepository.php
└── Migrations/
└── MigrationVersion_Queue_1.php
How it works
Jobs are stored as rows in neo_queue_jobs — no long-running worker
process, no Supervisor/systemd setup required. Instead, a system cron
calls queue:work on a schedule (typically every minute); each run
picks up a batch of due jobs, processes them, and exits.
* * * * * php /path/to/bin/neo queue:work --project=MyProject >> /dev/null 2>&1
If a job throws, it's automatically retried with exponential backoff
(1 minute, then 5 minutes) up to 3 attempts total, after which it's
marked permanently failed and left for manual inspection.
Installation
php bin/neo package:require neophp/queue-package --project=MyProject
Register it in the project's Config/app.config.php:
return [ // ... 'packages' => [ \Vendor\NeoPHP\QueuePackage\NeoQueuePackage::class, ], ];
Run the migration:
php bin/neo database:migration:migrate --project=MyProject
Creating a job
php bin/neo make:job SendWelcomeEmail --project=MyProject
Generates src/MyProject/App/Job/SendWelcomeEmailJob.php:
<?php declare(strict_types=1); namespace Neo\Src\MyProject\App\Job; use Vendor\NeoPHP\QueuePackage\Interface\JobInterface; final class SendWelcomeEmailJob implements JobInterface { public function __construct(private readonly int $userId) { } public function handle(): void { // your logic here — e.g. resolve the user and send an email } }
Keep job constructor parameters simple (scalars, IDs) rather than
storing whole entity objects — a job may sit in the queue for a while
before being processed, so a stale entity reference (deleted row,
detached object) is a real risk. Re-fetch what you need inside
handle().
Pushing a job
public function register(Request $request, QueueManager $queue): Response { // ... $queue->push(new SendWelcomeEmailJob($user->getId())); // with an explicit queue name and priority (higher runs first) $queue->push(new SendWelcomeEmailJob($user->getId()), queue: 'emails', priority: 10); // ... }
Processing jobs
php bin/neo queue:work --project=MyProject
| Option | Default | Purpose |
|---|---|---|
--queue |
default |
Which queue to process |
--limit |
50 |
Maximum jobs processed in this run |
Each invocation processes what's currently due, then exits — safe to
run every minute via cron even if the previous run is still finishing,
since each job is marked processing immediately and won't be picked
up twice.
Retrying permanently failed jobs
php bin/neo queue:retry --project=MyProject
Resets every job in the failed state back to pending with a fresh
attempt count, so the next queue:work run picks them up again. There
is currently no way to inspect why a job failed other than querying
neo_queue_jobs.last_error directly — no CLI/UI listing is provided in
this version.
QueueManager API
| Method | Purpose |
|---|---|
push(JobInterface $job, string $queue = 'default', int $priority = 0): void |
Enqueue a job |
work(string $queue = 'default', int $limit = 50): array{processed, succeeded, failed} |
Process a batch of due jobs |
retryFailed(): int |
Reset all failed jobs to pending, returns count |
stats(): array{pending, processing, done, failed} |
Current job counts by status |
Known limitations
- No true concurrency protection. If two
queue:workprocesses run at the exact same moment (e.g. a slow run overlapping with the next cron tick), there's a small race window between reading due jobs and marking themprocessing— acceptable for most use cases, but not suitable for jobs that absolutely cannot run twice without a database-level lock, which this package does not implement. - Jobs are serialized with PHP's native
serialize(). If you change a job class's constructor signature or namespace after jobs referencing it are already queued, those pending jobs will fail to unserialize. Keep queues empty during deployments that rename or restructure job classes. - No priority across queues — priority only orders jobs within the same queue name, not between different queues.
- No dashboard/UI — inspect job status directly via SQL or build your
own admin page using
QueueJobRepository.
License
MIT