Skip to content

Commit 265e7a7

Browse files
authored
Merge pull request #2 from epic-64/add-try-box
Add try box
2 parents 9a36762 + 50fab6b commit 265e7a7

File tree

5 files changed

+241
-68
lines changed

5 files changed

+241
-68
lines changed

README.md

Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# Welcome to PhpBox
22

3-
PhpBox contains exactly one class: `Box`.
4-
5-
You can find it here: [src/Box.php](src/Box.php)
3+
PhpBox contains two classes:
4+
- `Box` [src/Box.php](src/Box.php)
5+
- `TryBox` [src/TryBox.php](src/TryBox.php)
66

77
# Installation
8-
Currently, there is no composer package.
9-
10-
Simply copy the `Box.php` file to your project and adjust its namespace if needed.
8+
A composer package is in the works, but I recommend just copying the files into your project for now.
119

1210
# Requirements
1311
The only hard requirement is PHP 8.0 or higher.
@@ -23,25 +21,20 @@ You put any value into a box.
2321

2422
Then you can chain map(), dump() and assert() calls on it.
2523

26-
To get the value out of the box, call unbox() or get().
24+
To get the value out of the box, call value() or get().
2725

28-
## Note
29-
In an earlier version:
30-
- map() was called pipe()
31-
- get() was called pull()
32-
33-
## Examples
26+
## Box Examples
3427

3528
```php
3629
$value = Box::of(5)
3730
->map(fn($value) => $value + 1)
3831
->map(fn($value) => $value * 2)
39-
->unbox();
32+
->value();
4033

4134
echo $value; // 12
4235
```
4336

44-
Use get() to combine map() and unbox() in one call:
37+
Use get() to combine map() and value() in one call:
4538
```php
4639
$value = Box::of(5)
4740
->map(fn($value) => $value + 1)
@@ -57,7 +50,7 @@ $isEven = fn($value) => $value % 2 === 0;
5750
$value = Box::of(5)
5851
->map(fn($it) => $it + 1)->assert(6)
5952
->map(fn($it) => $it * 2)->assert($isEven)->dump()
60-
->unbox();
53+
->value();
6154

6255
echo $value; // 12
6356
```
@@ -84,28 +77,85 @@ $user = Box::of($inputEmail)
8477
->get(fn($it) => $userRepository->create(['email' => $it]));
8578
```
8679

87-
Using flatMap, you can compose presets of operations on boxes:
80+
Using mod(), you can compose presets of operations:
8881
```php
8982
/** @throws LogicException */
9083
function assertEmail(Box $box): Box
9184
{
9285
return $box
93-
->assert(fn($x) => is_string($x), 'Not a string')
86+
->assert(fn(mixed $x) => is_string($x), 'Not a string')
9487
->assert(fn(string $x) => strlen($x) > 0, 'Too short')
9588
->assert(fn(string $x) => strlen($x) < 256, 'Too long')
9689
->assert(fn(string $x) => filter_var($x, FILTER_VALIDATE_EMAIL), 'Not an email');
9790
}
9891

99-
$validEmail = Box::of('')->flatMap(assertEmail(...))->unbox();
100-
// throws LogicException: "Value is too short"
92+
$validEmail = Box::of('')->flatMap(assertEmail(...))->value();
93+
// throws LogicException: "Too short"
10194

10295
$user = Box::of('[email protected]')
103-
->flatMap(fn($box) => assertEmail($box))
96+
->mod(fn($box) => assertEmail($box))
10497
->get(fn($email) => $userRepository->create(['email' => $email]));
10598
```
106-
Note, in this example we still have 4 separate assertions and error messages.
107-
Using flatMap is the key here. Unlike map(), which transforms the value inside the Box,
108-
flatMap() transforms the Box itself. This allows us to compose behavior in a more functional way.
99+
100+
## TryBox Examples
101+
102+
TryBox works just like Box, except that it catches any and all errors instead of blowing up directly.
103+
Its value() method has a more complicated return type, which is either T or Throwable.
104+
- advantage: you can run a chain without risk of blowing up (subsequent map() calls will be skipped),
105+
and then decide what to do with the error at the end.
106+
- disadvantage: you have to handle the error case even if you're sure it will never happen.
107+
108+
Let's define a realistic function that sometimes throws an error (like a database call or an API request). In this
109+
case it either returns whatever was passed in or throws a RuntimeException at random.
110+
```php
111+
/**
112+
* @template T
113+
* @param T $value
114+
* @return T
115+
*
116+
* @throws RuntimeException
117+
*/
118+
function roulette($value)
119+
{
120+
if (random_int(0, 1) === 1) {
121+
throw new RuntimeException('boo');
122+
}
123+
124+
return $value;
125+
}
126+
```
127+
128+
Let's use this dangerous function in a TryBox chain:
129+
```php
130+
$result = TryBox::of(5)
131+
->map(fn($value) => roulette($value)) // TryBox stores the exception
132+
->map(fn($value) => $value * 2)
133+
->map(fn($value) => $value + 1)
134+
->value();
135+
136+
// type signature is int|Throwable, so we have to handle the error case
137+
if ($result instanceof Throwable) {
138+
echo $result->getMessage(); // boo
139+
return;
140+
}
141+
142+
// if we reach this line, $result is guaranteed to be an int
143+
$calc = $result + 1;
144+
echo $calc;
145+
```
146+
147+
If you are sure the error case will never happen, or simply don't care, you can use rip() instead of value():
148+
`rip()` will either throw the stored error (if any) or return the value of type T.
149+
```php
150+
$result = TryBox::of(5)
151+
->map(fn($value) => roulette($value))
152+
->map(fn($value) => $value * 2)
153+
->map(fn($value) => $value + 1)
154+
->rip(); // The original runtime exception will be thrown here, if present
155+
156+
// if we reach this line, $result is guaranteed to be an int
157+
$calc = $result + 1;
158+
```
109159

110160
# Type Safety
111161
Thanks to meticulously crafted PHPDoc annotations, this class is type safe if you use PHPStan for static analysis.
@@ -137,8 +187,6 @@ The `@template T` annotation tells PHPStan that the class is generic and that th
137187
In the constructor, we use the $value parameter with the type T,
138188
and the type of the Box is automatically reverse engineered from the input.
139189

140-
141-
142190
Examples:
143191
- `Box::of(5)` will be of type `Box<int>`
144192
- `Box::of('hello')` will be of type `Box<string>`
@@ -168,10 +216,8 @@ the type of U, and the resulting `Box<U>` is inferred from the return type of th
168216

169217
This mechanism is incredibly powerful at preventing you from writing bad code. And again, a totally normal feature
170218
in other languages like Java, Scala, Kotlin, Haskell, Rust, F#, C#, Go, Swift, TypeScript.
171-
Please pick one of these and learn it.
172-
173219
```php
174-
$value = Box::of('Hello World') // string
220+
$value = Box::of('Hello World') // string
175221
->map(fn($value) => strtoupper($value)) // string
176222
->map(fn($value) => str_replace('WORLD', 'PHP', $value)) // string
177223
->map(fn($value) => str_split($value)) // array<string>

