Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 97 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ zenpipe(100)
4. [Usage](#usage)
- [Pipeline Operations](#pipeline-operations)
- [Class Methods as Operations](#class-methods-as-operations)
5. [Examples](#examples)
- [RAG Processes](#rag-processes)
- [Email Validation](#email-validation)
- [More Examples](#more-examples)
5. [API Reference](#api-reference)
6. [Contributing](#contributing)
7. [License](#license)
8. [Roadmap](#roadmap)
Expand All @@ -59,28 +58,93 @@ composer require dynamik-dev/zenpipe-php
## Usage
### Pipeline Operations

Pipeline operations are functions that take an input and return a processed value. They can be passed as a single function or as an array with the class name and method name.
Pipeline operations are functions that take an input and return a processed value. Each operation can receive up to three parameters:
- `$input`: The value being processed
- `$next`: A callback to pass the value to the next operation
- `$return`: (Optional) A callback to exit the pipeline early with a value

#### Basic Operation Example
Let's build an input sanitization pipeline:
```php
$pipeline = zenpipe()
->pipe(fn($input, $next) => $next(strtoupper($input)));
// String sanitization pipeline
$sanitizer = zenpipe()
->pipe(fn($input, $next) => $next(trim($input)))
->pipe(fn($input, $next) => $next(preg_replace('/\s+/', ' ', $input)))
->pipe(fn($input, $next) => $next(strip_tags($input)))
->pipe(fn($input, $next) => $next(htmlspecialchars($input)))
->pipe(fn($input, $next) => $next(mb_convert_encoding(
$input, 'UTF-8', mb_detect_encoding($input)
)));

// Usage examples:
$dirtyInput = " <script>alert('xss')</script> Hello World! ¥€$ ";
$cleanInput = $sanitizer($dirtyInput);
// Output: "Hello World! ¥€$"

// Can also be run on demand:
$result = zenpipe($dirtyInput)
->pipe(fn($input, $next) => $next(trim($input)))
->pipe(fn($input, $next) => $next(strip_tags($input)))
->process();
```

#### Operation with Early Return
Below is a practical example of a content moderation pipeline with early returns:
```php
// Content moderation pipeline with early returns
$moderationPipeline = zenpipe()
->pipe(function($content, $next, $return) {
// Skip moderation for trusted authors
if (Auth::user()->isTrusted()) {
return $return([
'status' => 'approved',
'content' => $content,
'skipped' => true
]);
}
return $next($content);
})
->pipe(function($content, $next, $return) {
// Quick check for banned words
if (containsBannedWords($content)) {
return $return([
'status' => 'rejected',
'reason' => 'prohibited_content'
]);
}
return $next($content);
})
->pipe(function($content, $next) {
// Send to AI moderation for nuanced analysis
return $next(
AI::moderate($content)
);
});

// Usage:
$result = $moderationPipeline("Hello, world!");
// Trusted user: Immediately returns approved
// Regular user: Goes through full moderation
```

### Class Methods as Operations

You can also use class methods as operations:
You can also use class methods as operations, with the same parameter options:

```php
class MyClass
{
public function uppercase($input)
public function validate($input, $next, $return)
{
return strtoupper($input);
if (empty($input)) {
return $return('Input cannot be empty');
}
return $next(strtoupper($input));
}
}

$pipeline = zenpipe()
->pipe([MyClass::class, 'uppercase']);
->pipe([MyClass::class, 'validate']);
```

You can also pass an array of operations:
Expand All @@ -89,11 +153,11 @@ You can also pass an array of operations:
$pipeline = zenpipe()
->pipe([
fn($input, $next) => $next(strtoupper($input)),
[MyClass::class, 'uppercase']
[MyClass::class, 'validate']
]);
```

### Examples
### More Examples

#### RAG Processes

Expand Down Expand Up @@ -134,36 +198,43 @@ $ragPipeline = zenpipe()
$answer = $ragPipeline("What's our refund policy?");
```

#### Email Validation
#### Email Validation with Early Return

This pipeline can be used to validate an email address.
This pipeline demonstrates early returns for email validation:

```php
$emailValidationPipeline = zenpipe()
->pipe(function($input, $next) {
$emailValidationPipeline = zenpipe()
->pipe(function($input, $next, $return) {
if (!is_string($input)) {
return $return('Input must be a string');
}
return $next(filter_var($input, FILTER_VALIDATE_EMAIL));
})
->pipe(function($email, $next) {

->pipe(function($email, $next, $return) {
if (!$email) {
return false;
return $return('Invalid email format');
}

$domain = substr(strrchr($email, "@"), 1);
$mxhosts = [];
$mxweight = [];

if (getmxrr($domain, $mxhosts, $mxweight)) {
return $next(true);
if (!getmxrr($domain, $mxhosts)) {
return $return('Domain has no valid mail servers');
}

// If MX records don't exist, check for A record as a fallback
return $next(checkdnsrr($domain, 'A'));
return $next(true);
});


$result = $emailValidationPipeline('example@example.com');
```
// Returns: 'Domain has no valid mail servers'

$result = $emailValidationPipeline('invalid-email');
// Returns: 'Invalid email format'
```

## API Reference

See [API Reference](docs/API.md) for details.

## Contributing

Expand All @@ -176,4 +247,3 @@ The MIT License (MIT). See [LICENSE](LICENSE) for details.
## Roadmap

- [ ] Add support for PSR-15 middleware

77 changes: 77 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# ZenPipe API Reference

The `ZenPipe` class provides a fluent interface for building and executing pipelines of operations.

## Class Overview

```php
namespace DynamikDev\ZenPipe;

class ZenPipe
```

## Methods

### Constructor

```php
public function __construct(mixed $initialValue = null)
```

Creates a new pipeline instance.

- **Parameters:**
- `$initialValue` (mixed|null): The initial value to be processed through the pipeline.

### make()

```php
public static function make(mixed $initialValue = null): self
```

Static factory method to create a new pipeline instance.

- **Parameters:**
- `$initialValue` (mixed|null): The initial value to be processed through the pipeline.
- **Returns:** A new `ZenPipe` instance.

### pipe()

```php
public function pipe($operation): self
```

Adds an operation to the pipeline.

- **Parameters:**
- `$operation`: Can be one of:
- `callable`: A function to process the value
- `array{class-string, string}`: A tuple of [className, methodName]
- `array`: An array of operations to be added sequentially
- **Returns:** The `ZenPipe` instance for method chaining.
- **Throws:** `\InvalidArgumentException` if the specified class does not exist.

### process()

```php
public function process($initialValue = null)
```

Executes the pipeline with the given initial value.

- **Parameters:**
- `$initialValue` (mixed|null): The value to process. If not provided, uses the value from constructor.
- **Returns:** The processed value after running through all operations.
- **Throws:** `\InvalidArgumentException` if no initial value is provided.

### __invoke()

```php
public function __invoke($initialValue)
```

Makes the pipeline instance callable.

- **Parameters:**
- `$initialValue`: The value to process through the pipeline.
- **Returns:** The processed value after running through all operations.
14 changes: 11 additions & 3 deletions src/ZenPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,29 @@ public function process($initialValue = null)
* It wraps the next operation in a closure that can handle both
* static method calls and regular callables.
*
* The operation can now accept a third parameter for early return:
* - For callables: function($value, $next, $return)
* - For class methods: method($value, $next, $return)
*
* @return callable
*/
protected function carry(): callable
public function carry(): callable
{
return function ($next, $operation) {
return function ($value) use ($next, $operation) {
$return = function ($value) {
return $value;
};

if (is_array($operation) && count($operation) === 2 && is_string($operation[0]) && is_string($operation[1])) {
$class = $operation[0];
$method = $operation[1];
$instance = new $class();

return $instance->$method($value, $next);
return $instance->$method($value, $next, $return);
}

return $operation($value, $next);
return $operation($value, $next, $return);
};
};
}
Expand Down
49 changes: 49 additions & 0 deletions tests/Unit/PipelineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,52 @@ public function handle($value, $next)

expect($pipeline(5))->toBe(9);
});

test('pipeline supports early return using third parameter', function () {
$pipeline = zenpipe()
->pipe(function ($value, $next, $return) {
if ($value > 10) {
return $return('early exit');
}
return $next($value + 1);
})
->pipe(function ($value, $next) {
return $next($value * 2);
});

expect($pipeline(5))->toBe(12); // Normal flow: (5 + 1) * 2
expect($pipeline(11))->toBe('early exit'); // Early return
});

test('pipeline supports early return in class methods', function () {
$testClass = new class () {
public function handle($value, $next, $return)
{
if ($value === 5) {
return $return('caught five');
}
return $next($value * 2);
}
};

$pipeline = zenpipe()
->pipe([$testClass, 'handle'])
->pipe(function ($value, $next) {
return $next($value + 3);
});

expect($pipeline(5))->toBe('caught five'); // Early return
expect($pipeline(3))->toBe(9); // Normal flow: (3 * 2) + 3
});

test('pipeline early return works with array of operations', function () {
$pipeline = zenpipe()
->pipe([
fn ($value, $next) => $next($value + 1),
fn ($value, $next, $return) => $value === 6 ? $return('found six') : $next($value * 2),
fn ($value, $next) => $next($value - 3),
]);

expect($pipeline(5))->toBe('found six'); // Early return when value becomes 6
expect($pipeline(3))->toBe(5); // Normal flow: ((3 + 1) * 2) - 3
});
Loading