Nhance uses a database-backed job queue for long-running or deferred work. A
producer adds a row into the jobs table through
Jobs::addJob(), and the CLI worker in
JobWorker picks up queued rows, executes the mapped handler, and
writes the final status and response back to the same record.
JobWorker::$event_class_mapping and the
helper methods already used by controllers like EmployeeController
and LeadsController.
The queue flow is simple: enqueue, store, process, update. It is used for tasks such as file validation, employee processing, claim import work, API sync jobs, bulk mail, and batch e-card generation.
New jobs are inserted through Jobs::addJob(array $payload). The
method validates the input, generates a UUID, JSON-encodes the inner payload,
and stores the row with status queued unless a custom status is
provided.
| Key | Required | Description |
|---|---|---|
| job_name | required | Name used to look up the handler in JobWorker::$event_class_mapping. |
| payload | required | Array that will be JSON-encoded into the jobs.payload column. |
| status | optional | Defaults to queued. |
$job = Jobs::addJob([
'job_name' => 'memberDataListExcelFileFormatValidation',
'payload' => [
'lead_id' => $lead_id,
'age_validation' => true,
],
]);
The method returns an array with id, uuid, and
job_name. Real code paths already use this pattern, for example
after lead placement data is saved and then queued for validation.
JobWorker is the execution engine. It defines four statuses:
queued, running, done, and
failed.
processJobs() fetches all queued rows
Jobs are selected from jobs ordered by created_dt ASC.
processJob()
The worker can process the next queued row, or a specific id plus uuid pair.
running
Once picked, the worker updates the row before invoking the handler.
The worker resolves the handler from the event map and passes the job payload into it.
After execution, the worker stores the final status, runtime, and response JSON back into the same job row.
LIMIT 1 FOR UPDATE when fetching a single
job. In practice, treat the queue as database-backed and process it from CLI
workers, not from normal web requests.
Every executable job must be registered in
JobWorker::$event_class_mapping. Each entry defines a handler
category and a target class or function.
| Type | Meaning | Example from code |
|---|---|---|
CC |
Controller class handler | App\Controllers\Jobs\SubJob, EmployeeServiceController |
HC |
Helper-style class handler | App\Helpers\HttpRequestHelper, App\Helpers\MailHelper |
HF |
Standalone function handler | fancy_date_time_format |
Resolution order inside the worker is:
If the name is missing, the worker throws an exception and marks the job failed.
CC or HC
The mapped class must exist.
If a method matching the job name exists, it is used first; otherwise the worker falls back to handle().
HF, call the mapped function directly
The handler value itself is treated as the callable.
The app/Controllers/Jobs/ directory currently contains two simple
examples that show the expected pattern for small job classes:
| File | Method | Behavior |
|---|---|---|
app/Controllers/Jobs/AddJob.php |
handle($payload) |
Returns $payload['a'] + $payload['b']. |
app/Controllers/Jobs/SubJob.php |
handle($payload) |
Logs through mylogger and returns $payload['a'] - $payload['b']. |
namespace App\Controllers\Jobs;
use App\Controllers\PublicController;
class ExampleJob extends PublicController
{
public function handle($payload)
{
return [
'ok' => true,
'received' => $payload,
];
}
}
The queue storage model is app/Models/JobModel.php, which maps to
the jobs table and allows these main fields:
| Column | Purpose |
|---|---|
id | Primary key returned after enqueue. |
name | Logical job name used in the worker registry. |
payload | JSON-encoded input payload. |
response | JSON-encoded handler output or failure details. |
status | queued, running, done, or failed. |
run_time | Measured execution time for the job. |
uuid | Generated at enqueue time and used when fetching a specific row. |
Both worker-level failures and handler-level failures are caught and written
back into jobs.response as structured JSON. The stored data
includes the error message, file, line, trace, and whether the failure
happened in the task or the worker wrapper.
file_id, the
worker also updates the related files row to
status = failed and writes a generic system error reason. This
is important for file-processing jobs that surface state back to the UI.
The worker is exposed through CLI routes in app/Config/Routes.php.
| Route | Target | Purpose |
|---|---|---|
cli/processjob |
JobWorker::processJob |
Process one queued job. |
cli/processjobs |
JobWorker::processJobs |
Loop through all queued jobs in created order. |
php spark cli/processjob
php spark cli/processjobs
There is also a web route named processjob, but operationally this
queue should be treated as a CLI worker flow.
For live environments where the worker should keep polling continuously, a
simple shell loop can call the queue worker across all required applications.
Use the root-level phpqueue.sh script and replace the placeholder
app paths with your deployment-specific values.
#!/bin/bash
PHP_BIN="${PHP_BIN:-/usr/bin/php}"
# Dummy example paths for documentation.
# Replace each one with the real public/index.php path in that environment.
APP_INDEXES=(
"/var/www/example-suite/zenith-app/public/index.php"
"/var/www/example-suite/enrolment-app/public/index.php"
"/var/www/example-suite/partner-api/public/index.php"
)
while true; do
echo "Running job at $(date)"
for index_file in "${APP_INDEXES[@]}"; do
"${PHP_BIN}" "${index_file}" cli/processjob
done
sleep 1
done
| Dummy path | Description |
|---|---|
/var/www/example-suite/zenith-app/public/index.php |
Example public entry file for the first application queue target. |
/var/www/example-suite/enrolment-app/public/index.php |
Example public entry file for the second application queue target. |
/var/www/example-suite/partner-api/public/index.php |
Example public entry file for the third application queue target. |
systemd, supervisord, or another
service wrapper rather than being launched manually in a shell session.
For Ubuntu-style live deployments, keep a systemd unit file such as
nhance_php_queue_server.service. The repository now includes a
sanitized template with placeholder values.
[Unit]
Description=Nhance PHP Queue Server
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/example-suite/nhance-app
ExecStart=/bin/bash /var/www/example-suite/nhance-app/phpqueue.sh
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
| Dummy value | Description |
|---|---|
/var/www/example-suite/nhance-app |
Example project root where phpqueue.sh is kept. |
www-data |
Example service account; replace it with the real Linux user and group used by PHP on that server. |
After replacing the placeholders, deploy and enable it with standard systemd commands:
sudo mv nhance_php_queue_server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable nhance_php_queue_server.service
sudo systemctl start nhance_php_queue_server.service
sudo systemctl restart nhance_php_queue_server.service
The queue runner script should live in the project root as
phpqueue.sh, and its ownership plus execute permission should be
set for the service user and group used by PHP in that environment.
sudo chown <service-user>:<service-group> phpqueue.sh
sudo chmod 750 phpqueue.sh
phpqueue.sh file has the correct owner, group, and
execute permission for that service account.
app/Libraries/JobStatusService.php now supports lookup by job
name, by job id, and by job uuid. All methods normalize the
decoded response and runtime before returning them.
| Method | Use when |
|---|---|
getJobStatusByName(string $jobName) |
You want the latest job row for a logical job name. |
getJobStatusById(int $jobId) |
You know the numeric queue row id and want that exact job record. |
getJobStatusByUuid(string $uuid) |
You want to track one exact job instance using the UUID returned at enqueue time. |
$service = new \App\Libraries\JobStatusService();
$status = $service->getJobStatusByName('bulkGenerateEcardAndStoreinS3');
$job = Jobs::addJob([
'job_name' => 'exampleJob',
'payload' => [
'file_id' => 123,
],
]);
$service = new \App\Libraries\JobStatusService();
$statusById = $service->getJobStatusById((int) $job['id']);
$statusByUuid = $service->getJobStatusByUuid($job['uuid']);
The service returns success, job_name,
status, response, job_id,
uuid, run_time, and a message.
job name when you only care about the latest run for a given
handler. Use job id or uuid when the enqueue response
is available and you need to track one specific job instance across retries
or parallel runs.
For small custom jobs, app/Controllers/Jobs/ is already used as a simple home for dedicated job handlers.
JobWorker::$event_class_mapping
Pick the correct type: CC, HC, or HF.
The worker first looks for a method matching the job name, then falls back to handle().
Jobs::addJob()
Pass a stable job name and only the payload fields the handler actually needs.
Validate the final status, runtime, and response before integrating the job into larger workflows.
// 1. Register in JobWorker::$event_class_mapping
'exampleJob' => [
'type' => 'CC',
'handler' => 'App\Controllers\Jobs\ExampleJob',
],
// 2. Queue it
Jobs::addJob([
'job_name' => 'exampleJob',
'payload' => [
'file_id' => 123,
'source' => 'manual-test',
],
]);