Skip to content

Instantly share code, notes, and snippets.

@mikeschinkel
Last active March 30, 2021 10:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikeschinkel/22e2f437ae76609321c2d628717a30b3 to your computer and use it in GitHub Desktop.
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{}`
<?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