Package · thread

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.

Time ~5 minLevel BeginnerPrereqs PHP 8.4+

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-pcntl and ext-posix (check with php -m)

Install the package:

bash
composer require flytachi/winter-thread

Full details are on Installation & requirements.

Step 1 — Write your task

A task is any class that implements Runnable. All the work goes inside run().

src/ReportGenerator.php
<?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.

run-report.php
<?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).

php
$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:

bash
cat /tmp/report-42.json

If you see the JSON, your first background job ran end to end. 🎉

The whole thing

The complete, copy-paste version:

run-report.php
<?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/null by default.
  • join() blocked until the child finished and handed back its exit code.

Next steps