src/Box.php

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* A container that allows chaining transformations and assertions on a value.
1111
*
12-
* @template T
12+
* @template T of mixed
1313
*/
1414
class Box
1515
{
@@ -46,15 +46,15 @@ public function map(callable $callback): Box
4646
}
4747

4848
/**
49-
* Apply a transformation function to the box itself
49+
* Modify the box itself via a callback.
5050
*
51-
* This method will always return a new instance of Box, even for objects.
51+
* Useful grouping multiple calls on a box into one call (e.g. for common validation rules).
5252
*
5353
* @template U
5454
* @param callable(self<T>): Box<U> $callback
5555
* @return Box<U>
5656
*/
57-
public function flatMap(callable $callback): Box
57+
public function mod(callable $callback): Box
5858
{
5959
return $callback($this);
6060
}
@@ -69,15 +69,15 @@ public function flatMap(callable $callback): Box
6969
*/
7070
public function get(callable $callback)
7171
{
72-
return $callback($this->unbox());
72+
return $callback($this->value());
7373
}
7474

7575
/**
7676
* Unwrap the final value.
7777
*
7878
* @return T
7979
*/
80-
public function unbox()
80+
public function value()
8181
{
8282
return $this->value;
8383
}
@@ -118,20 +118,6 @@ public function assert(mixed $check, string $message = ''): Box
118118
return $this;
119119
}
120120

121-
/**
122-
* Run an assertion against the value and return it.
123-
*
124-
* @template U
125-
* @param U|callable(T):bool $check
126-
* @return T
127-
*/
128-
public function assertGet(mixed $check): mixed
129-
{
130-
$this->assert($check);
131-
132-
return $this->unbox();
133-
}
134-
135121
/**
136122
* Dump the value to the console.
137123
*

src/TryBox.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace Epic64\PhpBox;
4+
5+
use LogicException;
6+
use Throwable;
7+
8+
/**
9+
* A container that allows chaining transformations and assertions on a value.
10+
* value() will return T|Throwable (must be narrowed manually with error handling).
11+
* rip() will return T, but throws an exception if there is an error.
12+
*
13+
* @template T
14+
* @template E
15+
*/
16+
class TryBox
17+
{
18+
/**
19+
* @template U
20+
* @param U $value
21+
* @return TryBox<U, null>
22+
*/
23+
public static function of($value): TryBox
24+
{
25+
/** @var TryBox<U, null> $box */
26+
$box = new self($value, null);
27+
28+
return $box;
29+
}
30+
31+
/**
32+
* @param T $value
33+
*/
34+
public function __construct(
35+
private $value,
36+
private ?Throwable $error = null
37+
) {
38+
}
39+
40+
/**
41+
* Apply a transformation function to the value.
42+
*
43+
* @template U
44+
* @param callable(T): U $callback
45+
* @return TryBox<U, null>|TryBox<T, Throwable>
46+
*/
47+
public function map(callable $callback): TryBox
48+
{
49+
if ($this->error !== null) {
50+
return $this;
51+
}
52+
53+
return $this->try($callback);
54+
}
55+
56+
/**
57+
* Unbox the value, which might be anything including a throwable.
58+
*
59+
* @return Throwable|T
60+
*/
61+
public function value()
62+
{
63+
return $this->error ?? $this->value;
64+
}
65+
66+
/**
67+
* @return T
68+
*
69+
* @throws Throwable
70+
*/
71+
public function rip()
72+
{
73+
if ($this->error !== null) {
74+
throw $this->error;
75+
}
76+
77+
return $this->value;
78+
}
79+
80+
/**
81+
* @template U
82+
* @param callable(T):U $callback
83+
* @return TryBox<U, null>|TryBox<T, Throwable>
84+
*/
85+
private function try(callable $callback): TryBox
86+
{
87+
try {
88+
/** @var TryBox<U, null> $result */
89+
$result = new self($callback($this->value), null);
90+
} catch (Throwable $e) {
91+
/** @var TryBox<T, Throwable> $result */
92+
$result = new self($this->value, $e);
93+
}
94+
95+
return $result;
96+
}
97+
}

0 commit comments

Comments
 (0)