Throwing a Boomerang
Kevin McFadden, Former Viget
Article Category:
Posted on
<?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?
- The database might not be available, so $dbh could be null.
- What if the cart is empty? $cart could be null or an error generated if the session variable is not set.
- The cart might not exist so you would have a method call on a non-object error.
- Nothing wrong here, but the object could have incorrect or blank fields.
- The constant PAYMENT_PROCESSOR_NAME might not be set correctly resulting in a null value or non-object.
- Nothing should go wrong here.
- The authorization might fail for any number of reasons, from invalid data, unavailable payment processor, or credit unavailable.
- Nothing wrong here.
- What happens if the order cannot be processed?
- Aw heck, let's just charge them. Nothing could have gone wrong before this point.
- And, we'll make sure they feel warm and fuzzy regardless of whether something went "DOH!".
<?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.