Package · thread

Framework integration

Winter Thread spawns a PHP CLI process per task. Inside a framework or a web SAPI, you usually need to tell it which PHP to run, where the child bootstrap lives, and how to sign payloads. All of that lives on a Launcher you bind once at application startup.

Bind the launcher at bootstrap

Configuration goes through a single seam: Thread::bindLauncher(). Call it once, early — in a service provider’s boot(), a container bootstrap, or any file every process loads (including Swoole workers). Every Thread::start() afterwards uses the bound launcher.

The zero-config default is AdaptiveLauncher: on every launch it looks at the runtime and routes to SwooleLauncher (inside a coroutine) or CliLauncher (plain CLI, FPM), resolving the binary, the wRunner path and the secret for you. If you bind nothing, Thread::launcher() lazily creates one — so you only bind when you need an override.

bootstrap.php
<?php

use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

// Explicitly what happens with no configuration at all.
Thread::bindLauncher(AdaptiveLauncher::adaptive());

To override just one aspect, pass the named argument — launchers are immutable (final readonly), so a fresh one is constructed:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

Thread::bindLauncher(AdaptiveLauncher::adaptive(
  secret:     $_ENV['APP_SECRET'],
  binaryPath: '/usr/bin/php',
));

bindLauncher is the only binding

Thread::bindLauncher() replaces the per-setting binders. There is no bindBinaryPath(), bindRunner(), bindSerSecurity(), or bindPayloadMode() — every one of those is now a property of the launcher. Rebinding a fresh launcher is how you “reset” to defaults.

The transport belongs to CliLauncher only

AdaptiveLauncher::adaptive() has no transport argument: under Swoole, SwooleLauncher picks delivery itself (shared memory with ext-shmop, a temp file otherwise). An explicit transport only makes sense on the CLI/FPM path — bind CliLauncher::adaptive(transport: …) there.

Set the PHP binary under FPM

Under PHP-FPM or CGI, the running interpreter is the web handler, not a CLI binary that can spawn background workers. CliLauncher already handles this: under a non-CLI SAPI it resolves PHP_BINDIR/php (falling back to php on PATH), so the common case needs no config. Override it only when the CLI binary lives elsewhere:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

Thread::bindLauncher(AdaptiveLauncher::adaptive(binaryPath: '/usr/bin/php'));

For an explicit, environment-independent config, build a CliLauncher with its constructor, setting each part:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\CliLauncher;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

Thread::bindLauncher(new CliLauncher(
  binaryPath: '/usr/bin/php',
  runnerPath: __DIR__ . '/vendor/flytachi/winter-thread/wRunner',
  transport:  new TempFileTransport(),
));

Symptom of a wrong binary

If tasks silently fail to start from a web request, a wrong binary path is the usual cause — the child was launched under the FPM SAPI instead of CLI. Check the binary path first.

Point to the packaged runner script

The child is bootstrapped by the packaged wRunner script (not wExecutor — that name is gone). The launcher finds it automatically. If your deployment relocates vendor/ or you ship a custom bootstrap, set the path explicitly:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

Thread::bindLauncher(AdaptiveLauncher::adaptive(
  runnerPath: __DIR__ . '/vendor/flytachi/winter-thread/wRunner',
));

A custom runner bootstrap is a child-side concern independent of the launcher; if you replace it you typically also replace the launcher. See Runner lifecycle.

Sign payloads app-wide

opis/closure serializes and (optionally) signs tasks so closures and anonymous classes can cross the process boundary. Set a secret app-wide and forged or tampered payloads are rejected in the child before any object is built. Two equivalent ways to supply it:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

// 1. The launcher's named argument
Thread::bindLauncher(AdaptiveLauncher::adaptive(secret: $_ENV['APP_SECRET']));

// 2. No code at all — set the WINTER_THREAD_SECRET env var and the launcher reads it.

The secret reaches the child through the WINTER_THREAD_SECRET environment variable (owner-only), never argv — so signing works just as well when the secret is passed explicitly. Details in Security & performance.

Swoole: the transport is auto-selected

You do not need to detect Swoole yourself. Under an active runtime (inside a coroutine, or with runtime hooks enabled) AdaptiveLauncher routes the launch to SwooleLauncher, which delivers the payload pipe-free — shared memory with ext-shmop, a temp file otherwise. A raw stdin pipe would not do there: it is not coroutine-safe.

Forcing a specific transport is possible only on the CLI/FPM path, on CliLauncher:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\CliLauncher;
use Flytachi\Winter\Thread\Payload\ShmTransport;

// Force shared-memory delivery (requires ext-shmop).
Thread::bindLauncher(CliLauncher::adaptive(transport: new ShmTransport()));

The transport trade-offs are in Swoole & payload delivery and Payload modes.

Advanced: build a worker pool without the Thread facade

Framework code that manages its own pool can reach the launcher and drive it directly, one ProcessHandle per worker instead of a Thread object per task. Serialize the task with the launcher’s security provider, build a LaunchSpec, and harvest with a non-blocking loop:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\LaunchSpec;

$launcher = Thread::launcher();
$payload  = \Opis\Closure\serialize($runnable, $launcher->security());

$handle = $launcher->launch(new LaunchSpec(
  payload:   $payload,
  namespace: 'Pool',
  name:      'MyTask',
));

// non-blocking: true once finished (and reaped), false while still running
if ($handle->reap()) {
  $exit = $handle->getExitCode();
}

Because reap() and detach() never block on a live process, one loop can manage hundreds of handles without zombies. Full signatures for LaunchSpec, Launcher, and ProcessHandle are in the API reference.