Skip to content

Commit

Permalink
Pipeline Pattern added.
Browse files Browse the repository at this point in the history
  • Loading branch information
cmatosbc committed Dec 25, 2024
1 parent c4f9cc8 commit bae6db9
Show file tree
Hide file tree
Showing 6 changed files with 733 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .phpcs-cache

Large diffs are not rendered by default.

159 changes: 159 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Quick Links:
- [7. Specification Pattern](#7-specification-pattern)
- [8. Strategy Pattern](#8-strategy-pattern)
- [9. State Pattern](#9-state-pattern)
- [10. Pipeline Pattern](#10-pipeline-pattern)

### 1. Singleton Pattern
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.
Expand Down Expand Up @@ -557,6 +558,164 @@ The State pattern is perfect for managing complex workflows like order processin
- Payment Processing
- Task Management Systems

### 10. Pipeline Pattern

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.

#### Features
- Fluent interface for operation chaining
- Built-in error handling
- Input validation
- Type-safe operations with PHP 8.2+ generics
- Side effect management
- Conditional processing
- Operation composition

#### Basic Usage

```php
use DesiredPatterns\Pipeline\Pipeline;

// Basic pipeline
$result = Pipeline::of(5)
->pipe(fn($x) => $x * 2) // 10
->pipe(fn($x) => $x + 1) // 11
->get(); // Returns: 11

// Pipeline with error handling
$result = Pipeline::of($value)
->try(
fn($x) => processData($x),
fn(\Throwable $e) => handleError($e)
)
->get();

// Pipeline with validation
$result = Pipeline::of($data)
->when(
fn($x) => $x > 0,
fn($x) => sqrt($x)
)
->get();
```

#### Advanced Usage with PipelineBuilder

The PipelineBuilder provides a more structured way to create complex pipelines with validation and error handling:

```php
use DesiredPatterns\Pipeline\PipelineBuilder;

$builder = new PipelineBuilder();
$result = $builder
->withValidation(fn($x) => $x > 0, 'Value must be positive')
->withValidation(fn($x) => $x < 100, 'Value must be less than 100')
->withErrorHandling(fn(\Throwable $e) => handleValidationError($e))
->add(fn($x) => $x * 2)
->add(fn($x) => "Result: $x")
->build(50)
->get();
```

#### Real-World Example: Data Processing Pipeline

Here's a real-world example of using the Pipeline pattern for processing user data:

```php
class UserDataProcessor
{
private PipelineBuilder $pipeline;

public function __construct()
{
$this->pipeline = new PipelineBuilder();
$this->pipeline
->withValidation(
fn($data) => isset($data['email']),
'Email is required'
)
->withValidation(
fn($data) => filter_var($data['email'], FILTER_VALIDATE_EMAIL),
'Invalid email format'
)
->withErrorHandling(fn(\Throwable $e) => [
'success' => false,
'error' => $e->getMessage()
])
->add(function($data) {
// Normalize email
$data['email'] = strtolower($data['email']);
return $data;
})
->add(function($data) {
// Hash password if present
if (isset($data['password'])) {
$data['password'] = password_hash(
$data['password'],
PASSWORD_DEFAULT
);
}
return $data;
})
->add(function($data) {
// Add metadata
$data['created_at'] = new DateTime();
$data['status'] = 'active';
return $data;
});
}

public function process(array $userData): array
{
return $this->pipeline
->build($userData)
->get();
}
}

// Usage
$processor = new UserDataProcessor();

// Successful case
$result = $processor->process([
'email' => '[email protected]',
'password' => 'secret123'
]);
// Returns: [
// 'email' => '[email protected]',
// 'password' => '$2y$10$...',
// 'created_at' => DateTime,
// 'status' => 'active'
// ]

// Error case
$result = $processor->process([
'email' => 'invalid-email'
]);
// Returns: [
// 'success' => false,
// 'error' => 'Invalid email format'
// ]
```

#### Benefits
1. **Separation of Concerns**: Each operation in the pipeline has a single responsibility.
2. **Maintainability**: Easy to add, remove, or modify processing steps without affecting other parts.
3. **Reusability**: Pipeline operations can be reused across different contexts.
4. **Error Handling**: Built-in error handling makes it easy to manage failures.
5. **Validation**: Input validation can be added at any point in the pipeline.
6. **Type Safety**: PHP 8.2+ generics provide type safety throughout the pipeline.
7. **Testability**: Each operation can be tested in isolation.

#### Use Cases
- Data transformation and normalization
- Form validation and processing
- API request/response handling
- Image processing workflows
- ETL (Extract, Transform, Load) operations
- Document processing pipelines
- Multi-step validation processes

## Testing

Run the test suite using PHPUnit :
Expand Down
131 changes: 131 additions & 0 deletions src/Pipeline/Pipeline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

namespace DesiredPatterns\Pipeline;

/**
* Pipeline pattern implementation for processing data through a series of operations.
*
* @template T
*/
class Pipeline
{
/**
* @var T
*/
private mixed $value;

/**
* @param T $value
*/
private function __construct(mixed $value)
{
$this->value = $value;
}

/**
* Create a new pipeline with an initial value.
*
* @template U
* @param U $value
* @return self<U>
*/
public static function of(mixed $value): self
{
return new self($value);
}

/**
* Apply a transformation function to the current value.
*
* @template U
* @param callable(T): U $fn
* @return self<U>
*/
public function pipe(callable $fn): self
{
return new self($fn($this->value));
}

/**
* Get the final value from the pipeline.
*
* @return T
*/
public function get(): mixed
{
return $this->value;
}

/**
* Map multiple values through the pipeline.
*
* @template U
* @param array<T> $values
* @param callable(T): U $fn
* @return array<U>
*/
public function map(array $values, callable $fn): array
{
return array_map($fn, $values);
}

/**
* Chain multiple operations in sequence.
*
* @param callable[] $operations
* @return self
*/
public function through(array $operations): self
{
return array_reduce(
$operations,
fn($carry, $operation) => $carry->pipe($operation),
$this
);
}

/**
* Apply a side effect without modifying the value.
*
* @param callable(T): void $fn
* @return self<T>
*/
public function tap(callable $fn): self
{
$fn($this->value);
return $this;
}

/**
* Conditionally apply a transformation.
*
* @template U
* @param callable(T): bool $predicate
* @param callable(T): U $fn
* @return self<T|U>
*/
public function when(callable $predicate, callable $fn): self
{
if ($predicate($this->value)) {
return $this->pipe($fn);
}
return $this;
}

/**
* Handle errors in the pipeline.
*
* @template U
* @param callable(T): U $fn
* @param callable(\Throwable): U $errorHandler
* @return self<U>
*/
public function try(callable $fn, callable $errorHandler): self
{
try {
return $this->pipe($fn);
} catch (\Throwable $e) {
return new self($errorHandler($e));
}
}
}
Loading

0 comments on commit bae6db9

Please sign in to comment.