Search by

Fluent chains, reusable application flows, and bounded concurrent HTTP batches for PHP 8.5.

Package info

github.com/tommyknocker/chain

pkg:composer/tommyknocker/chain

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.1 2026-10-26 19:22 UTC

This package is auto-updated.

Last update: 2026-10-26 19:49:25 UTC


README

CI License: MIT PHP Version PHPStan Level 8

Three focused PHP utilities in one package: fluent object calls, reusable step plans, and bounded concurrent HTTP requests.

Installation

composer require tommyknocker/chain

The package requires PHP 8.5+, ext-curl, and psr/container. ext-curl is required even if you only use Chain or Flow, because all three utilities are part of one package. Chain::timeout() additionally requires ext-pcntl.

Choose an execution model

Utility Use it for When work runs Result
Chain Fluent calls and transformations around an object Immediately as methods are called A current object and current value
Flow A reusable sequence of application steps On run() or execute() A final value or detailed execution
Batch Fetching or downloading independent HTTP resources Inside a blocking run() A report of successes and failures

These utilities are independent and can be combined where useful.

Chain: work with an object

Wrap an object, call its methods fluently, and read the current value:

use tommyknocker\chain\Chain;

$counter = new class {
    private int $value = 0;

    public function next(): int { return ++$this->value; }
    public function reset(): void { $this->value = 0; }
};

$chain = Chain::of($counter);
$value = $chain->next()
    ->pipe(static fn (int $n): int => $n * 10)
    ->get();                         // 10

$receiver = $chain->instance();      // $counter
$result = $chain->reset()->get();    // null

get() returns the current value. Initially it is the wrapped object; after a wrapped method call it is that method's result, including null from a void method. instance() returns the current object used as the receiver for wrapped method calls. An object returned by a call becomes the new receiver; a scalar or null leaves the receiver unchanged.

Use invoke('method', ...$args) when a wrapped method name conflicts with the Chain API. requireInstance(Foo::class) verifies the receiver type for PHPStan after a dynamic transition. Chain also provides branching, conditions, transformations, retry policies, and extension hooks.

See the state model, Chain examples, and API reference.

Flow: compose reusable steps

Build a plan once, then run it with different inputs:

use tommyknocker\chain\Flow;

$base = Flow::of(static fn (string $name): string => trim($name));
$normalized = $base->then(static fn (string $name): string => strtolower($name));

$base->run('  PHP  ');        // 'PHP'
$normalized->run('  PHP  ');  // 'php'

Steps can be callables, StepInterface objects, class names resolved through a PSR-11 container, or nested flows. then() returns a new plan. Flow::stop() ends execution successfully, while execute() provides a result, failure, stop state, and step trace.

Plan immutability is structural: step objects, closure captures, and payloads are not cloned and may retain mutable state between runs. See the Flow guide and the runnable Flow example.

Batch: fetch and download URLs

Fetch response bodies into memory:

use tommyknocker\chain\Batch;

$texts = Batch::fetch([
    'article-1' => 'https://example.com/first.txt',
    'article-2' => 'https://example.com/second.txt',
])->concurrency(8)->run()->throwIfFailed()->bodies();

// $texts is keyed by article ID.

Or stream responses to files, with file names supplied as input keys:

$report = Batch::download([
    'avatar-1.jpg' => 'https://example.com/images/first.jpg',
    'avatar-2.jpg' => 'https://example.com/images/second.jpg',
])->toDirectory('/var/app/images')
    ->fileNamesFromKeys()
    ->concurrency(8)
    ->run();

$downloads = $report->downloads(); // Successful downloads, keyed by file name.
$failures = $report->failures();   // Final failures, keyed by file name.

Batch uses cURL multi for bounded concurrent HTTP I/O. run() blocks until the batch finishes or stops; it does not run PHP callbacks in parallel. The report can contain partial results. throwIfFailed() throws when failures were recorded; it does not roll back successful requests or files, and cancellation does not create failures for work that never started.

For request headers, authentication, retries, size limits, file naming, and other controls, see the Batch guide and API reference.

Documentation

Development

composer install
composer quality       # Style, PHPStan, and tests
composer examples      # Run the examples
composer test          # PHPUnit only
composer test:coverage # Coverage report
composer phpstan       # Static analysis
composer cs:fix        # Apply code style fixes

License

MIT. See LICENSE.