Skip to content

Instantly share code, notes, and snippets.

@mikeschinkel
Last active May 14, 2020 21:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mikeschinkel/fb6b7f4c59eb9f63bbe4d32ae169f999 to your computer and use it in GitHub Desktop.
Save mikeschinkel/fb6b7f4c59eb9f63bbe4d32ae169f999 to your computer and use it in GitHub Desktop.
PHP Examples that could benefit from Try...Break (method excerpts from real production code)

Making PHP error handling robust

This Gist covers example from production code that show how using do{...}while(false); and make it much easier to write robust code with PHP.

Background

Over the past several years I have been using a construct when writing PHP code that allows me to use guard clauses more effectively than I was every able to with early return.

Avoiding early returns was the original catalyst

I originally started using it to avoid issues with using XDEBUG and where many early returns in a function required finding and setting breakpoints on them all to ensure the debugger would stop before that function existed.

However, since using it I found other much greater benefits that I had not anticipated. See section after the example.

Example of this Pattern

That construct is as illustrated with the following example:

function example() {
    do { 
        $value = DEFAULT;
        if ( ! $foo ) {
             break;
        }
        if ( ! $bar ) {
             break;
        }
        if ( $baz ) {
             break;
        }
        $value = do_real_work_here();
    } while (false);
    if ( is_null($value) ) { 
        do_common_stuff_here();
    }
    return $value;
}

I have found it to be extremely usable, even to the point when I try not to use it I find myself reverting to it.

Benefits

The benefits I have discovered when using this pattern are:

  1. It allows for significant consistency for functions that need guard clauses.
  2. It is easy to document in a standards doc,
  3. It is easy to apply, and
  4. It is easy to code review.
  5. It naturally encourages the writing more guard clauses because it makes writing them is so easy; you rarely ever have to think about the logic.
  6. It makes it more obvious when a function should be refactored into multiple functions; whenever you feel the need to use multiple nested do{...}while(false); constructs you have identified a need to refactor your logic into two functions instead of just one.
  7. It makes the refactoring extremely easy — with very little tedious recoding — unlike I had found when using early returns.

Best Practices

