Skip to content

Commit bae6db9

Browse files
committed
Pipeline Pattern added.
1 parent c4f9cc8 commit bae6db9

File tree

6 files changed

+733
-1
lines changed

6 files changed

+733
-1
lines changed

.phpcs-cache

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

README.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Quick Links:
2727
- [7. Specification Pattern](#7-specification-pattern)
2828
- [8. Strategy Pattern](#8-strategy-pattern)
2929
- [9. State Pattern](#9-state-pattern)
30+
- [10. Pipeline Pattern](#10-pipeline-pattern)
3031

3132
### 1. Singleton Pattern
3233
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. Our implementation uses a trait to make it reusable.
@@ -557,6 +558,164 @@ The State pattern is perfect for managing complex workflows like order processin
557558
- Payment Processing
558559
- Task Management Systems
559560

561+
### 10. Pipeline Pattern
562+
563+
The Pipeline pattern allows you to process data through a series of operations, where each operation takes input from the previous operation and produces output for the next one. This pattern is particularly useful for data transformation, validation, and processing workflows.
564+
565+
#### Features
566+
- Fluent interface for operation chaining
567+
- Built-in error handling
568+
- Input validation
569+
- Type-safe operations with PHP 8.2+ generics
570+
- Side effect management
571+
- Conditional processing
572+
- Operation composition
573+
574+
#### Basic Usage
575+
576+
```php
577+
use DesiredPatterns\Pipeline\Pipeline;
578+
579+
// Basic pipeline
580+
$result = Pipeline::of(5)
581+
->pipe(fn($x) => $x * 2) // 10
582+
->pipe(fn($x) => $x + 1) // 11
583+
->get(); // Returns: 11
584+
585+
// Pipeline with error handling
586+
$result = Pipeline::of($value)
587+
->try(
588+
fn($x) => processData($x),
589+
fn(\Throwable $e) => handleError($e)
590+
)
591+
->get();
592+
593+
// Pipeline with validation
594+
$result = Pipeline::of($data)
595+
->when(
596+
fn($x) => $x > 0,
597+
fn($x) => sqrt($x)
598+
)
599+
->get();
600+
```
601+
602+
#### Advanced Usage with PipelineBuilder
603+
604+
The PipelineBuilder provides a more structured way to create complex pipelines with validation and error handling:
605+
606+
```php
607+
use DesiredPatterns\Pipeline\PipelineBuilder;
608+
609+
$builder = new PipelineBuilder();
610+
$result = $builder
611+
->withValidation(fn($x) => $x > 0, 'Value must be positive')
612+
->withValidation(fn($x) => $x < 100, 'Value must be less than 100')
613+
->withErrorHandling(fn(\Throwable $e) => handleValidationError($e))
614+
->add(fn($x) => $x * 2)
615+
->add(fn($x) => "Result: $x")
616+
->build(50)
617+
->get();
618+
```
619+
620+
#### Real-World Example: Data Processing Pipeline
621+
622+
Here's a real-world example of using the Pipeline pattern for processing user data:
623+
624+
```php
625+
class UserDataProcessor
626+
{
627+
private PipelineBuilder $pipeline;
628+
629+
public function __construct()
630+
{
631+
$this->pipeline = new PipelineBuilder();
632+
$this->pipeline
633+
->withValidation(
634+
fn($data) => isset($data['email']),
635+
'Email is required'
636+
)
637+
->withValidation(
638+
fn($data) => filter_var($data['email'], FILTER_VALIDATE_EMAIL),
639+
'Invalid email format'
640+
)
641+
->withErrorHandling(fn(\Throwable $e) => [
642+
'success' => false,
643+
'error' => $e->getMessage()
644+
])
645+
->add(function($data) {
646+
// Normalize email
647+
$data['email'] = strtolower($data['email']);
648+
return $data;
649+
})
650+
->add(function($data) {
651+
// Hash password if present
652+
if (isset($data['password'])) {
653+
$data['password'] = password_hash(
654+
$data['password'],
655+
PASSWORD_DEFAULT
656+
);
657+
}
658+
return $data;
659+
})
660+
->add(function($data) {
661+
// Add metadata
662+
$data['created_at'] = new DateTime();
663+
$data['status'] = 'active';
664+
return $data;
665+
});
666+
}
667+
668+
public function process(array $userData): array
669+
{
670+
return $this->pipeline
671+
->build($userData)
672+
->get();
673+
}
674+
}
675+
676+
// Usage
677+
$processor = new UserDataProcessor();
678+
679+
// Successful case
680+
$result = $processor->process([
681+
'email' => '[email protected]',
682+
'password' => 'secret123'
683+
]);
684+
// Returns: [
685+
// 'email' => '[email protected]',
686+
// 'password' => '$2y$10$...',
687+
// 'created_at' => DateTime,
688+
// 'status' => 'active'
689+
// ]
690+
691+
// Error case
692+
$result = $processor->process([
693+
'email' => 'invalid-email'
694+
]);
695+
// Returns: [
696+
// 'success' => false,
697+
// 'error' => 'Invalid email format'
698+
// ]
699+
```
700+
701+
#### Benefits
702+
1. **Separation of Concerns**: Each operation in the pipeline has a single responsibility.
703+
2. **Maintainability**: Easy to add, remove, or modify processing steps without affecting other parts.
704+
3. **Reusability**: Pipeline operations can be reused across different contexts.
705+
4. **Error Handling**: Built-in error handling makes it easy to manage failures.
706+
5. **Validation**: Input validation can be added at any point in the pipeline.
707+
6. **Type Safety**: PHP 8.2+ generics provide type safety throughout the pipeline.
708+
7. **Testability**: Each operation can be tested in isolation.
709+
710+
#### Use Cases
711+
- Data transformation and normalization
712+
- Form validation and processing
713+
- API request/response handling
714+
- Image processing workflows
715+
- ETL (Extract, Transform, Load) operations
716+
- Document processing pipelines
717+
- Multi-step validation processes
718+
560719
## Testing
561720

562721
Run the test suite using PHPUnit :

src/Pipeline/Pipeline.php

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
namespace DesiredPatterns\Pipeline;
4+
5+
/**
6+
* Pipeline pattern implementation for processing data through a series of operations.
7+
*
8+
* @template T
9+
*/
10+
class Pipeline
11+
{
12+
/**
13+
* @var T
14+
*/
15+
private mixed $value;
16+
17+
/**
18+
* @param T $value
19+
*/
20+
private function __construct(mixed $value)
21+
{
22+
$this->value = $value;
23+
}
24+
25+
/**
26+
* Create a new pipeline with an initial value.
27+
*
28+
* @template U
29+
* @param U $value
30+
* @return self<U>
31+
*/
32+
public static function of(mixed $value): self
33+
{
34+
return new self($value);
35+
}
36+
37+
/**
38+
* Apply a transformation function to the current value.
39+
*
40+
* @template U
41+
* @param callable(T): U $fn
42+
* @return self<U>
43+
*/
44+
public function pipe(callable $fn): self
45+
{
46+
return new self($fn($this->value));
47+
}
48+
49+
/**
50+
* Get the final value from the pipeline.
51+
*
52+
* @return T
53+
*/
54+
public function get(): mixed
55+
{
56+
return $this->value;
57+
}
58+
59+
/**
60+
* Map multiple values through the pipeline.
61+
*
62+
* @template U
63+
* @param array<T> $values
64+
* @param callable(T): U $fn
65+
* @return array<U>
66+
*/
67+
public function map(array $values, callable $fn): array
68+
{
69+
return array_map($fn, $values);
70+
}
71+
72+
/**
73+
* Chain multiple operations in sequence.
74+
*
75+
* @param callable[] $operations
76+
* @return self
77+
*/
78+
public function through(array $operations): self
79+
{
80+
return array_reduce(
81+
$operations,
82+
fn($carry, $operation) => $carry->pipe($operation),
83+
$this
84+
);
85+
}
86+
87+
/**
88+
* Apply a side effect without modifying the value.
89+
*
90+
* @param callable(T): void $fn
91+
* @return self<T>
92+
*/
93+
public function tap(callable $fn): self
94+
{
95+
$fn($this->value);
96+
return $this;
97+
}
98+
99+
/**
100+
* Conditionally apply a transformation.
101+
*
102+
* @template U
103+
* @param callable(T): bool $predicate
104+
* @param callable(T): U $fn
105+
* @return self<T|U>
106+
*/
107+
public function when(callable $predicate, callable $fn): self
108+
{
109+
if ($predicate($this->value)) {
110+
return $this->pipe($fn);
111+
}
112+
return $this;
113+
}
114+
115+
/**
116+
* Handle errors in the pipeline.
117+
*
118+
* @template U
119+
* @param callable(T): U $fn
120+
* @param callable(\Throwable): U $errorHandler
121+
* @return self<U>
122+
*/
123+
public function try(callable $fn, callable $errorHandler): self
124+
{
125+
try {
126+
return $this->pipe($fn);
127+
} catch (\Throwable $e) {
128+
return new self($errorHandler($e));
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)