tommyknocker / chain
Chainable class for fluent, cross-object method chaining.
Installs: 7
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)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2025-10-13 16:45:56 UTC
README
A lightweight chaining utility for PHP 8.4+.
It allows you to fluently call methods across different objects, switch the current context on the fly,
and still access the last result or the current instance.
Why Chain?
The problem
In plain PHP, when you need to work with multiple objects, you often end up with verbose code:
$user = new User('Alice'); $name = $user->getName(); $order = $container->get('order'); $total = $order->getTotal(); $user2 = $container->get('user'); $name2 = $user2->getName();
This is repetitive, breaks the flow, and mixes object creation, method calls, and dependency resolution.
The solution
Chain
provides a fluent, expressive way to work with objects:
$chain = Chain::from(new User('Alice')) ->getName() ->change('order') ->getTotal() ->change('user') ->getName(); echo $chain->result(); // "Alice"
Benefits
- Fluent syntax: write less boilerplate, focus on the logic.
- Context switching: move between objects seamlessly with
change()
. - Integration with DI: resolve objects from a registry or PSR‑11 container.
- Functional helpers:
tap
andmap
let you debug, log, or transform objects inline. - Consistency: always access the last result with
result()
and the current object withinstance()
.
When to use
- Building pipelines where objects change along the way.
- Writing expressive tests with minimal setup code.
- Debugging or logging intermediate states without breaking the chain.
- Rapid prototyping when you want to focus on flow, not boilerplate.
Installation
composer require tommyknocker/chain
Quick example
use tommyknocker\chain\Chain; // Start with a User object $user = new User('Bob'); // Switch to an Order object from the registry/DI container // Then switch back to the original User $chain = Chain::from($user) ->getName() // "Bob" ->change('order') // switch to Order ->getTotal() // 99.95 ->change('user') // switch back to User (Alice) ->getName(); // "Alice" echo $chain->result(); // "Alice"
Features
- Fluent chaining across multiple objects
- Context switching with
change()
- Dependency injection support via a resolver (PSR‑11 compatible)
- Functional helpers:
tap(callable $fn)
— run a callback without changing the contextmap(callable $fn)
— transform the current instance into a new object
- Accessors:
$chain->result()
— last method call result$chain->instance()
— current object in the chain
Usage
Creating a chain
$chain = Chain::from(new Foo());
Or create an object by class name:
$chain = Chain::make(Foo::class, 'arg1', 'arg2');
Calling methods
$chain->someMethod('param'); echo $chain->result();
Switching context
$chain ->change('order') // resolved from registry or DI ->getTotal();
Using tap
$chain->tap(function ($instance) { // Log or debug without changing the chain error_log(get_class($instance)); });
Using map
$chain->map(fn($user) => new Profile($user->getName())) ->getProfileName();
Resolver / Registry
$registry = new Registry(); $registry->set('user', new User('Alice')); $registry->set('order', new Order(99.95)); Chain::setResolver($registry);
Now you can switch by key:
$chain->change('order')->getTotal();
Advanced examples
Combining tap
, map
, and change
You can mix functional helpers with context switching to build expressive chains:
use tommyknocker\chain\Chain; // Assume we have a registry with "user" and "order" already set $chain = Chain::from(new User('Bob')) ->tap(function ($user) { // Log the current user echo "Current user: " . $user->getName(); }) ->map(fn($user) => new Profile($user->getName())) // transform User -> Profile ->tap(function ($profile) { // Log the profile echo "Profile created: " . $profile->getProfileName(); }) ->change('order') // switch to Order from registry/DI ->getTotal() ->change('user') // switch back to User (Alice) ->getName(); echo $chain->result(); // "Alice"
Using map for DTO transformation
$chain = Chain::from(new User('Alice')) ->map(fn(User $user) => new UserDTO($user->getName(), $user->getEmail())) ->getEmail(); echo $chain->result(); // e.g. "alice@example.com"
Using tap for debugging
$chain = Chain::from(new Order(199.50)) ->tap(fn($order) => error_log("Order total: " . $order->getTotal())) ->getTotal(); echo $chain->result(); // 199.50
These helpers make it easy to add logging, debugging, or transformations without breaking the fluent chain.
Testing
./vendor/bin/phpunit --bootstrap vendor/autoload.php tests
or
composer run-script test