Skip to content
Draft
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [ "8.3", "8.4" ]
php: [ "8.3", "8.4", "8.5" ]
dependencies: [ "lowest", "highest" ]

steps:
Expand All @@ -26,3 +26,20 @@ jobs:
- run: vendor/bin/phpstan

- run: vendor/bin/pest

# Test actual pipe operator syntax on PHP 8.5
pipe-operator-examples:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || contains(github.ref, 'pipe-operator')

steps:
- uses: actions/checkout@v3

- uses: shivammathur/setup-php@v2
with:
php-version: "8.5"

- uses: ramsey/composer-install@v3

- name: Run pipe operator examples
run: php examples/pipe-operator.php
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Can you put these in actual phpunit tests that conditionally run on 8.5?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I've converted the examples into proper PHPUnit tests that conditionally run on PHP 8.5.

Changes in commit 762df88:

  • Created tests/Option/Php85PipeOperatorTest.php85 - 10 tests with real |> syntax
  • Created tests/Result/Php85PipeOperatorTest.php85 - 12 tests with real |> syntax
  • Added tests/php85-bootstrap.php for conditional loading based on PHP version
  • Created tests/Pest.php configuration to integrate the bootstrap

The tests use .php85 extension to prevent parse errors on PHP < 8.5. They're automatically loaded and run when PHP 8.5 is detected, and safely skipped on earlier versions. They'll run as part of the regular test suite in CI when the PHP 8.5 matrix builds execute.

279 changes: 279 additions & 0 deletions PIPE_OPERATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
# PHP 8.5 Pipe Operator Support

This document demonstrates how to use PHP 8.5's new pipe operator (`|>`) with the monads library.

## What is the Pipe Operator?

The pipe operator (`|>`) allows you to chain function calls in a more readable, left-to-right manner. Instead of deeply nested function calls or intermediate variables, you can write:

```php
$result = $value
|> trim(...)
|> strtoupper(...)
|> fn($x) => str_replace(' ', '-', $x);
```

This is equivalent to:
```php
$result = str_replace(' ', '-', strtoupper(trim($value)));
```

## Using the Pipe Operator with Monads

### Option Monad with Pipe Operator

The library provides pipe-friendly helper functions in the `Superscript\Monads\Option\Pipe` namespace:

```php
use function Superscript\Monads\Option\Pipe\{option, map, filter, andThen, unwrapOr};

// Process user input with validation
$username = $rawInput
|> option(...) // Wrap in Option
|> map(fn($x) => trim($x)) // Trim whitespace
|> filter(fn($x) => strlen($x) > 0) // Filter empty strings
|> map(fn($x) => strtolower($x)) // Convert to lowercase
|> unwrapOr('guest'); // Provide default

// Safe array access
$users = [
1 => ['name' => 'Alice'],
2 => ['name' => 'Bob'],
];

$userName = $userId
|> fn($id) => $users[$id] ?? null
|> option(...)
|> map(fn($u) => $u['name'])
|> unwrapOr('Unknown');

// Validation with flatMap
$validateAge = fn(?int $age) =>
$age !== null && $age >= 18 && $age <= 100
? Some($age)
: None();

$result = $userAge
|> $validateAge(...)
|> map(fn($a) => "Age: $a years")
|> unwrapOr('Invalid age');
```

### Result Monad with Pipe Operator

The library provides pipe-friendly helper functions in the `Superscript\Monads\Result\Pipe` namespace:

```php
use function Superscript\Monads\Result\Pipe\{toOk, map, andThen, mapErr, unwrapOr, matchResult};

// Validate and process numbers
$parseInt = fn(string $s): int => (int) $s;
$validate = fn(int $x) => $x > 0 ? Ok($x) : Err("Must be positive");
$double = fn(int $x): int => $x * 2;

$result = $input
|> toOk(...)
|> map($parseInt)
|> andThen($validate)
|> map($double)
|> unwrapOr(0);

// Safe division chain
$divide = fn(int $a, int $b) =>
$b === 0 ? Err("Division by zero") : Ok($a / $b);

$result = 100
|> fn($x) => $divide($x, 2)
|> andThen(fn($x) => $divide((int)$x, 5))
|> unwrapOr(0);

// Error handling with match
$result = $value
|> toOk(...)
|> map(fn($x) => $x * 2)
|> matchResult(
fn($e) => "Error: $e",
fn($v) => "Success: $v"
);

// Parse JSON with validation
$parseJson = function (string $json) {
try {
return Ok(json_decode($json, true, 512, JSON_THROW_ON_ERROR));
} catch (Exception $e) {
return Err("Invalid JSON");
}
};

$validateUser = fn(array $data) =>
isset($data['name']) && isset($data['email'])
? Ok($data)
: Err("Missing required fields");

$extractName = fn(array $user): string => $user['name'];

$userName = $jsonString
|> $parseJson(...)
|> andThen($validateUser)
|> map($extractName)
|> unwrapOr('Unknown');
```

## Method Chaining (Works in PHP 8.3+)

The monads already work great with the pipe operator because they support method chaining:

```php
// Option chaining with pipe
$result = $value
|> Some(...)
|> fn($opt) => $opt->map(fn($x) => $x * 2)
|> fn($opt) => $opt->filter(fn($x) => $x > 10)
|> fn($opt) => $opt->unwrapOr(0);

// Or using the traditional method chaining (PHP 8.3+)
$result = Some($value)
->map(fn($x) => $x * 2)
->filter(fn($x) => $x > 10)
->unwrapOr(0);
```

