Rust-style Option and Result Classes for PHP

chore(src, tests): lint

Ciaran b68b0a6b 69935c3b

+325 -278
+58 -43
src/Option.php
··· 7 7 /** 8 8 * Option<T> represents an optional value. 9 9 * An option may be `some` or `none`, where `some` contains a value and `none` does not. 10 + * 10 11 * @template T 11 - */ 12 - 13 - class Option { 14 - 12 + */ 13 + class Option 14 + { 15 15 /** 16 - * Creates a `some` Option 17 - * @template T 18 - * @param T $value 19 - * @return Option<T> 20 - */ 21 - 22 - public static function Some(mixed $value = true): static { 16 + * Creates a `some` Option 17 + * 18 + * @template T 19 + * 20 + * @param T $value 21 + * @return Option<T> 22 + */ 23 + public static function Some(mixed $value = true): static 24 + { 23 25 return new static($value, true); 24 26 } 25 27 26 28 /** 27 - * Creates a `none` Option 28 - * @return Option<never> 29 - */ 30 - 31 - public static function None(): static { 29 + * Creates a `none` Option 30 + * 31 + * @return Option<never> 32 + */ 33 + public static function None(): static 34 + { 32 35 return new static(null, false); 33 36 } 34 37 35 38 /** @param T $value */ 36 - 37 39 private function __construct( 38 - private mixed $value, 40 + private mixed $value, 39 41 private bool $isSome 40 42 ) {} 41 43 42 44 /** Returns `true` if the option is a `some` option. */ 43 - 44 - public function isSome(): bool { 45 + public function isSome(): bool 46 + { 45 47 return $this->isSome; 46 48 } 47 49 48 50 /** Returns `true` if the option is a `none` option. */ 49 - 50 - public function isNone(): bool { 51 - return !$this->isSome(); 51 + public function isNone(): bool 52 + { 53 + return ! $this->isSome(); 52 54 } 53 55 54 56 /** 55 - * Returns the contained value if `some`, otherwise throws UnwrapNoneException. 56 - * @throws UnwrapNoneException When called on `None` 57 - * @return T The contained value 58 - */ 57 + * Returns the contained value if `some`, otherwise throws UnwrapNoneException. 58 + * 59 + * @return T The contained value 60 + * 61 + * @throws UnwrapNoneException When called on `None` 62 + */ 63 + public function unwrap(): mixed 64 + { 65 + if ($this->isNone()) { 66 + throw new UnwrapNoneException; 67 + } 59 68 60 - public function unwrap(): mixed { 61 - if ($this->isNone()) throw new UnwrapNoneException; 62 69 return $this->value; 63 70 } 64 71 65 72 /** 66 - * Returns the contained `some` value or a provided default. 67 - * @param V $or 68 - * @return T|V 69 - */ 73 + * Returns the contained `some` value or a provided default. 74 + * 75 + * @param V $or 76 + * @return T|V 77 + */ 78 + public function unwrapOr(mixed $or): mixed 79 + { 80 + if ($this->isSome()) { 81 + return $this->unwrap(); 82 + } 70 83 71 - public function unwrapOr(mixed $or): mixed { 72 - if ($this->isSome()) return $this->unwrap(); 73 84 return $or; 74 85 } 75 86 76 87 /** 77 - * Calls `fn` on contained value if `some`, returns `none` if `none` 78 - * @template U 79 - * @param callable(T): U $fn Function to transform the value 80 - * @return Option<U> 81 - */ 88 + * Calls `fn` on contained value if `some`, returns `none` if `none` 89 + * 90 + * @template U 91 + * 92 + * @param callable(T): U $fn Function to transform the value 93 + * @return Option<U> 94 + */ 95 + public function map(callable $fn): Option 96 + { 97 + if ($this->isNone()) { 98 + return Option::None(); 99 + } 82 100 83 - public function map(callable $fn): Option { 84 - if ($this->isNone()) return Option::None(); 85 101 return Option::Some($fn($this->value)); 86 102 } 87 - 88 103 }
+103 -72
src/Result.php
··· 6 6 use Ciarancoza\OptionResult\Exceptions\UnwrapOkException; 7 7 8 8 /** 9 - * Result<T, E> represents a success (`ok`) or an error (`err`) 10 - * @template T 11 - * @template E 12 - */ 13 - 14 - class Result { 15 - 16 - /** 17 - * Creates an `ok` result 18 - * @param T $value 19 - * @return Result<T,never> 20 - */ 21 - 22 - public static function Ok(mixed $value = true): static { 9 + * Result<T, E> represents a success (`ok`) or an error (`err`) 10 + * 11 + * @template T 12 + * @template E 13 + */ 14 + class Result 15 + { 16 + /** 17 + * Creates an `ok` result 18 + * 19 + * @param T $value 20 + * @return Result<T,never> 21 + */ 22 + public static function Ok(mixed $value = true): static 23 + { 23 24 return new static($value, true); 24 25 } 25 26 26 - /** 27 - * Creates an `err` result 28 - * @param E $value 29 - * @return Result<never,E> 30 - */ 31 - 32 - public static function Err(mixed $value): static { 27 + /** 28 + * Creates an `err` result 29 + * 30 + * @param E $value 31 + * @return Result<never,E> 32 + */ 33 + public static function Err(mixed $value): static 34 + { 33 35 return new static($value, false); 34 36 } 35 37 36 - /** @param T $value */ 37 - 38 + /** @param T $value */ 38 39 private function __construct( 39 40 protected mixed $value, 40 41 protected bool $isOk, 41 42 ) {} 42 43 43 44 /** Returns `true` if the result is an `ok` result. */ 44 - 45 - public function isOk(): bool { 45 + public function isOk(): bool 46 + { 46 47 return $this->isOk; 47 48 } 48 49 49 50 /** Returns `true` if the result is an `err` result. */ 50 - 51 - public function isErr(): bool { 52 - return !$this->isOk(); 51 + public function isErr(): bool 52 + { 53 + return ! $this->isOk(); 53 54 } 54 55 55 56 /** 56 - * Returns `Some(T)` if `ok`, or `None` if `err` 57 - * @return Option<T> 58 - */ 57 + * Returns `Some(T)` if `ok`, or `None` if `err` 58 + * 59 + * @return Option<T> 60 + */ 61 + public function getOk(): Option 62 + { 63 + if ($this->isErr()) { 64 + return Option::None(); 65 + } 59 66 60 - public function getOk(): Option { 61 - if ($this->isErr()) return Option::None(); 62 67 return Option::Some($this->value); 63 68 } 64 69 65 70 /** 66 - * Returns `Some(E)` if `err`, or `None` if `ok` 67 - * @return Option<E> 68 - */ 71 + * Returns `Some(E)` if `err`, or `None` if `ok` 72 + * 73 + * @return Option<E> 74 + */ 75 + public function getErr(): Option 76 + { 77 + if ($this->isOk()) { 78 + return Option::None(); 79 + } 69 80 70 - public function getErr(): Option { 71 - if ($this->isOk()) return Option::None(); 72 81 return Option::Some($this->value); 73 82 } 74 83 75 84 /** 76 - * Returns the contained value if `ok`, otherwise throws UnwrapErrException 77 - * @throws UnwrapErrException 78 - * @return T The contained value 79 - */ 85 + * Returns the contained value if `ok`, otherwise throws UnwrapErrException 86 + * 87 + * @return T The contained value 88 + * 89 + * @throws UnwrapErrException 90 + */ 91 + public function unwrap(): mixed 92 + { 93 + if ($this->isErr()) { 94 + throw new UnwrapErrException; 95 + } 80 96 81 - public function unwrap(): mixed { 82 - if ($this->isErr()) throw new UnwrapErrException; 83 97 return $this->value; 84 98 } 85 99 86 100 /** 87 - * Returns the contained value if `err`, otherwise throws UnwrapOkException 88 - * @throws UnwrapOkException 89 - * @return E The contained error value 90 - */ 101 + * Returns the contained value if `err`, otherwise throws UnwrapOkException 102 + * 103 + * @return E The contained error value 104 + * 105 + * @throws UnwrapOkException 106 + */ 107 + public function unwrapErr(): mixed 108 + { 109 + if ($this->isOk()) { 110 + throw new UnwrapOkException; 111 + } 91 112 92 - public function unwrapErr(): mixed { 93 - if ($this->isOk()) throw new UnwrapOkException; 94 113 return $this->value; 95 114 } 96 115 97 116 /** 98 - * Returns the contained `ok` value or a provided default. 99 - * @param V $or 100 - * @return T|V 101 - */ 117 + * Returns the contained `ok` value or a provided default. 118 + * 119 + * @param V $or 120 + * @return T|V 121 + */ 122 + public function unwrapOr(mixed $or): mixed 123 + { 124 + if ($this->isOk()) { 125 + return $this->unwrap(); 126 + } 102 127 103 - public function unwrapOr(mixed $or): mixed { 104 - if ($this->isOk()) return $this->unwrap(); 105 128 return $or; 106 129 } 107 130 108 131 /** 109 - * If `ok`, transform the value with `$fn` 110 - * @template U 111 - * @param callable(T): U $fn Function to transform the value 112 - * @return Result<U,E> 113 - */ 132 + * If `ok`, transform the value with `$fn` 133 + * 134 + * @template U 135 + * 136 + * @param callable(T): U $fn Function to transform the value 137 + * @return Result<U,E> 138 + */ 139 + public function map(callable $fn): Result 140 + { 141 + if ($this->isErr()) { 142 + return Result::Err($this->value); 143 + } 114 144 115 - public function map(callable $fn): Result { 116 - if ($this->isErr()) return Result::Err($this->value); 117 145 return Result::Ok($fn($this->value)); 118 146 } 119 147 120 - 121 148 /** 122 - * If `err`, transform the error value with `$fn` 123 - * @template U 124 - * @param callable(E): U $fn Function to transform the value 125 - * @return Result<T,U> 126 - */ 149 + * If `err`, transform the error value with `$fn` 150 + * 151 + * @template U 152 + * 153 + * @param callable(E): U $fn Function to transform the value 154 + * @return Result<T,U> 155 + */ 156 + public function mapErr(callable $fn): Result 157 + { 158 + if ($this->isOk()) { 159 + return Result::Ok($this->value); 160 + } 127 161 128 - public function mapErr(callable $fn): Result { 129 - if ($this->isOk()) return Result::Ok($this->value); 130 162 return Result::Err($fn($this->value)); 131 163 } 132 - 133 164 }
+67 -69
tests/OptionTest.php
··· 1 1 <?php 2 2 3 + use Ciarancoza\OptionResult\Exceptions\UnwrapNoneException; 4 + use Ciarancoza\OptionResult\Option; 3 5 use PHPUnit\Framework\TestCase; 4 - use Ciarancoza\OptionResult\Option; 5 - use Ciarancoza\OptionResult\Exceptions\UnwrapNoneException; 6 6 7 7 class OptionTest extends TestCase 8 8 { 9 - 10 9 // Unit tests - Adapted from Rust Docs 11 10 12 - public function testIsNone(): void 11 + public function test_is_none(): void 13 12 { 14 13 $x = Option::Some(2); 15 14 $this->assertSame(false, $x->isNone()); ··· 18 17 $this->assertSame(true, $x->isNone()); 19 18 } 20 19 21 - public function testIsSome(): void 20 + public function test_is_some(): void 22 21 { 23 22 $x = Option::Some(2); 24 23 $this->assertSame(true, $x->isSome()); ··· 27 26 $this->assertSame(false, $x->isSome()); 28 27 } 29 28 30 - public function testUnwrap(): void 29 + public function test_unwrap(): void 31 30 { 32 - $x = Option::Some("air"); 33 - $this->assertSame("air", $x->unwrap()); 31 + $x = Option::Some('air'); 32 + $this->assertSame('air', $x->unwrap()); 34 33 35 34 $this->expectException(UnwrapNoneException::class); 36 35 Option::None()->unwrap(); 37 36 } 38 37 39 - public function testUnwrapOr(): void 38 + public function test_unwrap_or(): void 40 39 { 41 - $this->assertSame("car", Option::Some("car")->unwrapOr("bike")); 42 - $this->assertSame("bike", Option::None()->unwrapOr("bike")); 40 + $this->assertSame('car', Option::Some('car')->unwrapOr('bike')); 41 + $this->assertSame('bike', Option::None()->unwrapOr('bike')); 43 42 } 44 43 45 - public function testUnwrapOrElse(): void 44 + public function test_unwrap_or_else(): void 46 45 { 47 - $this->markTestIncomplete("TODO"); 46 + $this->markTestIncomplete('TODO'); 48 47 $k = 21; 49 - $this->assertSame(4, Option::Some(4)->unwrapOrElse(fn() => 2 * $k)); 50 - $this->assertSame(42, Option::None()->unwrapOrElse(fn() => 2 * $k)); 48 + $this->assertSame(4, Option::Some(4)->unwrapOrElse(fn () => 2 * $k)); 49 + $this->assertSame(42, Option::None()->unwrapOrElse(fn () => 2 * $k)); 51 50 } 52 51 53 - public function testMap(): void 52 + public function test_map(): void 54 53 { 55 - $result = Option::Some("Hello World")->map(fn($s) => strlen($s)); 54 + $result = Option::Some('Hello World')->map(fn ($s) => strlen($s)); 56 55 $this->assertTrue($result->isSome()); 57 56 $this->assertSame(11, $result->unwrap()); 58 57 59 - $result = Option::None()->map(fn($s) => strlen($s)); 58 + $result = Option::None()->map(fn ($s) => strlen($s)); 60 59 $this->assertTrue($result->isNone()); 61 60 } 62 61 63 - public function testMapOr(): void 62 + public function test_map_or(): void 64 63 { 65 - $this->markTestIncomplete("TODO"); 66 - $this->assertSame(3, Option::Some("foo")->mapOr(42, fn($v) => strlen($v))); 67 - $this->assertSame(42, Option::None()->mapOr(42, fn($v) => strlen($v))); 64 + $this->markTestIncomplete('TODO'); 65 + $this->assertSame(3, Option::Some('foo')->mapOr(42, fn ($v) => strlen($v))); 66 + $this->assertSame(42, Option::None()->mapOr(42, fn ($v) => strlen($v))); 68 67 } 69 68 70 - public function testMapOrElse(): void 69 + public function test_map_or_else(): void 71 70 { 72 - $this->markTestIncomplete("TODO"); 71 + $this->markTestIncomplete('TODO'); 73 72 $k = 21; 74 - $this->assertSame(3, Option::Some("foo")->mapOrElse(fn() => 2 * $k, fn($v) => strlen($v))); 75 - $this->assertSame(42, Option::None()->mapOrElse(fn() => 2 * $k, fn($v) => strlen($v))); 73 + $this->assertSame(3, Option::Some('foo')->mapOrElse(fn () => 2 * $k, fn ($v) => strlen($v))); 74 + $this->assertSame(42, Option::None()->mapOrElse(fn () => 2 * $k, fn ($v) => strlen($v))); 76 75 } 77 76 78 - public function testFilter(): void 77 + public function test_filter(): void 79 78 { 80 - $this->markTestIncomplete("TODO"); 81 - $result = Option::Some(4)->filter(fn($x) => $x > 2); 79 + $this->markTestIncomplete('TODO'); 80 + $result = Option::Some(4)->filter(fn ($x) => $x > 2); 82 81 $this->assertTrue($result->isSome()); 83 82 $this->assertSame(4, $result->unwrap()); 84 83 85 - $result = Option::Some(1)->filter(fn($x) => $x > 2); 84 + $result = Option::Some(1)->filter(fn ($x) => $x > 2); 86 85 $this->assertTrue($result->isNone()); 87 86 88 - $result = Option::None()->filter(fn($x) => $x > 2); 87 + $result = Option::None()->filter(fn ($x) => $x > 2); 89 88 $this->assertTrue($result->isNone()); 90 89 } 91 90 92 - public function testExpect(): void 91 + public function test_expect(): void 93 92 { 94 - $this->markTestIncomplete("TODO"); 95 - $this->assertSame("value", Option::Some("value")->expect("fruits are healthy")); 93 + $this->markTestIncomplete('TODO'); 94 + $this->assertSame('value', Option::Some('value')->expect('fruits are healthy')); 96 95 97 96 $this->expectException(UnwrapNoneException::class); 98 - $this->expectExceptionMessage("fruits are healthy"); 99 - Option::None()->expect("fruits are healthy"); 97 + $this->expectExceptionMessage('fruits are healthy'); 98 + Option::None()->expect('fruits are healthy'); 100 99 } 101 100 102 - public function testAndThen(): void 101 + public function test_and_then(): void 103 102 { 104 - $this->markTestIncomplete("TODO"); 105 - $result = Option::Some(2)->andThen(fn($x) => Option::Some($x * 2)); 103 + $this->markTestIncomplete('TODO'); 104 + $result = Option::Some(2)->andThen(fn ($x) => Option::Some($x * 2)); 106 105 $this->assertTrue($result->isSome()); 107 106 $this->assertSame(4, $result->unwrap()); 108 107 109 - $result = Option::None()->andThen(fn($x) => Option::Some($x * 2)); 108 + $result = Option::None()->andThen(fn ($x) => Option::Some($x * 2)); 110 109 $this->assertTrue($result->isNone()); 111 110 112 111 // Returns None case 113 - $result = Option::Some(2)->andThen(fn($_) => Option::None()); 112 + $result = Option::Some(2)->andThen(fn ($_) => Option::None()); 114 113 $this->assertTrue($result->isNone()); 115 114 } 116 115 117 116 // Integration tests 118 117 119 - public function testCoreConstructionAndState(): void 118 + public function test_core_construction_and_state(): void 120 119 { 121 120 // Test Some with various values including falsy ones 122 121 $testValues = ['hello', 42, null, false, 0, '', []]; ··· 126 125 $this->assertFalse($some->isNone()); 127 126 $this->assertSame($value, $some->unwrap()); 128 127 } 129 - 128 + 130 129 // Test default value 131 130 $defaultSome = Option::Some(); 132 131 $this->assertTrue($defaultSome->isSome()); 133 132 $this->assertSame(true, $defaultSome->unwrap()); 134 133 } 135 134 136 - public function testEmailChainScenario(): void 135 + public function test_email_chain_scenario(): void 137 136 { 138 137 // Simulate the findUserEmail example from USAGE.md 139 138 $users = [ 140 - 123 => (object)[ 139 + 123 => (object) [ 141 140 'id' => 123, 142 - 'profile' => (object)['email' => 'JOHN@EXAMPLE.COM'] 141 + 'profile' => (object) ['email' => 'JOHN@EXAMPLE.COM'], 143 142 ], 144 - 456 => (object)[ 143 + 456 => (object) [ 145 144 'id' => 456, 146 - 'profile' => null 145 + 'profile' => null, 147 146 ], 148 147 ]; 149 - 150 - $findUser = fn(int $id): Option => 151 - isset($users[$id]) ? Option::Some($users[$id]) : Option::None(); 152 - 153 - $findUserEmail = function(int $userId) use ($findUser): Option { 148 + 149 + $findUser = fn (int $id): Option => isset($users[$id]) ? Option::Some($users[$id]) : Option::None(); 150 + 151 + $findUserEmail = function (int $userId) use ($findUser): Option { 154 152 return $findUser($userId) 155 - ->map(fn($user) => $user->profile) 156 - ->map(fn($profile) => $profile ? $profile->email : null) 157 - ->map(fn($email) => $email ? strtolower($email) : null); 153 + ->map(fn ($user) => $user->profile) 154 + ->map(fn ($profile) => $profile ? $profile->email : null) 155 + ->map(fn ($email) => $email ? strtolower($email) : null); 158 156 }; 159 - 157 + 160 158 $this->assertEquals('john@example.com', $findUserEmail(123)->unwrapOr('no-email@example.com')); 161 159 $this->assertNull($findUserEmail(456)->unwrapOr('no-email@example.com')); // Some(null) 162 160 $this->assertEquals('no-email@example.com', $findUserEmail(999)->unwrapOr('no-email@example.com')); 163 161 } 164 162 165 - public function testEdgeCases(): void 163 + public function test_edge_cases(): void 166 164 { 167 - $option = Option::Some("test"); 168 - $this->assertEquals("test", $option->unwrap()); 169 - $this->assertEquals("test", $option->unwrap()); 170 - $this->assertEquals("test", $option->unwrapOr("default")); 171 - 165 + $option = Option::Some('test'); 166 + $this->assertEquals('test', $option->unwrap()); 167 + $this->assertEquals('test', $option->unwrap()); 168 + $this->assertEquals('test', $option->unwrapOr('default')); 169 + 172 170 $option = Option::Some(5); 173 - $this->assertEquals(10, $option->map(fn($x) => $x * 2)->unwrap()); 174 - $this->assertEquals(15, $option->map(fn($x) => $x * 3)->unwrap()); 171 + $this->assertEquals(10, $option->map(fn ($x) => $x * 2)->unwrap()); 172 + $this->assertEquals(15, $option->map(fn ($x) => $x * 3)->unwrap()); 175 173 $this->assertEquals(5, $option->unwrap()); // Original unchanged 176 - 174 + 177 175 $this->expectException(RuntimeException::class); 178 - Option::Some("test")->map(fn($_) => throw new RuntimeException("Test exception")); 179 - 176 + Option::Some('test')->map(fn ($_) => throw new RuntimeException('Test exception')); 177 + 180 178 // Exception on None does not throw (callback not executed) 181 - $this->assertTrue(Option::None()->map(fn($_) => throw new RuntimeException("Should not execute"))->isNone()); 179 + $this->assertTrue(Option::None()->map(fn ($_) => throw new RuntimeException('Should not execute'))->isNone()); 182 180 } 183 181 }
+97 -94
tests/ResultTest.php
··· 2 2 3 3 declare(strict_types=1); 4 4 5 - require_once __DIR__ . '/../vendor/autoload.php'; 5 + require_once __DIR__.'/../vendor/autoload.php'; 6 6 7 - use PHPUnit\Framework\TestCase; 8 - use Ciarancoza\OptionResult\Result; 9 7 use Ciarancoza\OptionResult\Exceptions\UnwrapErrException; 10 8 use Ciarancoza\OptionResult\Exceptions\UnwrapOkException; 9 + use Ciarancoza\OptionResult\Result; 10 + use PHPUnit\Framework\TestCase; 11 11 12 12 class ResultTest extends TestCase 13 13 { 14 - 15 14 // Unit tests - Adapted from Rust Docs 16 15 17 - public function testIsOk(): void 16 + public function test_is_ok(): void 18 17 { 19 18 $x = Result::Ok(-3); 20 19 $this->assertSame(true, $x->isOk()); 21 20 22 - $x = Result::Err("Some error message"); 21 + $x = Result::Err('Some error message'); 23 22 $this->assertSame(false, $x->isOk()); 24 23 } 25 24 26 - public function testIsErr(): void 25 + public function test_is_err(): void 27 26 { 28 27 $x = Result::Ok(-3); 29 28 $this->assertSame(false, $x->isErr()); 30 29 31 - $x = Result::Err("Some error message"); 30 + $x = Result::Err('Some error message'); 32 31 $this->assertSame(true, $x->isErr()); 33 32 } 34 33 35 - public function testUnwrap(): void 34 + public function test_unwrap(): void 36 35 { 37 36 $x = Result::Ok(2); 38 37 $this->assertSame(2, $x->unwrap()); 39 38 40 39 $this->expectException(UnwrapErrException::class); 41 - Result::Err("emergency failure")->unwrap(); 40 + Result::Err('emergency failure')->unwrap(); 42 41 } 43 42 44 - public function testUnwrapErr(): void 43 + public function test_unwrap_err(): void 45 44 { 46 - $x = Result::Err("emergency failure"); 47 - $this->assertSame("emergency failure", $x->unwrapErr()); 45 + $x = Result::Err('emergency failure'); 46 + $this->assertSame('emergency failure', $x->unwrapErr()); 48 47 49 48 $this->expectException(UnwrapOkException::class); 50 49 Result::Ok(2)->unwrapErr(); 51 50 } 52 51 53 - public function testUnwrapOr(): void 52 + public function test_unwrap_or(): void 54 53 { 55 54 $default = 2; 56 55 $this->assertSame(9, Result::Ok(9)->unwrapOr($default)); 57 - $this->assertSame($default, Result::Err("error")->unwrapOr($default)); 56 + $this->assertSame($default, Result::Err('error')->unwrapOr($default)); 58 57 } 59 58 60 - public function testUnwrapOrElse(): void 59 + public function test_unwrap_or_else(): void 61 60 { 62 - $this->markTestIncomplete("TODO"); 63 - $this->assertSame(2, Result::Ok(2)->unwrapOrElse(fn($err) => strlen($err))); 64 - $this->assertSame(3, Result::Err("foo")->unwrapOrElse(fn($err) => strlen($err))); 61 + $this->markTestIncomplete('TODO'); 62 + $this->assertSame(2, Result::Ok(2)->unwrapOrElse(fn ($err) => strlen($err))); 63 + $this->assertSame(3, Result::Err('foo')->unwrapOrElse(fn ($err) => strlen($err))); 65 64 } 66 65 67 - public function testMap(): void 66 + public function test_map(): void 68 67 { 69 - $result = Result::Ok(5)->map(fn($i) => $i * 2); 68 + $result = Result::Ok(5)->map(fn ($i) => $i * 2); 70 69 $this->assertTrue($result->isOk()); 71 70 $this->assertSame(10, $result->unwrap()); 72 71 73 - $result = Result::Err("parse error")->map(fn($i) => $i * 2); 72 + $result = Result::Err('parse error')->map(fn ($i) => $i * 2); 74 73 $this->assertTrue($result->isErr()); 75 - $this->assertSame("parse error", $result->unwrapErr()); 74 + $this->assertSame('parse error', $result->unwrapErr()); 76 75 } 77 76 78 - public function testMapErr(): void 77 + public function test_map_err(): void 79 78 { 80 - $stringify = fn($x) => "error code: $x"; 79 + $stringify = fn ($x) => "error code: $x"; 81 80 82 81 $result = Result::Ok(2)->mapErr($stringify); 83 82 $this->assertTrue($result->isOk()); ··· 85 84 86 85 $result = Result::Err(13)->mapErr($stringify); 87 86 $this->assertTrue($result->isErr()); 88 - $this->assertSame("error code: 13", $result->unwrapErr()); 87 + $this->assertSame('error code: 13', $result->unwrapErr()); 89 88 } 90 89 91 - public function testMapOr(): void 90 + public function test_map_or(): void 92 91 { 93 - $this->markTestIncomplete("TODO"); 94 - $this->assertSame(3, Result::Ok("foo")->mapOr(42, fn($v) => strlen($v))); 95 - $this->assertSame(42, Result::Err("bar")->mapOr(42, fn($v) => strlen($v))); 92 + $this->markTestIncomplete('TODO'); 93 + $this->assertSame(3, Result::Ok('foo')->mapOr(42, fn ($v) => strlen($v))); 94 + $this->assertSame(42, Result::Err('bar')->mapOr(42, fn ($v) => strlen($v))); 96 95 } 97 96 98 - public function testMapOrElse(): void 97 + public function test_map_or_else(): void 99 98 { 100 - $this->markTestIncomplete("TODO"); 101 - $this->assertSame(3, Result::Ok("foo")->mapOrElse(fn($err) => strlen($err), fn($v) => strlen($v))); 102 - $this->assertSame(3, Result::Err("bar")->mapOrElse(fn($err) => strlen($err), fn($v) => strlen($v))); 99 + $this->markTestIncomplete('TODO'); 100 + $this->assertSame(3, Result::Ok('foo')->mapOrElse(fn ($err) => strlen($err), fn ($v) => strlen($v))); 101 + $this->assertSame(3, Result::Err('bar')->mapOrElse(fn ($err) => strlen($err), fn ($v) => strlen($v))); 103 102 } 104 103 105 - public function testGetOk(): void 104 + public function test_get_ok(): void 106 105 { 107 106 $option = Result::Ok(2)->getOk(); 108 107 $this->assertTrue($option->isSome()); 109 108 $this->assertSame(2, $option->unwrap()); 110 109 111 - $option = Result::Err("Nothing here")->getOk(); 110 + $option = Result::Err('Nothing here')->getOk(); 112 111 $this->assertTrue($option->isNone()); 113 112 } 114 113 115 - public function testGetErr(): void 114 + public function test_get_err(): void 116 115 { 117 116 $option = Result::Ok(2)->getErr(); 118 117 $this->assertTrue($option->isNone()); 119 118 120 - $option = Result::Err("Nothing here")->getErr(); 119 + $option = Result::Err('Nothing here')->getErr(); 121 120 $this->assertTrue($option->isSome()); 122 - $this->assertSame("Nothing here", $option->unwrap()); 121 + $this->assertSame('Nothing here', $option->unwrap()); 123 122 } 124 123 125 - public function testExpect(): void 124 + public function test_expect(): void 126 125 { 127 - $this->markTestIncomplete("TODO"); 128 - $this->assertSame("value", Result::Ok("value")->expect("Testing expect")); 126 + $this->markTestIncomplete('TODO'); 127 + $this->assertSame('value', Result::Ok('value')->expect('Testing expect')); 129 128 130 129 $this->expectException(UnwrapErrException::class); 131 - $this->expectExceptionMessage("Testing expect"); 132 - Result::Err("error")->expect("Testing expect"); 130 + $this->expectExceptionMessage('Testing expect'); 131 + Result::Err('error')->expect('Testing expect'); 133 132 } 134 133 135 - public function testExpectErr(): void 134 + public function test_expect_err(): void 136 135 { 137 - $this->markTestIncomplete("TODO"); 138 - $this->assertSame("value", Result::Err("value")->expectErr("Testing expect_err")); 136 + $this->markTestIncomplete('TODO'); 137 + $this->assertSame('value', Result::Err('value')->expectErr('Testing expect_err')); 139 138 140 139 $this->expectException(UnwrapOkException::class); 141 - $this->expectExceptionMessage("Testing expect_err"); 142 - Result::Ok("error")->expectErr("Testing expect_err"); 140 + $this->expectExceptionMessage('Testing expect_err'); 141 + Result::Ok('error')->expectErr('Testing expect_err'); 143 142 } 144 143 145 - public function testAndThen(): void 144 + public function test_and_then(): void 146 145 { 147 - $this->markTestIncomplete("TODO"); 148 - $result = Result::Ok(2)->andThen(fn($x) => Result::Ok($x * 2)); 146 + $this->markTestIncomplete('TODO'); 147 + $result = Result::Ok(2)->andThen(fn ($x) => Result::Ok($x * 2)); 149 148 $this->assertTrue($result->isOk()); 150 149 $this->assertSame(4, $result->unwrap()); 151 150 152 - $result = Result::Err("error")->andThen(fn($x) => Result::Ok($x * 2)); 151 + $result = Result::Err('error')->andThen(fn ($x) => Result::Ok($x * 2)); 153 152 $this->assertTrue($result->isErr()); 154 - $this->assertSame("error", $result->unwrapErr()); 153 + $this->assertSame('error', $result->unwrapErr()); 155 154 156 155 // Returns Err case 157 - $result = Result::Ok(2)->andThen(fn($_) => Result::Err("new error")); 156 + $result = Result::Ok(2)->andThen(fn ($_) => Result::Err('new error')); 158 157 $this->assertTrue($result->isErr()); 159 - $this->assertSame("new error", $result->unwrapErr()); 158 + $this->assertSame('new error', $result->unwrapErr()); 160 159 } 161 160 162 - public function testTryCatch(): void 161 + public function test_try_catch(): void 163 162 { 164 - $this->markTestIncomplete("TODO"); 165 - $result = Result::tryCatch(fn() => "success", fn($e) => "Error: " . $e->getMessage()); 163 + $this->markTestIncomplete('TODO'); 164 + $result = Result::tryCatch(fn () => 'success', fn ($e) => 'Error: '.$e->getMessage()); 166 165 $this->assertTrue($result->isOk()); 167 - $this->assertSame("success", $result->unwrap()); 166 + $this->assertSame('success', $result->unwrap()); 168 167 169 168 $result = Result::tryCatch( 170 - fn() => throw new Exception("something failed"), 171 - fn($e) => "Error: " . $e->getMessage() 169 + fn () => throw new Exception('something failed'), 170 + fn ($e) => 'Error: '.$e->getMessage() 172 171 ); 173 172 $this->assertTrue($result->isErr()); 174 - $this->assertSame("Error: something failed", $result->unwrapErr()); 173 + $this->assertSame('Error: something failed', $result->unwrapErr()); 175 174 } 176 - 175 + 177 176 // Integration tests 178 177 179 - public function testCoreConstructionAndState(): void 178 + public function test_core_construction_and_state(): void 180 179 { 181 180 $testValues = ['success', 42, null, false, 0, '', []]; 182 181 foreach ($testValues as $value) { ··· 184 183 $this->assertTrue($ok->isOk()); 185 184 $this->assertFalse($ok->isErr()); 186 185 $this->assertSame($value, $ok->unwrap()); 187 - 186 + 188 187 $err = Result::Err($value); 189 188 $this->assertFalse($err->isOk()); 190 189 $this->assertTrue($err->isErr()); 191 190 $this->assertSame($value, $err->unwrapErr()); 192 191 } 193 - 192 + 194 193 $this->assertSame(true, Result::Ok()->unwrap()); 195 194 } 196 195 197 - public function testOptionConversion(): void 196 + public function test_option_conversion(): void 198 197 { 199 198 $ok = Result::Ok('data'); 200 199 $err = Result::Err('error'); 201 - 200 + 202 201 $this->assertTrue($ok->getOk()->isSome()); 203 202 $this->assertEquals('data', $ok->getOk()->unwrap()); 204 203 $this->assertTrue($err->getOk()->isNone()); 205 - 204 + 206 205 $this->assertTrue($err->getErr()->isSome()); 207 206 $this->assertEquals('error', $err->getErr()->unwrap()); 208 207 $this->assertTrue($ok->getErr()->isNone()); 209 - 208 + 210 209 foreach ([null, false, 0, '', []] as $value) { 211 210 $this->assertEquals($value, Result::Ok($value)->getOk()->unwrap()); 212 211 $this->assertEquals($value, Result::Err($value)->getErr()->unwrap()); 213 212 } 214 213 } 215 214 216 - public function testApiResponseScenario(): void 215 + public function test_api_response_scenario(): void 217 216 { 218 217 $successResponse = ['user' => ['id' => 123, 'name' => 'john doe', 'email' => 'JOHN@EXAMPLE.COM']]; 219 - 218 + 220 219 $expected = ['id' => 123, 'name' => 'John doe', 'email' => 'john@example.com']; 221 220 $this->assertEquals($expected, Result::Ok($successResponse) 222 - ->map(fn($data) => $data['user']) 223 - ->map(fn($user) => [ 221 + ->map(fn ($data) => $data['user']) 222 + ->map(fn ($user) => [ 224 223 'id' => $user['id'], 225 224 'name' => ucfirst($user['name']), 226 - 'email' => strtolower($user['email']) 225 + 'email' => strtolower($user['email']), 227 226 ])->unwrapOr(['error' => 'User not found'])); 228 - 229 - $this->assertEquals(['error' => 'User not found'], Result::Err("API request failed: 404") 230 - ->map(fn($data) => $data['user']) 231 - ->mapErr(fn($error) => "Failed to process user: " . $error) 227 + 228 + $this->assertEquals(['error' => 'User not found'], Result::Err('API request failed: 404') 229 + ->map(fn ($data) => $data['user']) 230 + ->mapErr(fn ($error) => 'Failed to process user: '.$error) 232 231 ->unwrapOr(['error' => 'User not found'])); 233 232 } 234 233 235 - public function testValidationChainScenario(): void 234 + public function test_validation_chain_scenario(): void 236 235 { 237 - $processInput = function(string $input) { 238 - if (empty(trim($input))) return Result::Err("Input cannot be empty"); 236 + $processInput = function (string $input) { 237 + if (empty(trim($input))) { 238 + return Result::Err('Input cannot be empty'); 239 + } 239 240 $trimmed = trim($input); 240 - if (strlen($trimmed) > 10) return Result::Err("Input too long"); 241 + if (strlen($trimmed) > 10) { 242 + return Result::Err('Input too long'); 243 + } 244 + 241 245 return Result::Ok(strtoupper($trimmed)); 242 246 }; 243 247 244 - $this->assertEquals("HELLO", $processInput(" hello ")->unwrap()); 245 - $this->assertEquals("Input cannot be empty", $processInput(" ")->unwrapErr()); 246 - $this->assertEquals("Input too long", $processInput("this is way too long")->unwrapErr()); 248 + $this->assertEquals('HELLO', $processInput(' hello ')->unwrap()); 249 + $this->assertEquals('Input cannot be empty', $processInput(' ')->unwrapErr()); 250 + $this->assertEquals('Input too long', $processInput('this is way too long')->unwrapErr()); 247 251 } 248 252 249 - public function testDataPipelineScenario(): void 253 + public function test_data_pipeline_scenario(): void 250 254 { 251 255 $data = ['users' => [ 252 256 ['name' => ' alice ', 'score' => 85], 253 257 ['name' => 'bob', 'score' => 92], 254 - ['name' => '', 'score' => 78] 258 + ['name' => '', 'score' => 78], 255 259 ]]; 256 260 257 261 $users = Result::Ok($data) 258 - ->map(fn($d) => $d['users']) 259 - ->map(fn($users) => array_filter($users, fn($u) => !empty(trim($u['name'])))) 260 - ->map(fn($users) => array_map(fn($u) => [ 262 + ->map(fn ($d) => $d['users']) 263 + ->map(fn ($users) => array_filter($users, fn ($u) => ! empty(trim($u['name'])))) 264 + ->map(fn ($users) => array_map(fn ($u) => [ 261 265 'name' => ucfirst(trim($u['name'])), 262 266 'score' => $u['score'], 263 - 'grade' => $u['score'] >= 90 ? 'A' : ($u['score'] >= 80 ? 'B' : 'C') 267 + 'grade' => $u['score'] >= 90 ? 'A' : ($u['score'] >= 80 ? 'B' : 'C'), 264 268 ], $users))->unwrap(); 265 269 266 270 $this->assertCount(2, $users); 267 271 $this->assertEquals(['name' => 'Alice', 'score' => 85, 'grade' => 'B'], $users[0]); 268 272 $this->assertEquals(['name' => 'Bob', 'score' => 92, 'grade' => 'A'], $users[1]); 269 273 } 270 - 271 274 }