Quickstart
From install to a running background job in five minutes: you’ll write a task, run it in a separate process, wait for it, and confirm it finished.
What you’ll build
A ReportGenerator task that queries a database and writes a report file. Because it can
be slow, you’ll run it in the background so your main script isn’t blocked — then wait
for it and check the result. That’s the whole loop: define → start → wait → verify.
Before you start
- PHP ≥ 8.4 on a POSIX OS (Linux, macOS, BSD — not Windows)
- Extensions
ext-pcntlandext-posix(check withphp -m)
Install the package:
composer require flytachi/winter-threadFull details are on Installation & requirements.
Step 1 — Write your task
A task is any class that implements Runnable. All the work goes inside run().
<?php
use Flytachi\Winter\Thread\Runnable;
class ReportGenerator implements Runnable
{
public function __construct(private int $reportId) {}
public function run(array $args): void
{
// Create resources INSIDE run(), never in the constructor.
$db = new PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
$orders = $db
->query("SELECT * FROM orders WHERE report_id = {$this->reportId}")
->fetchAll();
file_put_contents(
"/tmp/report-{$this->reportId}.json",
json_encode($orders)
);
}
}Why resources go inside run()
The task object is serialized and handed to a separate process, so its properties can’t
carry live resources like DB connections, file handles, or sockets. Keep the constructor
to plain data (ids, paths) and open connections inside run(). More in the
Runnable reference.
Step 2 — Start it in the background
Wrap the task in a Thread and call start(). It returns the child process PID
immediately — your script is never blocked.
<?php
require 'vendor/autoload.php';
use Flytachi\Winter\Thread\Thread;
$thread = new Thread(
new ReportGenerator(42),
'Billing', // namespace — shows up in `ps`
'ReportGenerator', // name
'report-42' // tag
);
$pid = $thread->start();
echo "Report started in the background (PID: {$pid})\n";
// Your script keeps running while the report is generated in a separate process.
echo "Main script is free to do other work...\n";Where does the output go?
By default output goes to /dev/null — safe for fire-and-forget jobs, no pipe is opened.
To capture logs or read output live, see Debugging & output.
See it running
While the job runs, ps shows the process under its title:
WinterThread Billing -> ReportGenerator@report-42.
Step 3 — Wait for it and check the result
Need the result before moving on? Call join(). It blocks until the child exits and
returns the exit code (0 = success).
$exitCode = $thread->join();
if ($exitCode === 0) {
echo "Done. Report written to /tmp/report-42.json\n";
} else {
echo "Report failed (exit code: {$exitCode}).\n";
}Confirm the file the task produced:
cat /tmp/report-42.jsonIf you see the JSON, your first background job ran end to end. 🎉
The whole thing
The complete, copy-paste version:
<?php
require 'vendor/autoload.php';
use Flytachi\Winter\Thread\Runnable;
use Flytachi\Winter\Thread\Thread;
class ReportGenerator implements Runnable
{
public function __construct(private int $reportId) {}
public function run(array $args): void
{
$db = new PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
$orders = $db
->query("SELECT * FROM orders WHERE report_id = {$this->reportId}")
->fetchAll();
file_put_contents(
"/tmp/report-{$this->reportId}.json",
json_encode($orders)
);
}
}
$thread = new Thread(new ReportGenerator(42), 'Billing', 'ReportGenerator', 'report-42');
$pid = $thread->start();
echo "Report started in the background (PID: {$pid})\n";
$exitCode = $thread->join();
echo $exitCode === 0
? "Done. Report written to /tmp/report-42.json\n"
: "Report failed (exit code: {$exitCode}).\n";What just happened
- A separate OS process ran your task, fully isolated — a crash there can’t take down your main script.
- The task crossed a process boundary by being serialized, which is why resources are
created inside
run()(Step 1). start()returned instantly with a PID; output went to/dev/nullby default.join()blocked until the child finished and handed back its exit code.
Next steps
- Installation & requirements — extensions and bootstrap config
- Mental model — how it works and why
- Queue worker — dispatch jobs & pass arguments
- Graceful shutdown — timeouts & stopping work
- API reference — every method and signal
- Payload modes — running under Swoole and event loops