## Real-World Examples

### Example 1: Processing Form Input

```php
use function Superscript\Monads\Option\Pipe\{option, map, filter, unwrapOr};

// Clean and validate email
$cleanEmail = $formData['email'] ?? null
|> option(...)
|> map(fn($e) => trim($e))
|> filter(fn($e) => filter_var($e, FILTER_VALIDATE_EMAIL))
|> map(fn($e) => strtolower($e))
|> unwrapOr(null);

if ($cleanEmail === null) {
throw new ValidationException('Invalid email');
}
```

### Example 2: Database Query with Error Handling

```php
use function Superscript\Monads\Result\Pipe\{toOk, andThen, map, unwrapOr};

$findUser = fn(int $id) =>
DB::find('users', $id)
? Ok(DB::find('users', $id))
: Err("User not found");

$validateUser = fn($user) =>
$user['active']
? Ok($user)
: Err("User is inactive");

$formatUser = fn($user) => [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
];

$userData = $userId
|> $findUser(...)
|> andThen($validateUser)
|> map($formatUser)
|> unwrapOr(null);
```

### Example 3: Configuration Loading

```php
use function Superscript\Monads\Result\Pipe\{toOk, map, andThen, matchResult};

$loadConfig = function(string $path) {
if (!file_exists($path)) {
return Err("Config file not found");
}

try {
$content = file_get_contents($path);
$config = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
return Ok($config);
} catch (Exception $e) {
return Err("Invalid config format");
}
};

$validateConfig = fn(array $config) =>
isset($config['app_name']) && isset($config['version'])
? Ok($config)
: Err("Missing required config keys");

$config = $configPath
|> $loadConfig(...)
|> andThen($validateConfig)
|> matchResult(
fn($err) => throw new ConfigException($err),
fn($cfg) => $cfg
);
```

## Helper Functions Reference

### Option Pipe Helpers

- `option($value)` - Wrap value in Option (Some if non-null, None if null)
- `map($fn)` - Transform the contained value
- `filter($predicate)` - Filter based on predicate
- `andThen($fn)` - FlatMap operation
- `unwrapOr($default)` - Extract value or return default
- `isSomeAnd($predicate)` - Check if Some and satisfies predicate

### Result Pipe Helpers

- `toOk($value)` - Wrap value in Ok
- `toErr($error)` - Wrap error in Err
- `map($fn)` - Transform the success value
- `mapErr($fn)` - Transform the error value
- `andThen($fn)` - FlatMap operation
- `unwrapOr($default)` - Extract value or return default
- `matchResult($errFn, $okFn)` - Handle both cases
- `matchResult($result)($errFn, $okFn)` - Handle both cases

## Why Pipe-Friendly Helpers?

While the monads already have great method chaining APIs, the pipe-friendly helpers make it easier to compose operations in a functional style:

1. **Clearer data flow** - Read operations left-to-right
2. **Better composition** - Easier to build reusable pipelines
3. **Functional style** - Matches the pipe operator's functional paradigm
4. **Point-free style** - Can pass functions directly without wrapping

## Migration Guide

If you're already using the library, you don't need to change anything! The traditional method chaining still works:

```php
// Traditional (still works great)
$result = Some($value)
->map(fn($x) => $x * 2)
->filter(fn($x) => $x > 10)
->unwrapOr(0);

// With pipe operator (PHP 8.5+)
$result = $value
|> Some(...)
|> fn($opt) => $opt->map(fn($x) => $x * 2)
|> fn($opt) => $opt->filter(fn($x) => $x > 10)
|> fn($opt) => $opt->unwrapOr(0);

// With pipe helpers (most functional)
$result = $value
|> option(...)
|> map(fn($x) => $x * 2)
|> filter(fn($x) => $x > 10)
|> unwrapOr(0);
```

Choose the style that best fits your codebase and preferences!
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,28 @@ composer require gosuperscript/monads
## Requirements

- PHP 8.3 or higher
- PHP 8.5+ for pipe operator support (optional)

## Usage

### 🚀 PHP 8.5 Pipe Operator Support

This library fully supports PHP 8.5's new pipe operator (`|>`), enabling clean, functional-style data transformations:

```php
use function Superscript\Monads\Option\Pipe\{option, map, filter, unwrapOr};

// Clean and readable data pipeline
$username = $userInput
|> option(...) // Wrap in Option
|> map(fn($x) => trim($x)) // Trim whitespace
|> filter(fn($x) => strlen($x) > 0) // Filter empty
|> map(fn($x) => strtolower($x)) // Lowercase
|> unwrapOr('guest'); // Default value
```

See [PIPE_OPERATOR.md](PIPE_OPERATOR.md) for comprehensive examples and migration guide.

### Option Monad

The `Option` type represents an optional value: every `Option` is either `Some` and contains a value, or `None`, and does not. This is a safer alternative to using `null`.
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
},
"files": [
"src/Result/functions.php",
"src/Option/functions.php"
"src/Option/functions.php",
"src/Result/pipe.php",
"src/Option/pipe.php"
]
},
"autoload-dev": {
Expand All @@ -17,7 +19,7 @@
}
},
"require": {
"php": "^8.3"
"php": "^8.3 || ^8.4 || ^8.5"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
Expand Down
Loading