Skip to content

Commit f9b9417

Browse files
Merge pull request #4 from dynamik-dev/feat/context
Add Context Value
2 parents 396beba + bd7985f commit f9b9417

6 files changed

Lines changed: 388 additions & 11 deletions

File tree

README.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ zenpipe(100)
4040
4. [Usage](#usage)
4141
- [Pipeline Operations](#pipeline-operations)
4242
- [Class Methods as Operations](#class-methods-as-operations)
43+
- [Context Passing](#context-passing)
44+
- [Exception Handling](#exception-handling)
4345
- [More Examples](#more-examples)
4446
5. [API Reference](#api-reference)
4547
6. [Contributing](#contributing)
@@ -58,10 +60,11 @@ composer require dynamik-dev/zenpipe-php
5860
## Usage
5961
### Pipeline Operations
6062

61-
Pipeline operations are functions that take an input and return a processed value. Each operation can receive up to three parameters:
63+
Pipeline operations are functions that take an input and return a processed value. Each operation can receive up to four parameters:
6264
- `$input`: The value being processed
6365
- `$next`: A callback to pass the value to the next operation
6466
- `$return`: (Optional) A callback to exit the pipeline early with a value
67+
- `$context`: (Optional) A shared context object passed to all operations
6568

6669
#### Basic Operation Example
6770
Let's build an input sanitization pipeline:
@@ -157,6 +160,77 @@ $pipeline = zenpipe()
157160
]);
158161
```
159162

163+
### Context Passing
164+
165+
You can pass a shared context object to all operations using `withContext()`. This is useful for sharing state, configuration, or dependencies across the pipeline without threading them through the value.
166+
167+
```php
168+
// Use any object as context - your own DTO, stdClass, or array
169+
class RequestContext
170+
{
171+
public function __construct(
172+
public string $userId,
173+
public array $permissions,
174+
public array $logs = []
175+
) {}
176+
}
177+
178+
$context = new RequestContext(
179+
userId: 'user-123',
180+
permissions: ['read', 'write']
181+
);
182+
183+
$result = zenpipe(['action' => 'update', 'data' => [...]])
184+
->withContext($context)
185+
->pipe(function ($request, $next, $return, RequestContext $ctx) {
186+
if (!in_array('write', $ctx->permissions)) {
187+
return $return(['error' => 'Unauthorized']);
188+
}
189+
$ctx->logs[] = "Permission check passed for {$ctx->userId}";
190+
return $next($request);
191+
})
192+
->pipe(function ($request, $next, $return, RequestContext $ctx) {
193+
$ctx->logs[] = "Processing {$request['action']}";
194+
return $next([...$request, 'processed_by' => $ctx->userId]);
195+
})
196+
->process();
197+
198+
// Context is mutable - logs are accumulated across operations
199+
// $context->logs = ['Permission check passed for user-123', 'Processing update']
200+
```
201+
202+
Type hint your context parameter in the operation signature for IDE support:
203+
204+
```php
205+
/** @var ZenPipe<array, RequestContext> */
206+
$pipeline = zenpipe()
207+
->withContext(new RequestContext(...))
208+
->pipe(fn($value, $next, $return, RequestContext $ctx) => ...);
209+
```
210+
211+
### Exception Handling
212+
213+
Use `catch()` to handle exceptions gracefully without breaking the pipeline:
214+
215+
```php
216+
$result = zenpipe($userData)
217+
->pipe(fn($data, $next) => $next(validateInput($data)))
218+
->pipe(fn($data, $next) => $next(processPayment($data))) // might throw
219+
->pipe(fn($data, $next) => $next(sendConfirmation($data)))
220+
->catch(fn(Throwable $e, $originalValue) => [
221+
'error' => $e->getMessage(),
222+
'input' => $originalValue,
223+
])
224+
->process();
225+
```
226+
227+
The catch handler receives:
228+
- `$e`: The thrown exception (`Throwable`)
229+
- `$value`: The original input value passed to `process()`
230+
- `$context`: The context set via `withContext()` (null if not set)
231+
232+
If no catch handler is set, exceptions propagate normally.
233+
160234
### More Examples
161235

162236
#### RAG Processes

docs/API.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,47 @@ Static factory method to create a new pipeline instance.
3535
- `$initialValue` (mixed|null): The initial value to be processed through the pipeline.
3636
- **Returns:** A new `ZenPipe` instance.
3737

38+
### withContext()
39+
40+
```php
41+
public function withContext(mixed $context): self
42+
```
43+
44+
Sets a context object that will be passed to all operations as the fourth parameter.
45+
46+
- **Parameters:**
47+
- `$context` (mixed): Any value to be passed as context (object, array, DTO, etc.)
48+
- **Returns:** The `ZenPipe` instance for method chaining.
49+
50+
**Example:**
51+
```php
52+
$pipeline = zenpipe($value)
53+
->withContext(new MyContext())
54+
->pipe(fn($v, $next, $return, MyContext $ctx) => $next($v));
55+
```
56+
57+
### catch()
58+
59+
```php
60+
public function catch(callable $handler): self
61+
```
62+
63+
Sets an exception handler for the pipeline.
64+
65+
- **Parameters:**
66+
- `$handler` (callable): A function that receives `(Throwable $e, mixed $originalValue, mixed $context)` and returns a fallback value.
67+
- **Returns:** The `ZenPipe` instance for method chaining.
68+
69+
**Example:**
70+
```php
71+
$pipeline = zenpipe($value)
72+
->withContext($myContext)
73+
->pipe(fn($v, $next) => $next(riskyOperation($v)))
74+
->catch(fn($e, $value, $ctx) => ['error' => $e->getMessage()]);
75+
```
76+
77+
If an exception occurs and no catch handler is set, the exception propagates normally.
78+
3879
### pipe()
3980

4081
```php
@@ -51,6 +92,13 @@ Adds an operation to the pipeline.
5192
- **Returns:** The `ZenPipe` instance for method chaining.
5293
- **Throws:** `\InvalidArgumentException` if the specified class does not exist.
5394

95+
**Operation Parameters:**
96+
Operations receive up to four parameters:
97+
1. `$value` - The current value being processed
98+
2. `$next` - Callback to pass value to next operation
99+
3. `$return` - Callback to exit pipeline early with a value
100+
4. `$context` - The context set via `withContext()` (null if not set)
101+
54102
### process()
55103

56104
```php

phpstan.neon

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ parameters:
66
- vendor
77
ignoreErrors:
88
- '#PHPDoc tag @var#'
9-
-
9+
-
1010
message: '#Template type T of function zenpipe\(\) is not referenced in a parameter.#'
1111
path: src/helpers.php
12+
-
13+
message: '#Template type TContext of function zenpipe\(\) is not referenced in a parameter.#'
14+
path: src/helpers.php
1215
reportUnmatchedIgnoredErrors: false

src/ZenPipe.php

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,54 @@
44

55
/**
66
* @template T
7+
* @template TContext
78
*/
89
class ZenPipe
910
{
1011
/** @var array<callable|array{class-string, string}> */
1112
protected array $operations = [];
1213

14+
/** @var TContext|null */
15+
protected mixed $context = null;
16+
17+
/** @var callable|null */
18+
protected $exceptionHandler = null;
19+
1320
public function __construct(protected mixed $initialValue = null)
1421
{
1522
}
1623

24+
/**
25+
* Set an exception handler for the pipeline.
26+
*
27+
* @param callable(\Throwable, mixed, TContext|null): mixed $handler
28+
* @return self<T, TContext>
29+
*/
30+
public function catch(callable $handler): self
31+
{
32+
$this->exceptionHandler = $handler;
33+
34+
return $this;
35+
}
36+
37+
/**
38+
* Set the context to be passed to each operation.
39+
*
40+
* @template TNewContext
41+
*
42+
* @param TNewContext $context
43+
* @return self<T, TNewContext>
44+
*/
45+
public function withContext(mixed $context): self
46+
{
47+
$this->context = $context;
48+
49+
return $this;
50+
}
51+
1752
/**
1853
* @param mixed|null $initialValue
19-
* @return self<T>
54+
* @return self<T, TContext>
2055
*/
2156
public static function make(mixed $initialValue = null): self
2257
{
@@ -25,7 +60,7 @@ public static function make(mixed $initialValue = null): self
2560

2661
/**
2762
* @param callable|array{class-string, string} $operation
28-
* @return self<T>
63+
* @return self<T, TContext>
2964
*/
3065
public function pipe($operation): self
3166
{
@@ -78,6 +113,7 @@ public function __invoke($initialValue)
78113
* @param T|null $initialValue
79114
* @return T
80115
* @throws \InvalidArgumentException
116+
* @throws \Throwable
81117
*/
82118
public function process($initialValue = null)
83119
{
@@ -93,17 +129,24 @@ public function process($initialValue = null)
93129
$this->passThroughOperation()
94130
);
95131

96-
return $pipeline($value);
132+
try {
133+
return $pipeline($value);
134+
} catch (\Throwable $e) {
135+
if ($this->exceptionHandler !== null) {
136+
return ($this->exceptionHandler)($e, $value, $this->context);
137+
}
138+
throw $e;
139+
}
97140
}
98141

99142
/**
100143
* This method is used to carry the value through the pipeline.
101144
* It wraps the next operation in a closure that can handle both
102145
* static method calls and regular callables.
103146
*
104-
* The operation can now accept a third parameter for early return:
105-
* - For callables: function($value, $next, $return)
106-
* - For class methods: method($value, $next, $return)
147+
* Operations can accept up to four parameters:
148+
* - For callables: function($value, $next, $return, $context)
149+
* - For class methods: method($value, $next, $return, $context)
107150
*
108151
* @return callable
109152
*/
@@ -120,10 +163,10 @@ public function carry(): callable
120163
$method = $operation[1];
121164
$instance = new $class();
122165

123-
return $instance->$method($value, $next, $return);
166+
return $instance->$method($value, $next, $return, $this->context);
124167
}
125168

126-
return $operation($value, $next, $return);
169+
return $operation($value, $next, $return, $this->context);
127170
};
128171
};
129172
}

src/helpers.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
* Create a new ZenPipe instance.
88
*
99
* @template T
10-
* @return ZenPipe<T>
10+
* @template TContext
11+
*
12+
* @return ZenPipe<T, TContext>
1113
*/
1214
function zenpipe(mixed $initialValue = null): ZenPipe
1315
{

0 commit comments

Comments
 (0)