Last active
March 30, 2021 10:40
-
-
Save mikeschinkel/22e2f437ae76609321c2d628717a30b3 to your computer and use it in GitHub Desktop.
Illustrating the try-break pattern in action for PHP 8.x — hopefully to see PHP > 8.0 allow replacing `do{...}while(false};` with a naked `try{}`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* This shows our try-break example in action. | |
*/ | |
function main() { | |
do { | |
$result = example(0,"hello"); | |
if ($result->is_error() ) { | |
$result->print_error(); | |
//exit; | |
} | |
$result = example(25,""); | |
if ($result->is_error() ) { | |
$result->print_error(); | |
//exit; | |
} | |
$result = example(250,"world"); | |
if ($result->is_error() ) { | |
$result->print_error(); | |
//exit; | |
} | |
$result = example(2500,"goodbye"); | |
if ($result->is_error() ) { | |
$result->print_error(); | |
//exit; | |
} | |
} while ( false ); | |
} | |
/** | |
* This example() function illustrates the *consistent* pattern that | |
* every function longer than a few lines can and should adopt when | |
* | |
* | |
* @return IntResult | |
*/ | |
function example(int $p1, string $p2):IntResult { | |
/* | |
* This section handles return variable initialization. | |
*/ | |
$default = 100; | |
$result = new IntResult( $default ); | |
/* | |
* This begins the `try{}` construct. | |
* | |
* Currently `do{...break...}while(false);` works. | |
* Ideally `try{...break...}` will work in the future. | |
* | |
* IMPORTANT: Inside the try{} all code should be | |
* indented NO MORE than two levels; first level of | |
* the "happy path" and the second being the breaks. | |
*/ | |
do { | |
/* | |
* This section handles precondition. | |
* A precondition should be as logically simple | |
* as possible using the try-break pattern. | |
* | |
* When this section gets too long or requires | |
* nesting of another try-break then it should | |
* be split into two or more functions. | |
* | |
* Nesting MUST be avoided beyond one additional | |
* level which is used on pre-conditions for a | |
* break. | |
* | |
* When a function needs to be split the developer | |
* DOES NOT NEED TO REFACTOR LOGIC; just make a copy | |
* of the function and remove the parts from each | |
* that you want in the other. Thus the try-break | |
* pattern allows your logic to stay in-tact without | |
* requiring it to be refactored, unlike often happens | |
* when early return statements are used. | |
* | |
* WHY does try-break require less refactoring than | |
* when using early returns? Because early returns | |
* do not have the ability to share code at the end | |
* of a function so code written with early returns | |
* has to accomodate that and when split into two | |
* functions it is that aspect that must be refactored. | |
* | |
* IMPORTANT: precondition expressions should be | |
* as simple as possible. So `if($x||$y){break}` | |
* should be split into two if statements, e.g. | |
* `if($x){break}` and `if($y){break}` | |
*/ | |
if ( $p1 < 100 ) { | |
$result->message = "int parameter cannot be less than 10"; | |
break; | |
} | |
if ( empty($p2) ) { | |
$result->message = "string parameter cannot be empty"; | |
break; | |
} | |
/* | |
* With all pre-conditions cleared, run the meat of | |
* the function's code here. | |
*/ | |
$result = something_useful($p1); | |
} while(false); | |
/** | |
* This is the code run for all paths; | |
* preconditions and happy path. | |
*/ | |
$result->maybe_set_error(); | |
return $result; | |
} | |
/** | |
* This is just a placeholder function for doing something useful. | |
* | |
* If does include its own preconditions, but does | |
* @param int $p1 | |
* | |
* @return IntResult | |
*/ | |
function something_useful(int $p1): IntResult { | |
$result = new IntResult(42); | |
do { | |
if ($p1 < 1 ) { | |
$result->message = "parameter must be greater than 0"; | |
} | |
if ($p1 > 999 ) { | |
$result->message = "parameter must be less than 1000"; | |
} | |
/** | |
* Something useful would actually be done here. | |
*/ | |
} while ( false ); | |
if ( $result->has_message() ) { | |
$result->set_message("%s; its value was %d", | |
$result->message, | |
$p1); | |
} | |
return $result; | |
} | |
/* | |
* These Return* classes simulate being about to return | |
* two values with the second one being required to be | |
* of type Exception or a subclass or Exception. | |
* | |
* See https://github.com/mikeschinkel/php-optional-exceptions | |
*/ | |
class NoErrorResult extends ErrorResult {} | |
class IntResult extends ErrorResult { | |
public ?int $value; | |
function __construct(?int $value, Exception|string|null $error=null) { | |
parent::__construct($error); | |
$this->value = $value; | |
} | |
function set_message(string $message, ...$args):void{ | |
$this->message = sprintf( $message, ...$args ); | |
$this->value = null; | |
} | |
} | |
class ErrorResult { | |
public ?Exception $error; | |
public ?string $message; | |
function __construct(Exception|string|null $error=null) { | |
if ( is_string($error) ) { | |
$error = new Exception($error); | |
} | |
$this->error = $error; | |
} | |
function is_error():bool { | |
return is_null($this->error); | |
} | |
function has_message():bool { | |
return !is_null($this->message); | |
} | |
function get_message():?string { | |
return $this->is_error() | |
? $this->error->getMessage() | |
: null; | |
} | |
function print_error():?string { | |
printf( "%s\n", $this->get_message() ); | |
} | |
function maybe_set_error():void { | |
do { | |
if ( ! $this->has_message() ) { | |
break; | |
} | |
if ( ! $this->is_error() ){ | |
$this->error = new Exception( $this->message ); | |
break; | |
} | |
/** | |
* This `if{}` is not need but I leave it here to | |
* document what is actually happening. | |
*/ | |
if ( $this->is_error() ){ | |
/** | |
* Note that each level of error capture adds its | |
* own annotations — see the `; %s` in string. | |
* This greatly helps in diagnosing errors from | |
* error logs. | |
*/ | |
$this->error = new Exception( | |
sprintf("%s; %s", | |
$this->message, | |
$this->error->getMessage() | |
) | |
); | |
/** | |
* I leave the break here for consistency, | |
* much like using trailing commas, i.e. | |
* if someone adds more code after it will | |
* not have to be modified and will still | |
* work as intended. | |
*/ | |
break; | |
} | |
} while ( false ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment