├── .gitignore ├── .travis.yml ├── src └── PhpTry │ ├── NoSuchElementException.php │ ├── Failure.php │ ├── Success.php │ ├── LazyAttempt.php │ └── Attempt.php ├── tests ├── bootstrap.php └── PhpTry │ ├── LazyAttemptFailureTest.php │ ├── LazyAttemptSuccessfulTest.php │ ├── LazyAttemptTest.php │ ├── AttemptTest.php │ ├── FailureTest.php │ ├── SuccessTest.php │ ├── SuccessfulAttemptTestCase.php │ └── FailedAttemptTestCase.php ├── phpunit.xml.dist ├── composer.json ├── LICENSE ├── examples └── user-input.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | composer.lock 3 | composer.phar 4 | /vendor/ 5 | tags 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - hhvm 8 | 9 | before_script: composer install 10 | 11 | script: phpunit 12 | -------------------------------------------------------------------------------- /src/PhpTry/NoSuchElementException.php: -------------------------------------------------------------------------------- 1 | add('PhpTry', __DIR__); 6 | } else { 7 | throw new RuntimeException('Install dependencies to run test suite.'); 8 | } 9 | -------------------------------------------------------------------------------- /tests/PhpTry/LazyAttemptFailureTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($called); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/PhpTry/AttemptTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('PhpTry\Success', $success); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_returns_Failure_if_the_callable_thrown() 24 | { 25 | $failure = Attempt::call(function() { throw new Exception(); }); 26 | 27 | $this->assertInstanceOf('PhpTry\Failure', $failure); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/PhpTry/ 17 | 18 | 19 | 20 | 21 | 22 | ./src/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phptry/phptry", 3 | "description": "Try type for PHP", 4 | "keywords": ["try", "success", "failure", "monad"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Alexander", 10 | "email": "iam.asm89@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3.2" 15 | }, 16 | "require-dev": { 17 | "phpoption/phpoption": "1.*" 18 | }, 19 | "autoload": { 20 | "psr-0": { "PhpTry\\": "src/" } 21 | }, 22 | "suggest": { 23 | "phpoption/phpoption": "If you want to turn your success/failure into an option." 24 | }, 25 | "extra": { 26 | "branch-alias": { 27 | "dev-master": "0.1-dev" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Alexander 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/PhpTry/FailureTest.php: -------------------------------------------------------------------------------- 1 | exception); 13 | } 14 | 15 | /** 16 | * @test 17 | */ 18 | public function it_returns_itself_on_flatMap() 19 | { 20 | $this->assertSame($this->failure, $this->failure->flatMap(function(){})); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function it_returns_itself_on_map() 27 | { 28 | $this->assertSame($this->failure, $this->failure->map(function(){})); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function it_returns_itself_on_filter() 35 | { 36 | $this->assertSame($this->failure, $this->failure->filter(function(){})); 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function it_returns_itself_onFailure() 43 | { 44 | $this->assertSame($this->failure, $this->failure->onFailure(function() {})); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function it_returns_itself_onSuccess() 51 | { 52 | $this->assertSame($this->failure, $this->failure->onSuccess(function() {})); 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function it_returns_itself_forAll() 59 | { 60 | $this->assertSame($this->failure, $this->failure->forAll(function() {})); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/PhpTry/SuccessTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($this->success, $this->success->orElse(new Failure(new Exception()))); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function it_returns_itself_on_orElseCall() 27 | { 28 | $this->assertEquals($this->success, $this->success->orElseCall(function (){ return 21; })); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function it_returns_itself_if_the_callable_returns_true_on_filter() 35 | { 36 | $this->assertSame($this->success, $this->success->filter(function() { return true; })); 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function it_returns_itself_on_recoverWith() 43 | { 44 | $this->assertSame($this->success, $this->success->recoverWith(function(){})); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function it_returns_itself_on_recover() 51 | { 52 | $this->assertSame($this->success, $this->success->recover(function(){})); 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function it_returns_itself_onSuccess() 59 | { 60 | $this->assertSame($this->success, $this->success->onSuccess(function() {})); 61 | } 62 | 63 | /** 64 | * @test 65 | */ 66 | public function it_returns_itself_onFailure() 67 | { 68 | $this->assertSame($this->success, $this->success->onFailure(function() {})); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/PhpTry/Failure.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 15 | } 16 | 17 | public function isFailure() 18 | { 19 | return true; 20 | } 21 | 22 | public function isSuccess() 23 | { 24 | return false; 25 | } 26 | 27 | public function get() 28 | { 29 | throw $this->exception; 30 | } 31 | 32 | public function flatMap($callable) 33 | { 34 | return $this; 35 | } 36 | 37 | public function map($callable) 38 | { 39 | return $this; 40 | } 41 | 42 | public function filter($callable) 43 | { 44 | return $this; 45 | } 46 | 47 | public function recoverWith($callable) 48 | { 49 | try { 50 | $value = call_user_func_array($callable, array($this->exception)); 51 | 52 | if ( ! $value instanceof Attempt) { 53 | return new Failure(new UnexpectedValueException('Return value of callable should be an Attempt.')); 54 | } 55 | 56 | return $value; 57 | } catch (Exception $ex) { 58 | return new Failure($ex); 59 | } 60 | } 61 | 62 | public function recover($callable) 63 | { 64 | return Attempt::call($callable, array($this->exception)); 65 | } 66 | 67 | public function onFailure($callable) 68 | { 69 | $value = call_user_func_array($callable, array($this->exception)); 70 | 71 | return $this; 72 | } 73 | 74 | public function onSuccess($callable) 75 | { 76 | return $this; 77 | } 78 | 79 | public function forAll($callable) 80 | { 81 | return $this; 82 | } 83 | 84 | public function toOption() 85 | { 86 | return \PhpOption\None::create(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/PhpTry/Success.php: -------------------------------------------------------------------------------- 1 | value = $value; 16 | } 17 | 18 | public function isFailure() 19 | { 20 | return false; 21 | } 22 | 23 | public function isSuccess() 24 | { 25 | return true; 26 | } 27 | 28 | public function get() 29 | { 30 | return $this->value; 31 | } 32 | 33 | public function flatMap($callable) 34 | { 35 | try { 36 | $value = call_user_func_array($callable, array($this->value)); 37 | 38 | if ( ! $value instanceof Attempt) { 39 | return new Failure(new UnexpectedValueException('Return value of callable should be an Attempt.')); 40 | } 41 | 42 | return $value; 43 | } catch (Exception $ex) { 44 | return new Failure($ex); 45 | } 46 | } 47 | 48 | public function map($callable) 49 | { 50 | return Attempt::call($callable, array($this->value)); 51 | } 52 | 53 | public function filter($callable) 54 | { 55 | try { 56 | $value = call_user_func_array($callable, array($this->value)); 57 | 58 | if ($value) { 59 | return $this; 60 | } 61 | 62 | return new Failure(new NoSuchElementException('Predicate does not hold for ' . $this->value)); 63 | } catch (Exception $ex) { 64 | return new Failure($ex); 65 | } 66 | } 67 | 68 | public function recoverWith($callable) 69 | { 70 | return $this; 71 | } 72 | 73 | public function recover($callable) 74 | { 75 | return $this; 76 | } 77 | 78 | public function onFailure($callable) 79 | { 80 | return $this; 81 | } 82 | 83 | public function onSuccess($callable) 84 | { 85 | $value = call_user_func_array($callable, array($this->value)); 86 | 87 | return $this; 88 | } 89 | 90 | public function toOption() 91 | { 92 | return new \PhpOption\Some($this->value); 93 | } 94 | 95 | public function forAll($callable) 96 | { 97 | call_user_func($callable, $this->value); 98 | 99 | return $this; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/PhpTry/LazyAttempt.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 17 | $this->arguments = $arguments; 18 | } 19 | 20 | public function isFailure() 21 | { 22 | return $this->attempt()->isFailure(); 23 | } 24 | 25 | public function isSuccess() 26 | { 27 | return $this->attempt()->isSuccess(); 28 | } 29 | 30 | public function getOrElse($default) 31 | { 32 | return $this->attempt()->getOrElse($default); 33 | } 34 | 35 | public function getOrCall($callable) 36 | { 37 | return $this->attempt()->getOrCall($callable); 38 | } 39 | 40 | public function orElse(Attempt $try) 41 | { 42 | return $this->attempt()->orElse($try); 43 | } 44 | 45 | public function orElseCall($callable) 46 | { 47 | return $this->attempt()->orElseCall($callable); 48 | } 49 | 50 | public function getIterator() 51 | { 52 | return $this->attempt()->getIterator(); 53 | } 54 | 55 | public function get() 56 | { 57 | return $this->attempt()->get(); 58 | } 59 | 60 | public function flatMap($callable) 61 | { 62 | return $this->attempt()->flatMap($callable); 63 | } 64 | 65 | public function map($callable) 66 | { 67 | return $this->attempt()->map($callable); 68 | } 69 | 70 | public function filter($callable) 71 | { 72 | return $this->attempt()->filter($callable); 73 | } 74 | 75 | public function recoverWith($callable) 76 | { 77 | return $this->attempt()->recoverWith($callable); 78 | } 79 | 80 | public function recover($callable) 81 | { 82 | return $this->attempt()->recover($callable); 83 | } 84 | 85 | public function onSuccess($callable) 86 | { 87 | return $this->attempt()->onSuccess($callable); 88 | 89 | } 90 | 91 | public function onFailure($callable) 92 | { 93 | return $this->attempt()->onFailure($callable); 94 | } 95 | 96 | private function attempt() 97 | { 98 | if (null === $this->attempt) { 99 | return $this->attempt = Attempt::call($this->callable, $this->arguments); 100 | } 101 | 102 | return $this->attempt; 103 | } 104 | 105 | public function toOption() 106 | { 107 | return $this->attempt()->toOption(); 108 | } 109 | 110 | public function forAll($callable) 111 | { 112 | return $this->attempt()->forAll($callable); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /examples/user-input.php: -------------------------------------------------------------------------------- 1 | map(function($elem) use ($c) { return multiply($elem, $c); }); 41 | } 42 | 43 | $try = promptDivide(); 44 | 45 | // on* handlers 46 | $try 47 | ->onSuccess(function($elem) { echo "Result of a / b * c is: $elem\n"; }) 48 | ->onFailure(function($elem) { echo "Something went wrong: " . $elem->getMessage() . "\n"; promptDivide(); }) 49 | ; 50 | 51 | //// get() the value, will throw 52 | //echo $try->get(); 53 | // 54 | //// return a default value if Failure 55 | //echo $try->getOrElse(-1); 56 | // 57 | //// return a default value returned by a function if Failure 58 | //echo $try->getOrCall(function() { return -1; }); 59 | // 60 | //// or else return another attempt 61 | //echo $try->orElse(Attempt::call('divide', array(42, 21)))->get(); 62 | // 63 | //// or else return another attempt from a callable 64 | //echo $try->orElseCall('promptDivide')->get(); 65 | // 66 | //// only foreachable if success 67 | //foreach ($try as $result) { 68 | // echo $result; 69 | //} 70 | // 71 | //// map with another attempt 72 | //echo $try->flatMap(function($elem) { 73 | // return Attempt::call('divide', array($elem, promptDivide()->get())); 74 | //})->get(); 75 | // 76 | //// map the success value to another value 77 | //echo $try->map(function($elem) { return $elem * 2; })->get(); 78 | // 79 | //// filter the success value, returns Failure if it doesn't match the filter 80 | //echo $try->filter(function($elem) { return $elem === 42; })->get(); 81 | // 82 | //// recover with with a value returned by a callable 83 | //echo $try 84 | // ->filter(function($elem) { return $elem === 42; }) 85 | // ->recover(function($ex) { if ($ex instanceof RuntimeException) { return 21; } throw $ex; }) 86 | // ->get(); 87 | // 88 | //// recover with with an attempt returned by a callable 89 | //echo $try 90 | // ->filter(function($elem) { return $elem === 42; }) 91 | // ->recoverWith(function() { return promptDivide(); }) 92 | // ->get(); 93 | -------------------------------------------------------------------------------- /tests/PhpTry/SuccessfulAttemptTestCase.php: -------------------------------------------------------------------------------- 1 | success = $this->createSuccess(42); 15 | } 16 | 17 | abstract protected function createSuccess($value); 18 | 19 | /** 20 | * @test 21 | */ 22 | public function it_is_success() 23 | { 24 | $this->assertTrue($this->success->isSuccess()); 25 | } 26 | 27 | /** 28 | * @test 29 | */ 30 | public function it_is_not_failure() 31 | { 32 | $this->assertFalse($this->success->isFailure()); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function it_returns_the_wrapped_value() 39 | { 40 | $this->assertEquals(42, $this->success->get()); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function it_returns_the_wrapped_value_on_getOrElse() 47 | { 48 | $this->assertEquals(42, $this->success->getOrElse(21)); 49 | 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function it_returns_the_wrapped_value_on_getOrCall() 56 | { 57 | $this->assertEquals(42, $this->success->getOrCall(function(){ return 21; })); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function it_returns_the_element_with_foreach() 64 | { 65 | $called = 0; 66 | foreach ($this->success as $elem) { 67 | $this->assertEquals(42, $elem); 68 | $called++; 69 | } 70 | 71 | $this->assertEquals(1, $called); 72 | } 73 | 74 | /** 75 | * @test 76 | * @dataProvider attemptValues 77 | */ 78 | public function it_returns_the_result_of_the_callable_on_flatMap(Attempt $attempt) 79 | { 80 | $this->assertSame($attempt, $this->success->flatMap(function() use ($attempt) { return $attempt; })); 81 | } 82 | 83 | public function attemptValues() 84 | { 85 | return array( 86 | array(new Success(42)), 87 | array(new Failure(new Exception())), 88 | ); 89 | } 90 | 91 | /** 92 | * @test 93 | */ 94 | public function it_passes_its_value_to_the_flatMap_callable() 95 | { 96 | $value = null; 97 | 98 | $this->success->flatMap(function($elem) use (&$value) { $value = $elem; return new Success(21); }); 99 | 100 | $this->assertEquals($this->success->get(), $value); 101 | } 102 | 103 | /** 104 | * @test 105 | */ 106 | public function it_returns_Failure_if_the_flatMap_callable_throws() 107 | { 108 | $result = $this->success->flatMap(function() { throw new Exception('meh.'); }); 109 | 110 | $this->assertInstanceOf('PhpTry\\Failure', $result); 111 | } 112 | 113 | /** 114 | * @test 115 | */ 116 | public function it_returns_Failure_if_the_flatMap_callable_does_not_return_an_Attempt() 117 | { 118 | $result = $this->success->flatMap(function() { return 21; }); 119 | 120 | $this->assertInstanceOf('PhpTry\\Failure', $result); 121 | } 122 | 123 | /** 124 | * @test 125 | */ 126 | public function it_returns_the_result_of_the_callable_on_map() 127 | { 128 | $this->assertEquals(new Success(21), $this->success->map(function() { return 21; })); 129 | } 130 | 131 | /** 132 | * @test 133 | */ 134 | public function it_passes_its_value_to_the_map_callable() 135 | { 136 | $value = null; 137 | 138 | $this->success->map(function($elem) use (&$value) { $value = $elem; return 21; }); 139 | 140 | $this->assertEquals($this->success->get(), $value); 141 | } 142 | 143 | /** 144 | * @test 145 | */ 146 | public function it_returns_Failure_if_the_map_callable_throws() 147 | { 148 | $result = $this->success->map(function() { throw new Exception('meh.'); }); 149 | 150 | $this->assertInstanceOf('PhpTry\\Failure', $result); 151 | } 152 | 153 | /** 154 | * @test 155 | */ 156 | public function it_returns_Failure_if_the_callable_returns_false_on_filter() 157 | { 158 | $result = $this->success->filter(function() { return false; }); 159 | $this->assertInstanceOf('PhpTry\\Failure', $result); 160 | } 161 | 162 | /** 163 | * @test 164 | */ 165 | public function it_passes_its_value_to_the_filter_callable() 166 | { 167 | $value = null; 168 | 169 | $this->success->filter(function($elem) use (&$value) { $value = $elem; return true; }); 170 | 171 | $this->assertEquals($this->success->get(), $value); 172 | } 173 | 174 | /** 175 | * @test 176 | */ 177 | public function it_returns_Failure_if_the_filter_callable_throws() 178 | { 179 | $result = $this->success->filter(function() { throw new Exception('meh.'); }); 180 | 181 | $this->assertInstanceOf('PhpTry\\Failure', $result); 182 | } 183 | 184 | /** 185 | * @test 186 | */ 187 | public function it_passes_its_value_to_the_onSuccess_callable() 188 | { 189 | $value = null; 190 | 191 | $this->success->onSuccess(function($elem) use (&$value) { $value = $elem; }); 192 | 193 | $this->assertEquals($this->success->get(), $value); 194 | } 195 | 196 | /** 197 | * @test 198 | */ 199 | public function it_passes_does_not_call_onFailure() 200 | { 201 | $called = false; 202 | 203 | $this->success->onFailure(function() use (&$called) { $called = true; }); 204 | 205 | $this->assertFalse($called); 206 | } 207 | 208 | /** 209 | * @test 210 | */ 211 | public function it_can_be_converted_to_a_Some_option() 212 | { 213 | $this->assertEquals(new \PhpOption\Some(42), $this->success->toOption()); 214 | } 215 | 216 | /** 217 | * @test 218 | */ 219 | public function it_calls_on_forAll() 220 | { 221 | $called = false; 222 | $value = null; 223 | 224 | $this->success->forAll(function($v) use (&$called, &$value) { $called = true; $value = $v; }); 225 | 226 | $this->assertTrue($called); 227 | $this->assertEquals(42, $value); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Php Try type 2 | ============ 3 | 4 | A Try type for PHP. 5 | 6 | The Try type is useful when called code will either return a value (Success) or 7 | throw an exception (Failure). Instead of relying on the `try {} catch {}` 8 | mechanism to handle this cases, the fact that code might either throw or return 9 | a value is now encoded in its return type. 10 | 11 | The type shows it usefulness with it's ability to create a "pipeline" 12 | operations, catching exceptions along the way. 13 | 14 | > Note: this implementation of the Try type is called Attempt, because "try" is 15 | > a reserved keyword in PHP. 16 | 17 | ## Before / after 18 | 19 | Before, the `UserService` and `Serializer` code might throw exceptions, so we 20 | have an explicit try/catch: 21 | 22 | ```php 23 | try { 24 | $user = $userService->findBy($id); 25 | $responseBody = $this->serializeUser($user); 26 | 27 | return new Response($user); 28 | } catch (Exception $ex) { 29 | return Response('error', 500); 30 | } 31 | ``` 32 | 33 | After, the `UserService` and `Serializer` now return a response of type `Try` 34 | meaning that the computation will either be a `Failure` or a `Success`. The 35 | combinators on the `Try` type are used to chain the following code in the case 36 | the previous operation was successful. 37 | 38 | ```php 39 | return $userService->findBy($id) 40 | ->flatMap(function($user) { return $this->serializeUser($user); }) // walk the happy path! 41 | ->map(function($responseBody) { return new Response($responseBody); }) 42 | ->recover(function($ex) { return new Response('error', 500); }) 43 | ->get(); // returns the wrapped value 44 | ``` 45 | 46 | ## Installation 47 | 48 | Run: 49 | 50 | ``` 51 | composer require phptry/phptry 52 | ``` 53 | or add it to your `composer.json` file. 54 | 55 | ## Usage 56 | 57 | > Note: most of the example code below can be tried out with the 58 | > `user-input.php` example from the `examples/` directory. 59 | 60 | ### Constructing an Attempt 61 | 62 | Turn any callable in an `Attempt` using the `Attempt::call()` construct it. 63 | 64 | ```php 65 | \PhpTry\Attempt::call('callableThatMightThrow', array('argument1', 'argument2')); 66 | ``` 67 | 68 | Or use `Success` and `Failure` directly in your API instead of throwing exceptions: 69 | 70 | ```php 71 | function divide($dividend, $divisor) { 72 | if ($divisor === 0) { 73 | return new \PhpTry\Failure(new InvalidArgumentException('Divisor cannot be 0.')); 74 | } 75 | 76 | return new \PhpTry\Success($dividend / $divisor); 77 | } 78 | ``` 79 | 80 | ### Using combinators on an Attempt 81 | 82 | Now that we have the `Attempt` object we can use it's combinators to handle the 83 | success and failure cases. 84 | 85 | #### Getting the value 86 | 87 | Gets the value from Success, or throws the original exception if it was a Failure. 88 | 89 | ```php 90 | $try->get(); 91 | ``` 92 | 93 | #### Falling back to a default value if Failure 94 | 95 | Gets the value from Success, or get a provided alternative if the computation failed. 96 | 97 | ```php 98 | // or a provided fallback value 99 | $try->getOrElse(-1); 100 | 101 | // or a value returned by the callable 102 | // note: if the provided callable throws, this exception will not be catched 103 | $try->getOrCall(function() { return -1; }); 104 | 105 | // or else return another Attempt 106 | $try->orElse(Attempt::call('divide', array(42, 21))); 107 | 108 | // or else return Another attempt from a callable 109 | $try->orElseCall('promptDivide'); 110 | ``` 111 | 112 | #### Walking the happy path 113 | 114 | Sometimes you care about the Success path and want to propagate or even ignore 115 | Failure. The `filter`, `flatMap` and `map` operators shown below will execute 116 | the given code if the previous computation was a Success, or propagate the 117 | Failure otherwise. If the function passed to `flatMap` or `map` throws, the 118 | operation will result in a Failure. 119 | 120 | ```php 121 | // map to Another attempt 122 | $try->flatMap(function($elem) { 123 | return Attempt::call('divide', array($elem, promptDivide()->get())); 124 | }); 125 | 126 | // map the success value to another value 127 | $try->map(function($elem) { return $elem * 2; }); 128 | 129 | // Success, if the predicate holds for the Success value, Failure otherwise 130 | $try->filter(function($elem) { return $elem === 42; }) 131 | 132 | // only foreachable if success 133 | foreach ($try as $result) { 134 | echo $result; 135 | } 136 | ``` 137 | 138 | #### Recovering from failure 139 | 140 | When we do care about the Failure path we might want to try and fix things. The 141 | `recover` and `recoverWith` operations are for Failure, what `flatMap` and 142 | `map` are for Success. 143 | 144 | ```php 145 | // recover with with a value returned by a callable 146 | $try->recover(function($ex) { if ($ex instanceof RuntimeException) { return 21; } throw $ex; }) 147 | 148 | // recover with with an attempt returned by a callable 149 | $try->recoverWith(function() { return promptDivide(); }) 150 | ``` 151 | 152 | The `recover` and `recoverWith` combinators can be useful when calling for 153 | example http services that might fail. A failed call can be recovered by 154 | calling the service again or calling an alternative service. 155 | 156 | #### Don't call us, we'll call you 157 | 158 | The Try type can also call provided callables on a successful or failed computation: 159 | 160 | ```php 161 | // on* handlers 162 | $try 163 | ->onSuccess(function($elem) { echo "Result of a / b * c is: $elem\n"; }) 164 | ->onFailure(function($elem) { echo "Something went wrong: " . $elem->getMessage() . "\n"; promptDivide(); }) 165 | ; 166 | ``` 167 | 168 | #### Lazily executed Attempts 169 | 170 | It is possible to execute the provided callable only when needed. This is 171 | especially useful when recovering with for example expensive alternatives. 172 | 173 | ```php 174 | $try->orElse(Attempt::lazily('someExpensiveComputationThatMightThrow')); 175 | ``` 176 | 177 | #### Other options 178 | 179 | When you have [phpoption/phpoption] installed, the Attempt can be converted to 180 | an Option. In this mapping a Succes maps to Some and a Failure maps to a None 181 | value. 182 | 183 | ```php 184 | $try->toOption(); // Some(value) or None() 185 | ``` 186 | 187 | [phpoption/phpoption]: https://github.com/schmittjoh/php-option 188 | 189 | ## Inspiration 190 | 191 | - Implementation and general idea is based on scala's [Try] 192 | - Schmittjoh's [Option type] for PHP 193 | 194 | [Try]: http://www.scala-lang.org/api/2.9.3/scala/util/Try.html 195 | [Option type]: https://github.com/schmittjoh/php-option 196 | -------------------------------------------------------------------------------- /tests/PhpTry/FailedAttemptTestCase.php: -------------------------------------------------------------------------------- 1 | exception = new TestException(); 16 | $this->failure = $this->createFailure($this->exception); 17 | } 18 | 19 | abstract protected function createFailure($exception); 20 | 21 | /** 22 | * @test 23 | */ 24 | public function it_is_not_success() 25 | { 26 | $this->assertFalse($this->failure->isSuccess()); 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function it_is_failure() 33 | { 34 | $this->assertTrue($this->failure->isFailure()); 35 | } 36 | 37 | /** 38 | * @test 39 | * @expectedException PhpTry\TestException 40 | */ 41 | public function it_throws_when_getting_the_value() 42 | { 43 | $this->failure->get(); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function it_returns_the_other_value_on_getOrElse() 50 | { 51 | $this->assertEquals(21, $this->failure->getOrElse(21)); 52 | 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function it_returns_the_value_returned_by_the_callable_on_getOrCall() 59 | { 60 | $this->assertEquals(21, $this->failure->getOrCall(function(){ return 21; })); 61 | } 62 | 63 | /** 64 | * @test 65 | * @expectedException PhpTry\TestException 66 | */ 67 | public function it_returns_does_not_handle_the_exception_thrown_by_the_callable_on_getOrCall() 68 | { 69 | $this->failure->getOrCall(function(){ throw new TestException(); }); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function it_returns_the_other_attempt_on_orElse() 76 | { 77 | $other = new Success(42); 78 | $this->assertEquals($other, $this->failure->orElse($other)); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function it_returns_the_result_of_the_callable_on_orElseCall() 85 | { 86 | $this->assertEquals(new Success(21), $this->failure->orElseCall(function (){ return new Success(21); })); 87 | } 88 | 89 | /** 90 | * @test 91 | */ 92 | public function it_returns_Failure_if_the_result_of_the_callable_is_not_an_attempt_on_orElseCall() 93 | { 94 | $result = $this->failure->orElseCall(function (){ return 21; }); 95 | $this->assertInstanceOf('PhpTry\\Failure', $result); 96 | } 97 | 98 | /** 99 | * @test 100 | */ 101 | public function it_returns_Failure_if_the_callable_throws_on_orElseCall() 102 | { 103 | $result = $this->failure->orElseCall(function (){ throw new TestException(); }); 104 | $this->assertInstanceOf('PhpTry\\Failure', $result); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function it_does_not_return_an_element_in_foreach() 111 | { 112 | $called = false; 113 | foreach ($this->failure as $elem) { 114 | $called = true; 115 | } 116 | 117 | $this->assertFalse($called); 118 | } 119 | 120 | /** 121 | * @test 122 | * @dataProvider attemptValues 123 | */ 124 | public function it_returns_the_result_of_the_callable_on_recoverWith(Attempt $attempt) 125 | { 126 | $this->assertSame($attempt, $this->failure->recoverWith(function() use ($attempt) { return $attempt; })); 127 | } 128 | 129 | public function attemptValues() 130 | { 131 | return array( 132 | array(new Success(42)), 133 | array(new Failure(new Exception())), 134 | ); 135 | } 136 | 137 | /** 138 | * @test 139 | */ 140 | public function it_passes_its_value_to_the_recoverWith_callable() 141 | { 142 | $value = null; 143 | 144 | $this->failure->recoverWith(function($elem) use (&$value) { $value = $elem; return new Success(21); }); 145 | 146 | $this->assertSame($this->exception, $value); 147 | } 148 | 149 | /** 150 | * @test 151 | */ 152 | public function it_returns_Failure_if_the_recoverWith_callable_throws() 153 | { 154 | $result = $this->failure->recoverWith(function() { throw new Exception('meh.'); }); 155 | 156 | $this->assertInstanceOf('PhpTry\\Failure', $result); 157 | } 158 | 159 | /** 160 | * @test 161 | */ 162 | public function it_returns_Failure_if_the_recoverWith_callable_does_not_return_an_Attempt() 163 | { 164 | $result = $this->failure->recoverWith(function() { return 21; }); 165 | 166 | $this->assertInstanceOf('PhpTry\\Failure', $result); 167 | } 168 | 169 | /** 170 | * @test 171 | */ 172 | public function it_returns_the_result_of_the_callable_on_recover() 173 | { 174 | $this->assertEquals(new Success(21), $this->failure->recover(function() { return 21; })); 175 | } 176 | 177 | /** 178 | * @test 179 | */ 180 | public function it_passes_its_value_to_the_recover_callable() 181 | { 182 | $value = null; 183 | 184 | $this->failure->recover(function($elem) use (&$value) { $value = $elem; return 21; }); 185 | 186 | $this->assertSame($this->exception, $value); 187 | } 188 | 189 | /** 190 | * @test 191 | */ 192 | public function it_returns_Failure_if_the_recover_callable_throws() 193 | { 194 | $result = $this->failure->recover(function() { throw new Exception('meh.'); }); 195 | 196 | $this->assertInstanceOf('PhpTry\\Failure', $result); 197 | } 198 | 199 | /** 200 | * @test 201 | */ 202 | public function it_passes_its_value_to_the_onFailure_callable() 203 | { 204 | $value = null; 205 | 206 | $this->failure->onFailure(function($elem) use (&$value) { $value = $elem; }); 207 | 208 | $this->assertEquals($this->exception, $value); 209 | } 210 | 211 | /** 212 | * @test 213 | */ 214 | public function it_passes_does_not_call_onSuccess() 215 | { 216 | $called = false; 217 | 218 | $this->failure->onSuccess(function() use (&$called) { $called = true; }); 219 | 220 | $this->assertFalse($called); 221 | } 222 | 223 | /** 224 | * @test 225 | */ 226 | public function it_can_be_converted_to_a_None_option() 227 | { 228 | $this->assertEquals(\PhpOption\None::create(), $this->failure->toOption()); 229 | } 230 | 231 | /** 232 | * @test 233 | */ 234 | public function it_does_not_call_on_forAll() 235 | { 236 | $called = false; 237 | 238 | $this->failure->forAll(function() use (&$called) { $called = true; }); 239 | 240 | $this->assertFalse($called); 241 | } 242 | } 243 | 244 | class TestException extends Exception {} 245 | -------------------------------------------------------------------------------- /src/PhpTry/Attempt.php: -------------------------------------------------------------------------------- 1 | isSuccess() ? $this->get() : $default; 42 | } 43 | 44 | /** 45 | * Returns the value if it is Success, or the the return value of the callable otherwise. 46 | * 47 | * Note: will throw if the callable throws. 48 | * 49 | * @param callable $callable 50 | * 51 | * @return mixed 52 | */ 53 | public function getOrCall($callable) 54 | { 55 | return $this->isSuccess() ? $this->get() : call_user_func($callable); 56 | } 57 | 58 | /** 59 | * Returns this Attempt if Success, or the given Attempt otherwise. 60 | * 61 | * @param Attempt $try 62 | * 63 | * @return Attempt 64 | */ 65 | public function orElse(Attempt $try) 66 | { 67 | return $this->isSuccess() ? $this : $try; 68 | } 69 | 70 | /** 71 | * Returns this Attempt if Success, or the result of the callable otherwise. 72 | * 73 | * @param callable $callable Callable returning an Attempt. 74 | * 75 | * @return Attempt 76 | */ 77 | public function orElseCall($callable) 78 | { 79 | if ($this->isSuccess()) { 80 | return $this; 81 | } 82 | 83 | try { 84 | $value = call_user_func($callable); 85 | 86 | if ( ! $value instanceof Attempt) { 87 | return new Failure(new UnexpectedValueException('Return value of callable should be an Attempt.')); 88 | } 89 | 90 | return $value; 91 | } catch (Exception $ex) { 92 | return new Failure($ex); 93 | } 94 | } 95 | 96 | /** 97 | * {@inheritDoc} 98 | */ 99 | public function getIterator() 100 | { 101 | if ($this->isSuccess()) { 102 | return new ArrayIterator(array($this->get())); 103 | } else { 104 | return new EmptyIterator(); 105 | } 106 | } 107 | 108 | /** 109 | * Its value if Success, or throws the exception if this is a Failure. 110 | * 111 | * @return mixed 112 | */ 113 | abstract public function get(); 114 | 115 | 116 | /** 117 | * Returns the given function applied to the value from this Success, or returns this if this is a Failure. 118 | * 119 | * Useful for calling Attempting another operation that might throw an 120 | * exception, while an already catched exception gets propagated. 121 | * 122 | * @param callable $callable Callable returning an Attempt. 123 | * 124 | * @return Attempt 125 | */ 126 | abstract public function flatMap($callable); 127 | 128 | /** 129 | * Maps the given function to the value from this Success, or returns this if this is a Failure. 130 | * 131 | * @param callable $callable Callable returning a value. 132 | * 133 | * @return Attempt 134 | */ 135 | abstract public function map($callable); 136 | 137 | /** 138 | * Converts this to a Failure if the predicate is not satisfied. 139 | * 140 | * @param mixed $callable Callable retuning a boolean. 141 | * 142 | * @return Attempt 143 | */ 144 | abstract public function filter($callable); 145 | 146 | /** 147 | * Applies the callable if this is a Failure, otherwise returns if this is a Success. 148 | * 149 | * Note: this is like `flatMap` for the exception. 150 | * 151 | * @param callable $callable Callable taking an exception and returning an Attempt. 152 | * 153 | * @return Attempt 154 | */ 155 | abstract public function recoverWith($callable); 156 | 157 | /** 158 | * Applies the callable if this is a Failure, otherwise returns if this is a Success. 159 | * 160 | * Note: this is like `map` for the exception. 161 | * 162 | * @param callable $callable Callable taking an exception and returning a value. 163 | * 164 | * @return Attempt 165 | */ 166 | abstract public function recover($callable); 167 | 168 | /** 169 | * Callable called when this is a Success. 170 | * 171 | * @param mixed $callable Callable taking a value. 172 | * 173 | * @return void 174 | */ 175 | abstract public function onSuccess($callable); 176 | 177 | /** 178 | * Callable called when this is a Success. 179 | * 180 | * @param mixed $callable Callable taking an exception. 181 | * 182 | * @return void 183 | */ 184 | abstract public function onFailure($callable); 185 | 186 | /** 187 | * Converts the Attempt to an Option. 188 | * 189 | * Returns 'Some' if it is Success, or 'None' if it's a Failure. 190 | * 191 | * @return \PhpOption\Option 192 | */ 193 | abstract public function toOption(); 194 | 195 | /** 196 | * Callable called when this is a Success. 197 | * 198 | * Like `map`, but without caring about the return value of the callable. 199 | * Useful for consuming the possible value of the Attempt. 200 | * 201 | * @return Attempt The current Attempt 202 | */ 203 | abstract public function forAll($callable); 204 | 205 | /** 206 | * Constructs an Attempt by calling the passed callable. 207 | * 208 | * @param callable $callable 209 | * @param array $arguments Optional arguments for the callable. 210 | * 211 | * @return Attempt 212 | */ 213 | public static function call($callable, $arguments = array()) 214 | { 215 | try { 216 | return new Success(call_user_func_array($callable, $arguments)); 217 | } catch (Exception $e) { 218 | return new Failure($e); 219 | } 220 | } 221 | 222 | /** 223 | * Constructs a LazyAttempt by calling the passed callable. 224 | * 225 | * The callable will only be called if a method on the Attempt is called. 226 | * 227 | * @param callable $callable 228 | * @param array $arguments Optional arguments for the callable. 229 | * 230 | * @return LazyAttempt 231 | */ 232 | public static function lazily($callable, $arguments = array()) 233 | { 234 | return new LazyAttempt($callable, $arguments); 235 | } 236 | } 237 | --------------------------------------------------------------------------------