My "best practices" for using this pattern are as follows:

  1. Follow the Happy Path. Avoid nesting as much as possible,
  2. Simplify complex guard clauses expressions into multiple guard clauses, if possible,
  3. Make the nested block of code in your guard clause as short as possible, ideally just break
  4. Don't use naked if () break as it disrupts the visual pattern of guard clauses
  5. Don't nest or duplicate do{...}while(false); move to their own methods.
  6. Always initialize the return value variable at the top of the do {
  7. Never return early
  8. Never throw
  9. Always use break to exit early
  10. If the last guard clause is an if/else scenario, nest the shorter scenario in the if(...), especiallg if a break.
  11. Always capture thrown errors and restructure using break.
  12. "Handle" an error as soon as you recognize it, i.e. write to a log and then break.
  13. Ideally never handle an error more than once.
  14. Always return the return value via its variable or a referencing expression on the last line.
  15. Invert single AND logic into a do{...}while(false) block of multiple if(...) statements, when it simplifies code.
  16. Use these same best practices where applicable with for*{...} loops and continue

An example for refactoring

See UiElements.php below for a perfect example of a method that really should be broken in two because of the need for two different do{...}while(false); clauses.

Unfortunate aspects

However, there are several things I really dislike about this pattern:

  1. The need to always specify while (false) when it is really just visual noise for this use-case, and
  2. Developers not using an IDE with a feature like Live Templates in PhpStorm may accidentally type while (true) and create an endless loop they will need to later correct.
  3. It uses the do{...} while(...) for break and not for loops, and probably 99% of developers will first think of a loop when they see it in your code unless they are familiar with this pattern.

Fixing the unfortunate aspects

If PHP allowed any of the following it would address at least the first two (2) if not all unfortunate aspects of this pattern:

  1. Allowing do{...} for breaks without requiring while(...), or
  2. Allowing try{...} w/support for breaks without requiring catch(){...}, or
  3. Adding some other construct like block{...} or guard{...} or whatever that could support break and have appropriate semantics for this use-case.

Frankly this is my #1 request for PHP. If I could have this I would be (much) happy(er.)

Real World Examples

The following are real-world examples from actual production code that include code after the while(false).

Note however that this pattern is equally useful for guard clauses when when the while(false); immediately precedes the return statment.

(Being code from a production system means the code is not always perfect code and has aspects that could certainly be improved, so please avoid criticizing this code for reason other than the pattern being highlighted by this Gist.)

<?php
class Apis {
static function JsonGET( string $api_url, array $args = array() ) {
do {
$wp_error = null;
$args = wp_parse_args( $args, array(
'response_type' => ARRAY_A,
) );
$response = null;
$message = sprintf( "with request URL='%s'", $api_url );
$response = wp_remote_get( $api_url );
if ( is_wp_error( $response ) ) {
/**
* @var WP_Error $response
*/
$wp_error = $response;
$messages = implode( '; ', $response->get_error_messages() );
$message = sprintf( "%s: %s", $message, $messages );
break;
}
if ( ! $body = wp_remote_retrieve_body( $response ) ) {
$message = "response body is empty";
break;
}
$response = json_decode( $body, true, 512, JSON_UNESCAPED_SLASHES );
if ( is_null( $response ) ) {
$body = @Strings::strip_newlines( $body );
$message = sprintf( "invalid JSON returned in body: %s", $body );
break;
}
if ( $args[ 'response_type' ] === ARRAY_A && ! is_array( $response ) ) {
$body = @Strings::strip_newlines( $body );
$message = sprintf( "JSON returned in body was not an array: %s", $body );
break;
}
if ( $args[ 'response_type' ] === OBJECT && ! is_object( $response ) ) {
$body = @Strings::strip_newlines( $body );
$message = sprintf( "JSON returned in body was not an object: %s", $body );
break;
}
$message = null;
} while ( false );
if ( ! is_null( $message ) ) {
Errors::log( "API request failed for URL '%s: %s",
$api_url,
$message
);
$response = is_null($wp_error)
? new WP_Error( -1, $message )
: $wp_error;
}
return $response;
}
}
<?php
class Args {
const render_callback = 'render_callback';
static function set_object( array &$args, string $element_name, string $class_name, $default = null ) {
do {
if ( ! isset( $args[ $element_name ] ) ) {
$args[ $element_name ] = null;
break;
}
if ( is_a( $args[ $element_name ], $class_name, true ) ) {
break;
}
if ( is_subclass_of( $args[ $element_name ], $class_name, true ) ) {
break;
}
Errors::log( "element '%s' set but not of class %s; found instead: %s",
$element_name,
$class_name,
Represent::as_string( $args[ $element_name ] )
);
if ( is_null( $default ) ) {
$args[ $element_name ] = new $class_name();
break;
}
if ( is_a( $default, $class_name, true ) ) {
break;
}
if ( is_subclass_of( $default, $class_name, true ) ) {
break;
}
Errors::notify( "default not null but not of class %s; found instead: %s",
$class_name,
Represent::as_string( $args[ $default ] )
);
} while ( false );
if ( is_null( $args[ $element_name ] ) ) {
$args[ $element_name ] = is_callable( $default )
? $default()
: $default;
}
return $args;
}
}
<?php
Class Objects {
/**
* @param WP_Post $post
* @param array $args
* @return |null
*/
static function load_post( $post, array $args = array() ) {
do {
$object = $reason = null;
$post = get_post( $post );
if ( empty( $post ) ) {
$reason = 'post is empty';
break;
}
$class_name = PostTypes::get_post_type_class( $post->post_type );
if ( empty( $post ) ) {
$reason = sprintf( "post type '%s' does not have an associated PHP class",
$post->post_type
);
break;
}
$factory_class = Reflect::get_class_constant( $class_name, WellKnown::FACTORY_CLASS );
if ( empty( $post ) ) {
$reason = sprintf( "class '%s' for post type '%s' does not have a FACTORY_CLASS constant",
$class_name,
$post->post_type
);
break;
}
if ( ! method_exists( $factory_class, Methods::make_new_instance ) ) {
$reason = sprintf( "factory class '%s' for class '%s' for post type '%s' does not have a %s() method",
$factory_class,
$class_name,
$post->post_type,
Methods::make_new_instance
);
}
$args[ Instances::post] = $post;
$object = $factory_class::make_new_instance( $args );
} while ( false );
if ( empty( $object ) ) {
Errors::notify( "failed call to %s() called from %s because %s: %s",
Methods::make_new_instance,
Caller::trace( 1 ),
$reason,
$post
);
}
return $object;
}
}
<?php
class OfferingCard
function expiration_date():?string {
$value = $should_show = null;
do {
if ( ! $offering = $this->contained_offering() ) {
break;
}
$value = $offering->expiration_date();
/**
* @todo Shouldn't this be in the the_approval_number() method?
* As is there is no want to get the approval number if needed
* for some other reason that showing it. - Mike
*/
$should_show = $offering->should_show_expiration_date();
} while ( false );
return ( $value && $should_show )
? date_format( $value, 'M j, Y | h:i a e' )
: null;
}
}
<?php
class Php {
static function call_context( CallContext $call_context, bool $trigger_error = true ) {
do {
$result = null;
$exists = false;
if ( ! $call_context->can_call() ) {
break;
}
$exists = true;
$result = $call_context->call();
} while ( false );
if ( !$exists && $trigger_error ) {
Errors::notify( 'invalid callable %s() when called by %s()',
$call_context->representation(),
Caller::trace(1)
);
}
return $result;
}
}
<?php
Class Requirements {
/**
* @TODO - THIS WILL BE REPLACED WHEN BRANCH
* CL2K-241-business-objects is finished
*
* @param int $requirement_id
*
* @return Requirement
*/
static function load( int $requirement_id ): ?self {
do {
global $wpdb;
if ( $requirements = Cache::get( $requirement_id, __METHOD__ ) ) {
break;
}
$common_db = new CommonDb();
$sql = <<<SQL
# noinspection SqlResolve, SqlNoDataSourceInspectionForFile
SELECT DISTINCT
ltr.LicenseTypeRequirementId AS requirements_id,
s.StateName AS state_name,
ltr.StateCode AS state_code,
ltr.LicenseTypeId AS license_type_id,
p.ProfessionId AS profession_id,
p.ProfessionName AS profession_name
FROM
{$common_db->license_type_requirement} ltr
JOIN {$common_db->state} s ON s.StateId = ltr.StateId
JOIN {$common_db->license_type} lt ON ltr.LicenseTypeId = lt.LicenseTypeId
JOIN {$common_db->profession} p ON p.ProfessionId = lt.ProfessionId
WHERE
LicenseTypeRequirementId=%d
SQL;
$sql = $wpdb->prepare( $sql, intval( $requirement_id ) );
if ( is_null( $requirements = $wpdb->get_row( $sql ) ) ) {
$requirements = array();
}
$requirements = new Requirements( $requirement_id, (array) $requirements );
Cache::set( $requirement_id, $requirements, __METHOD__ );
} while ( false );
return ! is_null( $requirements )
? $requirements
: new Requirements( 0 );
}
}
<?php
class Subsidiary {
/**
* @return string
*/
function slug(): string {
do {
if ( ! empty( $slug = $this->IntIdKeyed_slug() ) ) {
break;
}
$slug = $this->make_slug();
$this->IntIdKeyed_set_slug( $slug );
} while ( false );
return ( $slug );
}
/**
* Create a value for slug.
*
* May change based on which fields are updated in Admin or Netsuite
* But we have to guess at it, and cannot use a field if empty.
*
* @todo Would be best to have StudentAPI return a slug
* value that internal users can set explicitly.
*
* @return string
*/
function make_slug(): string {
do {
if ( ! empty( $slug = $this->_siteURL ) ) {
break;
}
if ( ! empty( $slug = $this->IntIdKeyed_slug() ) ) {
break;
}
if ( ! empty( $slug = $this->_SubsidiaryDescription ) ) {
break;
}
if ( ! empty( $slug = $this->_BVPreffix ) ) {
break;
}
Errors::log('invalid slug value for subsidiary %d', $this->id() );
$slug = 'anonymous';
} while ( false );
$slug = preg_replace( '#^(https?://)?(www\.)?(.+)\.com$#', '$3', $slug );
return sanitize_title_with_dashes( $slug );
}
}
<?php
class UiElements {
static function register_element( $param1, $param2 = null ) {
do {
/**
* @var string|null $identifier
*/
$identifier = null;
/**
* @var callable|null $callable
*/
$callable = null;
/**
* @var Interfaces\Registerable|null $registerable
*/
$registerable = null;
if ( is_callable( $param1 ) ) {
$callable = $param1;
break;
}
if ( is_object( $param1 ) ) {
$registerable = $param1;
break;
}
if ( is_string( $param1 ) ) {
$identifier = $param1;
}
if ( is_callable( $param2 ) ) {
$callable = $param2;
break;
}
if ( is_object( $param2 ) ) {
$registerable = $param2;
break;
}
if ( ! is_string( $param2 ) ) {
break;
}
if ( ! class_exists( $param2 ) ) {
Errors::log('%s is not a valid class and cannot be used to register %s',
$param2,
Represent::as_string($param1)
);
break;
}
$callable = function() use ($param2) {
return new $param2();
};
} while ( false );
do {
if ( $callable ) {
self::_register_callable( $identifier, $callable );
break;
}
if ( ! $registerable ) {
Errors::trigger( 'invalid registration; neither `callable` nor %s provided',
Interfaces\Registerable::class
);
break;
}
if ( ! $registerable instanceof Interfaces\Registerable ) {
Errors::trigger( 'invalid registration; type %s provided. Must be type %s instead: %s',
gettype( $registerable ),
Interfaces\Registerable::class,
Represent::as_string( $registerable )
);
break;
}
if ( ! is_null( $identifier ) ) {
$registerable->set_identifier( $identifier );
}
self::_register_instance( $registerable );
} while ( false );
}
}
<?php
class YouTube implements Serializable {
use Traits\Serializable;
const KEY_PREFIX = 'YT'; // Should be short
/**
* @param string $video_id
*
* @return Listing\Item
*/
/**
* @const int — 24 hours
*/
const CACHE_DURATION = 24 * 60 * 60;
static function fetch_item( string $video_id ): ?Videos\Listing\Item {
do {
$response = null;
$api_url = Videos\Api::get_url( [ 'id' => $video_id ] );
$transient_key = Transients\TransientKey::make_new( self::KEY_PREFIX, $api_url );
/*if ( $video = Transients\Transients::get( $transient_key ) ) {
break;
}*/
$video = null;
$response = Apis::JsonGET( $api_url );
if ( is_wp_error( $response ) ) {
break;
}
if ( isset( $response[ 'error' ] ) ) {
break;
}
$video_list = new Videos\Listing\Response( $response );
if ( ! $video_list->has_items() ) {
break;
}
$video = $video_list->get_item( 0 );
if ( false === $video ) {
$video = null;
break;
}
Transients\Transients::set( $transient_key, $video, self::CACHE_DURATION );
} while ( false );
if ( is_null( $video ) ) {
Errors::log( "error when calling YouTube API '%s': %s",
$api_url,
$response
);
}
return $video;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment