Package · thread

Installation & Requirements

Winter Thread targets a POSIX PHP CLI environment. Install it with one Composer command, confirm a couple of extensions are present, and — for most apps — write no configuration at all.

Install

bash
composer require flytachi/winter-thread

Installing the package also exposes the child bootstrap script as vendor/bin/wRunner. You never call it by hand — the engine invokes it for you — but it must remain executable and reachable on disk (see The runner path below).

Requirements

Requirement Why Mandatory?
PHP >= 8.4 modern language features (readonly classes, first-class callable syntax, named args) Required
ext-pcntl pcntl_fork() for detached mode Required (Composer)
ext-posix posix_kill() (signals) and posix_setsid() (detached mode) Required (Composer)
opis/closure ^4.5 safe serialization of closures / anonymous classes and signed payloads Required (Composer)
ext-shmop only for the shared-memory transport (ShmTransport) Optional

ext-pcntl and ext-posix are standard, lightweight POSIX extensions bundled with almost every Linux/macOS PHP CLI — nothing exotic. There is no ZTS build requirement and no heavy extension (swoole / parallel / pthreads) involved.

opis/closure is a hard dependency and is installed automatically by composer require flytachi/winter-thread — there is no separate step. It is what lets anonymous classes and Closure objects be serialized and executed in a background process, and it signs payloads when a secret is set; PHP’s native serialize() cannot handle closures or class@anonymous and throws on them.

Check installed extensions with php -m:

bash
php -m | grep -E 'pcntl|posix|shmop'

What each dependency gates

The bare spawn/wait path (start()join()/reap()) only needs proc_open. ext-posix powers signal control (pause, resume, interrupt, terminate, kill); ext-pcntl powers detached mode’s fork. The package requires both so the full feature set always works — if a platform lacks one, only the corresponding feature is affected.

ext-shmop (optional)

The shared-memory transport (ShmTransport, useful under Swoole) is the only feature that needs ext-shmop. It is checked at runtime: staging or receiving a payload throws a clear ThreadException ("ShmTransport requires ext-shmop.") if the extension is missing — never a fatal error. If it is unavailable, use TempFileTransport instead, which needs no extra extension. See Payload delivery.

Operating system

Winter Thread targets a POSIX-compliant OS (Linux, macOS, BSD). It relies on POSIX signals, setsid, and /proc, and is developed and tested on Linux and macOS. Windows is not supported.

Bootstrap configuration

Configuration is process-wide and lives in the Engine — the parent-side strategy object that decides which PHP binary, runner script, and payload transport a Thread uses. Bind it once during your application’s bootstrap.

For most applications there is nothing to configure. If you never bind an engine, the first Thread lazily creates a default AdaptiveEngine, which self-configures for the runtime it finds — plain CLI, PHP-FPM/CGI, or an active Swoole runtime. Just start threads:

php
<?php
require 'vendor/autoload.php';

use Flytachi\Winter\Thread\Runnable;
use Flytachi\Winter\Thread\Thread;

$thread = new Thread(new class implements Runnable {
  public function run(array $args): void { /* work here */ }
});

$thread->start();

Explicit engine

To take control, build an engine and bind it with Thread::bindEngine(). Use ManualEngine for a clean slate where you set every part yourself, or AdaptiveEngine with named arguments to override only what you need.

php
<?php

use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\ManualEngine;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

Thread::bindEngine(
  (new ManualEngine())
      ->withTransport(new TempFileTransport())
      ->withBinaryPath('/usr/bin/php')
      ->withRunnerPath(__DIR__ . '/vendor/flytachi/winter-thread/wRunner')
      ->withSecurity('your-secret-key')
);

ManualEngine configures nothing for you

transport, binaryPath, and runnerPath must all be set or the engine throws a ThreadException when they are accessed. withSecurity() is optional. The withers are immutable — each returns a new clone. Use ManualEngine only when you deliberately want full control; otherwise AdaptiveEngine handles the common cases on its own.

Or keep AdaptiveEngine’s auto-detection but override individual aspects with named arguments:

php
<?php

use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

Thread::bindEngine(new AdaptiveEngine(
  secret: 'your-secret-key',
  transport: new TempFileTransport(),
  binaryPath: '/usr/bin/php',
));

AdaptiveEngine is a readonly, immutable class — to change one aspect, construct a new instance. See the API reference for the full engine surface.

The secret

A secret enables HMAC signing of the serialized payload (via opis/closure), so a forged or tampered payload is rejected before any object is built in the child process. Signing is opt-in but strongly recommended whenever you serialize closures or anonymous classes.

You can supply it three ways — ManualEngine::withSecurity(), the AdaptiveEngine(secret:) argument (shown above), or the WINTER_THREAD_SECRET environment variable, which AdaptiveEngine reads automatically:

bash
export WINTER_THREAD_SECRET='your-secret-key'

The secret reaches the child through that env var (owner-only /proc/<pid>/environ), never through argv.

Set a secret in production

Without a secret, payloads are still handled by opis/closure (never native unserialize()), but they are unverified — trust falls back to the private, owner-only delivery channel. Set a secret to reject forged payloads before they can execute code in a child process.

Environment notes

PHP-FPM / web SAPI

proc_open must be permitted (not listed in disable_functions). Under FPM/CGI, PHP_BINARY points at the FPM binary, not a CLI one — running your worker through that would be wrong. The default AdaptiveEngine detects a non-CLI SAPI and resolves a real PHP CLI binary from PHP_BINDIR/php instead. If detection fails in an unusual setup, set the path explicitly with withBinaryPath() on a ManualEngine or the AdaptiveEngine(binaryPath:) argument.

The runner path

The child is bootstrapped by the wRunner script shipped in the package root. AdaptiveEngine locates it automatically at vendor/flytachi/winter-thread/wRunner. Two situations need attention:

  • Phar / relocated deployments. If your code is packed into a .phar, or the vendor directory is not on a normal filesystem path, the script may not be directly executable. Point a ManualEngine at a real on-disk copy with withRunnerPath(...).
  • open_basedir. The binary and runner paths must be inside any configured open_basedir.

Containers

If you run detached tasks with your app as PID 1 in a container, add a reaping init (docker run --init, or init: true in Compose) so orphaned workers are collected. Without it, detached workers reparent to your app (PID 1), which does not reap them, and they accumulate as zombies. Attached tasks that you join()/reap() do not need this.

Verify your install

php
<?php
require 'vendor/autoload.php';

use Flytachi\Winter\Thread\Runnable;
use Flytachi\Winter\Thread\Thread;

$thread = new Thread(new class implements Runnable {
  public function run(array $args): void { /* nothing */ }
});

echo 'PID:  ' . $thread->start() . PHP_EOL;
echo 'exit: ' . $thread->join() . PHP_EOL; // 0

Expected output is a numeric PID followed by exit: 0. Anonymous classes work because opis/closure is a hard dependency — you are not restricted to named task classes (though named classes give more readable process titles and simpler debugging).

Next, walk through a real task in the Quickstart.