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.

i
Current implementation This page describes the queue exactly as it exists today, including the handler registry in JobWorker::$event_class_mapping and the helper methods already used by controllers like EmployeeController and LeadsController.

Overview

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.

flowchart TD A[Controller or service] --> B[Jobs::addJob] B --> C[(jobs table)] C --> D[php spark cli/processjobs] D --> E[JobWorker::processJobs] E --> F[JobWorker::processJob] F --> G[event_class_mapping lookup] G --> H[Handler method or function] H --> I[Update status, run_time, response]

Queueing jobs

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.

KeyRequiredDescription
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.

Worker lifecycle

JobWorker is the execution engine. It defines four statuses: queued, running, done, and failed.

  1. processJobs() fetches all queued rows

    Jobs are selected from jobs ordered by created_dt ASC.

  2. Each queued row is forwarded to processJob()

    The worker can process the next queued row, or a specific id plus uuid pair.

  3. Status changes to running

    Once picked, the worker updates the row before invoking the handler.

  4. The handler receives the decoded payload

    The worker resolves the handler from the event map and passes the job payload into it.

  5. The row is finalized

    After execution, the worker stores the final status, runtime, and response JSON back into the same job row.

!
Selection and locking The worker query uses 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.

Handler registry

Every executable job must be registered in JobWorker::$event_class_mapping. Each entry defines a handler category and a target class or function.

TypeMeaningExample 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:

  1. Look up the job name in the mapping

    If the name is missing, the worker throws an exception and marks the job failed.

  2. Instantiate the mapped class for CC or HC

    The mapped class must exist.

  3. Choose the callable

    If a method matching the job name exists, it is used first; otherwise the worker falls back to handle().

  4. For HF, call the mapped function directly

    The handler value itself is treated as the callable.

Sample handlers

The app/Controllers/Jobs/ directory currently contains two simple examples that show the expected pattern for small job classes:

FileMethodBehavior
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,
        ];
    }
}

Status lifecycle

The queue storage model is app/Models/JobModel.php, which maps to the jobs table and allows these main fields:

ColumnPurpose
idPrimary key returned after enqueue.
nameLogical job name used in the worker registry.
payloadJSON-encoded input payload.
responseJSON-encoded handler output or failure details.
statusqueued, running, done, or failed.
run_timeMeasured execution time for the job.
uuidGenerated at enqueue time and used when fetching a specific row.
stateDiagram-v2 [*] --> queued queued --> running running --> done running --> failed

Failure behavior

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.

x
Special file failure branch When a failed job payload contains only a numeric 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.

Running via CLI

The worker is exposed through CLI routes in app/Config/Routes.php.

RouteTargetPurpose
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.

Live runner script

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 pathDescription
/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.
!
Production note This script is an infinite loop, so it should be started under a process manager such as systemd, supervisord, or another service wrapper rather than being launched manually in a shell session.

Systemd service

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 valueDescription
/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

Permissions setup

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
i
Why this matters In live environments, the two crucial steps are: using a managed system service for the queue loop, and ensuring the root-level phpqueue.sh file has the correct owner, group, and execute permission for that service account.

Checking status

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.

MethodUse 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.

i
Which lookup should you use? Use 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.

Adding a new handler

  1. Create the handler class or function

    For small custom jobs, app/Controllers/Jobs/ is already used as a simple home for dedicated job handlers.

  2. Register the job in JobWorker::$event_class_mapping

    Pick the correct type: CC, HC, or HF.

  3. Expose a callable method

    The worker first looks for a method matching the job name, then falls back to handle().

  4. Queue the job through Jobs::addJob()

    Pass a stable job name and only the payload fields the handler actually needs.

  5. Run the worker and inspect the job row

    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',
    ],
]);
+
Practical rule Keep payloads explicit and handler names stable. The job name is the contract between producers and the worker registry, so renaming it has queue-wide impact.