tommyknocker / chain
Professional-grade fluent method chaining library with advanced features: conditional execution, timeout protection, error handling, extension system, and PSR-11 container integration.
Installs: 8
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/tommyknocker/chain
Requires
- php: ^8.0
- psr/container: ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^1.11 || ^2.0
- phpunit/phpunit: ^9.6 || ^10.5 || ^11.0
README
Fluent method chaining & context switching utility for PHP. Call methods across objects, transform or branch chains, and conditionally execute logic in a concise style.
Quick Example
use tommyknocker\chain\Chain; // Process user data through multiple transformations $orderTotal = Chain::of(new User('Alice', 25)) ->setEmail('alice@example.com') ->tap(fn($u) => logger()->info("Processing order for: " . $u->getName())) ->map(fn($user) => new Order(strlen($user->getName()) * 10)) ->when( fn($order) => $order->getTotal() > 30, fn($chain) => $chain->applyDiscount(5) ) ->getTotal() ->get(); echo $orderTotal; // 45 (50 - 5 discount)
Installation
composer require tommyknocker/chain
Core Concepts
- Start with
Chain::of($object)orChain::of(ClassName::class, ...$args) - Every method call stays fluent; if a method returns an object, the chain context switches to it automatically
- Use
change($idOrObject)to jump to another object (resolved via PSR-11 container if configured) - Use
get()to read the last result (or the current instance if there is no last result)
API Overview
Core Methods:
of(string|object $target, ...$args): Chain- Start a chain from an object or instantiate a classget(): mixed- Get final resultvalue(): mixed- Alias for get(), more semanticinstance(): object- Get current wrapped object
Transformation:
tap(callable $fn): Chain- Execute side effectsmap(callable $fn): Chain- Transform to another objectpipe(callable ...$pipes): Chain- Functional pipeline
Enhanced Control Flow:
whenAll(callable ...$conditions): Chain- Execute when ALL conditions are truewhenAny(callable ...$conditions): Chain- Execute when ANY condition is truewhenNone(callable ...$conditions): Chain- Execute when NO conditions are truewhen(bool|callable $cond, callable $cb, ?callable $else = null): Chain- Conditional executionunless(bool|callable $cond, callable $cb, ?callable $else = null): Chain- Inverse conditionalclone(): Chain- Branch immutably
Resilience:
rescue(callable $callback, callable $handler): Chain- Handle exceptions with fallbackcatch(string $exceptionClass, callable $callback, callable $handler): Chain- Catch specific exceptionsretry(int $times, callable $callback, int $delayMs = 0): Chain- Retry with backofftimeout(int $seconds, callable $callback): Chain- Timeout protection
Iteration & Debugging:
each(callable $fn): Chain- Iterate over collectionsdump(string $label = ''): Chain- Debug output, continues chaindd(string $label = ''): never- Dump and die
Container Integration:
change(string|object $target): Chain- Switch to another object (PSR-11)
Configuration & Extensions:
Chain::configure(ChainConfig $config): void- Configure Chain behavioraddExtension(ChainExtensionInterface $extension): Chain- Add extension for monitoring/logging
Examples
Basic Chaining
// Create instance and chain $result = Chain::of(new User('Alice', 25)) ->getName() ->map(fn($user) => new Order(strlen($user->getName()) * 10)) ->getTotal() ->get(); // Or instantiate via of() $result = Chain::of(StringBuilder::class, 'Hello') ->append(' World') ->uppercase() ->toString() ->get();
Conditional Logic
// Smart banking: apply bonus for high-value accounts, charge fees for low balances $finalBalance = Chain::of(new Account()) ->deposit(500) ->when( fn($acc) => $acc->getBalance() > 300, fn($chain) => $chain->addBonus() // +100 bonus ) ->unless( fn($acc) => $acc->getBalance() < 100, fn($chain) => $chain->withdraw(50) // maintenance fee ) ->getBalance() ->get(); echo $finalBalance; // 550 (500 + 100 bonus - 50 fee)
Enhanced Conditional Logic
// Multiple condition checking $result = Chain::of(new User('Alice', 25)) ->whenAll( fn($u) => $u->isAdult(), fn($u) => strlen($u->getName()) > 3, fn($u) => $u->getAge() < 50 ) ->tap(fn($u) => $u->setEmail('alice@example.com')) ->getEmail() ->get(); // Any condition can be true $result = Chain::of(new User('Bob', 16)) ->whenAny( fn($u) => $u->isAdult(), fn($u) => $u->getAge() > 15, fn($u) => strlen($u->getName()) > 2 ) ->tap(fn($u) => $u->addRole('verified')) ->getRoles() ->get(); // No conditions should be true $result = Chain::of(new User('Charlie', 25)) ->whenNone( fn($u) => $u->getAge() > 30, fn($u) => $u->getAge() < 18, fn($u) => strlen($u->getName()) < 3 ) ->tap(fn($u) => $u->addRole('special')) ->getRoles() ->get();
// Complex order processing with business rules $total = Chain::of(new Order(100.0)) ->when( fn($order) => $order->getTotal() > 50, fn($chain) => $chain->applyDiscount(10) // $10 off for orders > $50 ) ->unless( fn($order) => $order->getTotal() < 20, fn($chain) => $chain->addTax(0.08) // 8% tax unless small order ) ->getTotal() ->get(); echo $total; // 97.20 (100 - 10 discount + 8% tax on 90)
Pipeline
// Calculate price with multiple transformations $finalPrice = Chain::of(new Calculator(100)) ->subtract(20) // Apply discount ->multiply(1.08) // Add 8% tax ->pipe( fn($c) => $c->getValue(), fn($v) => round($v, 2), // Round to 2 decimals fn($v) => max($v, 0) // Ensure non-negative ) ->get(); echo $finalPrice; // 86.4
Pipelines with pipe()
// Text processing pipeline for user input sanitization $sanitized = Chain::of(new StringBuilder(' Hello@World123 ')) ->pipe( fn($b) => $b->toString(), fn($text) => trim($text), fn($text) => strtolower($text), fn($text) => preg_replace('/[^a-z0-9]/', '', $text) ) ->get(); echo $sanitized; // 'helloworld123'
Tap for Side Effects
// Using tap for logging without breaking the chain $logger = Chain::of(new Logger()) ->log('Starting process') ->log('Loading data') ->tap(fn($l) => print("Current logs: " . $l->count() . "\n")) ->log('Processing') ->tap(fn($l) => print("Logs so far: " . implode(', ', $l->getLogs()) . "\n")) ->log('Completed') ->instance(); echo "Total logs: " . $logger->count() . "\n";
Branching
// Calculate different pricing scenarios from same base $baseCalc = Chain::of(new Calculator(100)); $retailPrice = $baseCalc->clone()->multiply(1.5)->getValue()->get(); // 150 $wholesalePrice = $baseCalc->clone()->multiply(1.2)->getValue()->get(); // 120 $memberPrice = $baseCalc->clone()->multiply(0.9)->getValue()->get(); // 90
Immutable Branching with clone()
// Explore different account scenarios without mutating original $baseAccount = Chain::of(new Account()); $scenario1 = $baseAccount->clone() ->deposit(1000) ->withdraw(200) ->getBalance()->get(); // 800 $scenario2 = $baseAccount->clone() ->deposit(500) ->addBonus() ->getBalance()->get(); // 600 (500 + 100 bonus) $original = $baseAccount->getBalance()->get(); // 0 (unchanged)
Composite Scenario
// Complex data processing workflow with multiple stages $report = Chain::of(new DataProcessor()) ->addItem(100) ->addItem(250) ->addItem(75) ->addItem(300) ->tap(fn($p) => logger()->info("Processing " . $p->count() . " items")) ->filter(fn($x) => $x >= 100) // Only items >= 100 ->transform(fn($x) => $x * 1.08) // Add 8% markup ->pipe( fn($p) => ['total' => $p->sum(), 'count' => $p->count()], fn($stats) => new Report($stats['total'], $stats['count']), fn($report) => $report->format() ) ->get(); echo $report; // "Total: 702.00, Count: 3, Average: 234.00"
Timeout Protection
// Protect against slow operations try { $result = Chain::of(new Calculator(10)) ->timeout(2, function ($calc) { // Simulate slow operation usleep(1000000); // 1 second return $calc->add(5); }) ->getValue() ->get(); echo "Result: $result\n"; } catch (\tommyknocker\chain\Exception\ChainTimeoutException $e) { echo "Operation timed out: " . $e->getMessage() . "\n"; }
Configuration & Extensions
// Configure Chain behavior Chain::configure(ChainConfig::performance()); // Add monitoring extension class LoggingExtension implements ChainExtensionInterface { private array $logs = []; public function beforeMethodCall(string $method, array $args): void { $this->logs[] = "Before: {$method}"; } public function afterMethodCall(string $method, mixed $result): void { $this->logs[] = "After: {$method}"; } public function getLogs(): array { return $this->logs; } } $logger = new LoggingExtension(); $result = Chain::of(new Calculator(10)) ->addExtension($logger) ->add(5) ->multiply(2) ->getValue() ->get(); // Check logs foreach ($logger->getLogs() as $log) { echo "$log\n"; }
// PSR-11 Container integration with change() $container = new SimpleContainer(); $container->set('email', new EmailService()); $container->set('notification', new NotificationService()); Chain::setResolver($container); // Switch between services dynamically $result1 = Chain::of($container->get('email')) ->send('user@example.com') ->get(); $result2 = Chain::of($container->get('email')) ->change('notification') // Switch to notification service ->notify('Important update') ->get(); echo "$result1\n"; // "Email sent to user@example.com" echo "$result2\n"; // "Notification: Important update"
Development
Installation
composer install
Testing
composer test # Run all tests composer test:coverage # Run tests with coverage report composer test:ci # Run tests for CI (with JUnit output)
Code Quality
composer phpstan # Static analysis composer phpstan:baseline # Generate PHPStan baseline composer cs:fix # Fix code style issues composer cs:check # Check code style (dry-run) composer quality # Run all quality checks composer quality:fix # Run quality checks and fix issues
Examples
composer examples # Test all examples
Release Management
composer release # Create new release
Examples
See the examples/ directory for working examples:
workflow.php- Complete workflow with User→Profile context switchingconditionals.php- Conditional execution with when/unlessbranching.php- Clone chains for independent branchespipeline.php- Functional pipelines with pipe()processing.php- Data processing with each(), dump(), value()resilience.php- Error handling with rescue(), catch(), retry()container.php- PSR-11 container integrationadvanced-features.php- NEW! All enhanced features demo
Run examples:
php examples/advanced-features.php # Run specific example composer examples # Test all examples
License
MIT. See LICENSE file.
Changelog
See CHANGELOG.md for a list of changes and version history.