Throwing a Boomerang

A boomerang is a crafty device invented by the aborigines of Australia having the useful feature of returning to the thrower when properly thrown and its target missed. Of course, if you fail to pay attention, the boomerang could very well come back and hit you. Similarly, failing to catch your exceptions and check your method return variables can also bonk you on the head. Usually the day before release. Friday at 4:45 PM. On the night your friends celebrate your birthday. Without you. We will ignore try-catch for now since most of our development is in PHP and its API is almost completely procedural. Being a procedural language, pretty much every function has a return value. Some of these are important and some can be occasionally ignored. Which are which?
<?php $bears = array('black', 'brown', 'grizzly', 'hybrid', 'polar'); // (1) No return. set_time_limit(666); // (2) Return but sometimes ignorable. reset($bears); // (3) Pointless unless return value is checked. count($bears); // (4) Ignore at your own risk! mysql_connect("localhost", "grizzlyman", "b3arsRkewl"); mysql_select_db("bear_studies"); $bearAttackSql = "INSERT INTO bear_violence (...) VALUES (...)"; mysql_query($bearAttackSql); ?> 

(2) and (4) are the interesting cases. Some functions return values which are ignorable because the value does not matter to you or the execution of the program. Other functions, as in (4), have serious side effects if they are ignored. In this case, the MySQL functions will use the last connection by default, so in a perfect world selecting the database will always work since we always know the database is always available and we never mistype anything.

Those are the easy examples. Now, what about potentially complex processes like billing systems and nuclear launch authentication? Since most of you aren't cleared to know the latter, let's quickly look at a simple billing system that expects everything to work correctly:

<?php $dbh = new DatabaseHandler($dbConnectionInfo); // 1 $cart = $_SESSION['cart']; // 2 $cartTotalValue = $cart->getTotal(); // 3 $cardHolder = new CardHolder($_POST['name'], $_POST['postalCode'], $_POST['cc_number'], $_POST['cc_exp'], $_POST['cvv2']); // 4 $paymentProcessor = PaymentProcessor::init(PAYMENT_PROCESSOR_NAME); // 5 $paymentProcessor->setCardHolder($cardHolder); // 6 $paymentProcessor->authorize($cartTotalValue); // 7 $order = new Order($dbh); // 8 $order->process($cart); // 9 $paymentProcessor->finalize(); // 10 header('Location: ' . ORDER_SUCCESSFUL_URL); // 11 ?>

So, how many places can the above code run into trouble?

  1. The database might not be available, so $dbh could be null.
  2. What if the cart is empty? $cart could be null or an error generated if the session variable is not set.
  3. The cart might not exist so you would have a method call on a non-object error.
  4. Nothing wrong here, but the object could have incorrect or blank fields.
  5. The constant PAYMENT_PROCESSOR_NAME might not be set correctly resulting in a null value or non-object.
  6. Nothing should go wrong here.
  7. The authorization might fail for any number of reasons, from invalid data, unavailable payment processor, or credit unavailable.
  8. Nothing wrong here.
  9. What happens if the order cannot be processed?
  10. Aw heck, let's just charge them. Nothing could have gone wrong before this point.
  11. And, we'll make sure they feel warm and fuzzy regardless of whether something went "DOH!".
What needs to be done to avoid these problems?
<?php $success = true; // Let's be optimistic. Plus, Boolean math works better this way. // Don't forget to initialize variables ... if ($success) { $cart = isset($_SESSION['cart']) ? $_SESSION['cart'] : null; $success = $success && is_object($cart); } if ($success) { $cartTotalValue = $cart->getTotal(); $cardHolder = new CardHolder($_POST['name'], $_POST['postalCode'], $_POST['cc_number'], $_POST['cc_exp'], $_POST['cvv2']); $paymentProcessor = PaymentProcessor::init(PAYMENT_PROCESSOR_NAME); $success = $success && !is_null($paymentProcessor); } if ($success) { $paymentProcessor->setCardHolder($cardHolder); $success = $success && $paymentProcessor->authorize($cartTotalValue); } if ($success) { $dbh = new DatabaseHandler($dbConnectionInfo); $success = $success && !is_null($dbh); $success = $success && $dbh->beginTransaction(); } if ($success) { $order = new Order($dbh); $success = $success && $order->process($cart); } $success = $success && $paymentProcessor->finalize(); if ($success) { $dbh->commitTransaction(); header('Location: ' . ORDER_SUCCESSFUL_URL); } else { $dbh->rollbackTransaction(); header('Location: ' . ORDER_FAILURE_URL); } ?>
Assuming all of the supporting functions are checking their return values, the above code should always work regardless of what goes wrong. You will probably want to handle error messages, logging, and other conveniences. You may want to refactor the code to improve usability; but, you will not be able to avoid checking your results to make sure you catch the boomerang.
Kevin McFadden

,
Posted in Article Category: #Code
on