diff --git a/README.md b/README.md index b8e77c4..f8eab9e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 = " 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: @@ -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 @@ -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 @@ -176,4 +247,3 @@ The MIT License (MIT). See [LICENSE](LICENSE) for details. ## Roadmap - [ ] Add support for PSR-15 middleware - diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..b8af952 --- /dev/null +++ b/docs/API.md @@ -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. diff --git a/src/ZenPipe.php b/src/ZenPipe.php index 29f550f..d053819 100644 --- a/src/ZenPipe.php +++ b/src/ZenPipe.php @@ -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); }; }; } diff --git a/tests/Unit/PipelineTest.php b/tests/Unit/PipelineTest.php index d9a9e88..3fefb74 100644 --- a/tests/Unit/PipelineTest.php +++ b/tests/Unit/PipelineTest.php @@ -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 +});