minfrastructure / chains
Build Different type of chains with optional filtering
Requires
- php: >=8.2
README
A PHP library for building event-driven execution chains with hooks, callbacks, policies, and a rich result system. Zero dependencies.
Chains lets you wrap any operation in a structured pipeline of phases and hooks, so that internal and external code can intercept, validate, transform, or react to every step — without tight coupling.
Current library focuses on reusability. You can implement once and customize the class behavior on according to the business needs.
Requirements
- PHP 8.2+
Installation
composer require minfra/chains
Quick Start
use Minfra\Chains\EventExecutor; use Minfra\Chains\EventInterface; use Minfra\Chains\Result; use Minfra\Chains\Meta; use Minfra\Chains\BasicResultInterface; //A simple example with real world application. class Product { protected string $price; function setPrice(string $price): BasicResultInterface { return EventExecutor::run($this, 'set_price', Meta::make(price: $price), [ EventInterface::ON_MID => [$this, '_set_price'], // or a single hook (any callable) ]); } protected function _set_price(Meta $meta): void { $meta->this->price = $meta->price; // $meta->this refers to the current object. // We could also do `return true;` or do `Result::true($meta)` or `Result::true()` or `Result::true(<ANYTHING>)` // when returning true or returning null(or no return at all). it automatically passes the $meta to the next phase. } }
// The above example, but even simpler class Product { protected string $price; function setPrice(string $price): BasicResultInterface { return EventExecutor::run($this, 'set_price', ...CommonEvents::setter(['price' => $price])); // CommonEvents provides wheels for working with properties } }
// The same as above example, but with more hooks[pipeline] class Product { protected string $price; function setPrice(string $price): BasicResultInterface { list($meta, $setter_cb) = CommonEvents::setter(['price' => $price]); return EventExecutor::run($this, 'set_price', $meta, [ EventInterface::ON_BEFORE => [[$this, "_ensure_correct_price"], [$this, "_ensure_correct_decimal_price"]], EventInterface::ON_MID => [[$this, "_set_price"]], EventInterface::ON_OK => fn(Meta $meta) => error_log("price is: {$meta->price}"), ]); } protected function _ensure_correct_price(Meta $meta): BasicResultInterface|true { if (!is_numeric($meta->price)) { return Result::false("invalid_price", $meta); // Or simply: Result::false("invalid_price"); } return true; // when returning true. it automatically passes the $meta to the next phase. } protected function _ensure_correct_decimal_price(Meta $meta): void { $meta->price = bcmath($meta->price, '1', 2); } protected function _set_price(Meta $meta): void { $meta->this->price = $meta->price; // $meta->this refers to the current object. // We could also do `return true;` or do `Result::true($meta)` or `Result::true()` or `Result::true(<ANYTHING>)` // when returning null(or no return at all). it automatically passes the $meta to the next phase. } }
// The same as above example, but make it pluggable(register external callbacks) use Minfra\Chains\EventExecutor; use Minfra\Chains\EventInterface; use Minfra\Chains\CallbackTrait; use Minfra\Chains\CommonEvents; use Minfra\Chains\Result; use Minfra\Chains\BasicResultInterface; use Minfra\Chains\Meta; class Product implements \Minfra\Chains\CallbackInterface { use CallbackTrait; protected string $price; protected int $type; const EVENT_ON_SET_TYPE = "set_type"; const EVENT_ON_GET_TYPE = "get_type"; const EVENT_ON_SET_PRICE = "set_price"; const EVENT_ON_GET_PRICE = "get_price"; function setType(int $type): BasicResultInterface { return EventExecutor::run($this, static::EVENT_ON_SET_TYPE, ...CommonEvents::setter(['type' => $type])); } function getType(): Result { $type = $this->type ?? null; return EventExecutor::run($this, static::EVENT_ON_GET_TYPE, ...CommonEvents::getter('type', $type)); } function setPrice(string $price): BasicResultInterface { list($meta, $setter_cb) = CommonEvents::setter(['price' => $price]); return EventExecutor::run($this, static::EVENT_ON_SET_PRICE, $meta, [ EventInterface::ON_BEFORE => [[$this, "_ensure_correct_price"], [$this, "_ensure_correct_decimal_price"]], EventInterface::ON_MID => [$setter_cb], EventInterface::ON_OK => fn(Meta $meta) => error_log("price is: {$meta->price}"), ]); } function getPrice(): Result { $price = $this->price ?? null; return EventExecutor::run($this, static::EVENT_ON_GET_PRICE, ...CommonEvents::getter('price', $price)); } protected function _ensure_correct_price(Meta $meta): BasicResultInterface|true { if (!is_numeric($meta->price)) { return Result::false("invalid_price", $meta); // Or simply: Result::false("invalid_price"); } return true; // when returning true. it automatically passes the $meta to the next phase. } protected function _ensure_correct_decimal_price(Meta $meta): BasicResultInterface|true { // $meta->price = bcmath($meta->price, '1', 2); return true; } } $product = new Product(); $product->appendCallback((new \Minfra\Chains\CallbackEntry($product::EVENT_ON_SET_PRICE, \Minfra\Chains\CallbackEntryInterface::EVENT_PRE))->setCallback(function(Meta $meta) { /** @var Product $product */ $product = $meta->this; if ($product->getType()->data()->type === 222) { if ($meta->price > "20") { return Result::false("invalid_product_222_issue", ['reason' => 'cannot be higher than 20']); // or if you prefer using meta: return Result::false("invalid_product_222_issue", Meta::make(reason: 'cannot be higher than 20')); } } return true; })); $product->appendCallback((new \Minfra\Chains\CallbackEntry($product::EVENT_ON_SET_PRICE, \Minfra\Chains\CallbackEntryInterface::EVENT_POST))->setCallback(function(Meta $meta) { var_dump("I got the price: {$meta->price}. let's do something with it."); })); $product->setType(222); var_dump($product->setPrice('21')->ok()); // false var_dump($product->setPrice('19')->ok()); // true
You can run the latest example here
How It Works
Every call to EventExecutor::run() executes a fixed sequence of phases:
ON_BEFORE → PRE callbacks → ON_MID → POST callbacks → ON_AFTER
│
┌────────────────────────────────┘
▼
ON_INCOMPLETE (if not completed)
ON_DONE (if done flag set)
ON_OK (if ok)
ON_FAILED (if not ok)
- Hooks are functions, methods or closures you pass to
EventExecutor::run(), keyed by phase. - Callbacks are externally registered on the instance itself (via
CallbackInterface) and run at the PRE and POST stages. think of them as plugin. - Results flow through the chain. Each step can inspect, modify, or replace the current result.
- Policies control what happens on failure, incomplete results, retries, and early termination.
Key Concepts
Result
The value object that flows through the chain. Carries state flags and a data payload.
Result::true($data, completed: true); // success Result::false('error_code', $data); // failure Result::incomplete('pending', $data); // not yet done Result::from($otherResult, ok: false); // clone with overrides $result->ok(); // bool — success? $result->data(); // mixed — the payload $result->status(); // string — error/status code $result->completed(); // bool — work fully done? $result->done(); // bool — stop the chain early? $result->again(); // bool — retry this step?
Callback Return Contract
Every hook/callback receives ($data, $phase, $executor, $result) and returns:
| Return value | Effect |
|---|---|
null or true |
Continue with the current result unchanged |
BasicResultInterface |
Replace the current result |
CommonEvents Helpers
Ready-made [Meta, callable] pairs for property operations on objects — including protected/private properties. Use the spread operator to pass them to EventExecutor::run():
// Set protected properties EventExecutor::run($obj, 'set_name', ...CommonEvents::setter(['name' => 'Alice'])); // Read & validate EventExecutor::run($obj, 'get_name', ...CommonEvents::getter('name', $value)); // Array operations EventExecutor::run($obj, 'add_tag', ...CommonEvents::adding('tags', ['php'])); EventExecutor::run($obj, 'append', ...CommonEvents::append('logs', 'auth', ['entry'])); // Remove & reset EventExecutor::run($obj, 'remove', ...CommonEvents::removing(['email'])); EventExecutor::run($obj, 'clear', ...CommonEvents::cleanup('tags', []));
CallbackInterface
Objects that implement CallbackInterface (via CallbackTrait) can register their own callbacks. The executor picks them up automatically:
class Order implements CallbackInterface { use CallbackTrait; function __construct() { $this->appendCallback( (new CallbackEntry('save', 'pre')) ->setCallback(function (Meta $d) { // validate before save return true; }), (new CallbackEntry('save', 'post')) ->setCallback(function (Meta $d) { // notify after save return Result::true($d, completed: true); }), ); } }
Execution Policy
DefaultEventExecutorPolicy controls chain behavior:
$policy = new DefaultEventExecutorPolicy( jump_on_failure: true, // continue chain after a failed step jump_on_incomplete: false, // stop on incomplete results keep_running_on_done: false, // stop when done flag is set ignore_failed_data_on_jump: true, // don't forward failed data mark_incomplete_as_failed: true, // treat incomplete as failure again_limit: 1, // max retries per callback ); EventExecutor::run($instance, 'event', policy: $policy, ...);
Source Interfaces
An instance can implement these optional interfaces so the executor automatically queries it for configuration — no extra arguments needed:
| Interface | Method | Purpose |
|---|---|---|
EventSourceDataInterface |
getEventData($name, $data) |
Provide custom event data |
EventSourceHooksInterface |
getEventHooks($name, $hooks) |
Inject hooks automatically |
EventSourcePolicyInterface |
getEventExecutorPolicy($name) |
Provide per-event policy |
Retry Mechanism
A callback can request a retry by calling setAgain() on the result. The executor triggers ON_AGAIN, then re-runs the callback up to againLimit times:
function (Meta $d) { if ($transientFailure) { return Result::true($d)->setAgain(); } return Result::true($d, completed: true); }
Examples
The examples/ directory contains runnable scripts covering every feature:
| File | Topic |
|---|---|
01_minimal.php |
Simplest possible usage |
02_hooks_and_phases.php |
Full phase lifecycle, terminal hooks |
03_results.php |
Result creation, states, from() |
04_common_events.php |
getter/setter/adding/removing/cleanup, safe reflection |
05_callbacks.php |
CallbackInterface, PRE/POST stages, priority |
06_policy.php |
All policy options demonstrated |
07_retry.php |
again mechanism, limits, abort |
08_source_interfaces.php |
EventSourceData/Hooks/Policy interfaces |
09_model_lifecycle.php |
Full domain model with event-driven getters/setters |
# Run all examples php examples/run_all.php # Run a single example php examples/01_minimal.php
Tutorial
See TUTORIAL.md for a step-by-step walkthrough from basic usage to building a full domain model with event-driven property access.