Assert the yummyness of your cake

Posted by Felix Geisendörfer, on Oct 22, 2007 - in PHP & CakePHP » Core & Hacking

Hey folks,

if you liked my last post on how to use PHP5 Exceptions in CakePHP, then here is a little addition to it:

When reading over my code, I saw that I had a lot of statements like this in there:

php
  1. $user = $Session->read('User');
  2. if (empty($user)) {
  3.    throw new AppException();
  4. }

Now there is nothing really wrong with that. However, I suddenly remembered what assertions are and how they could make my code more readable, while making it shorter. I took a quick look at PHP's assert function, but decided that I don't like it. For one because it takes a string value as a parameter that is than eval()'d and also because there is no way to "catch" failed assertions. Now don't get me wrong, I don't think you should normally catch either assertion or exceptions, as they are not meant for flow control. However, I hate limiting myself. The ability to quickly *hack* a piece of software without having to modify any of its actual code is something I really enjoy. So what I ended up doing was basically to create a class called Assert, and abuse it as a name-space for a whole bunch of useful assertion functions. If an assertion succeeds, it simply returns true. If it doesn't, it throws an AppException which will render a nice page to the user telling him what a crappy programmer I am : ).

But enough talk, lets look at how the above code could be re-factored using assertions.

php
  1. $user = $Session->read('User');
  2. Assert::notEmpty($user);

Or another example, this time rendering a 404 error page if the assertion fails.

php
  1. $this->Task->set('id', $id);
  2. if (!$this->Task->exists()) {
  3.    throw new AppException('404');
  4. }
  5.  
  6. // turns into
  7. $this->Task->set('id', $id);
  8. Assert::true($this->Task->exists(), '404');

Ok, ok ... here is the actual code that you'll need to try it out:

php
  1. /**
  2.  * undocumented class
  3.  *
  4.  * @package default
  5.  * @access public
  6.  */
  7. class Assert{
  8. /**
  9.  * undocumented function
  10.  *
  11.  * @param unknown $a
  12.  * @param unknown $b
  13.  * @param unknown $info
  14.  * @param unknown $strict
  15.  * @return void
  16.  * @access public
  17.  */
  18.    static function test($val, $expected, $info = array(), $strict = true) {
  19.       $success = ($strict)
  20.          ? $val === $expected
  21.          : $val == $expected;
  22.  
  23.       if ($success) {
  24.          return true;
  25.       }
  26.  
  27.       $calls = debug_backtrace();
  28.       foreach ($calls as $call) {
  29.          if ($call['file'] !== __FILE__) {
  30.             $assertCall = $call;
  31.             break;
  32.          }
  33.       }
  34.       $triggerCall = current($calls);
  35.       $type = Inflector::underscore($assertCall['function']);
  36.  
  37.       if (is_string($info)) {
  38.          $info = array('type' => $info);
  39.       }
  40.  
  41.       $info = am(array(
  42.          'file' => $assertCall['file']
  43.          , 'line' => $assertCall['line']
  44.          , 'function' => $triggerCall['class'].'::'.$triggerCall['function']
  45.          , 'assertType' => $type
  46.          , 'val' => $val
  47.          , 'expected' => $expected
  48.       ), $info);
  49.       throw new AppException($info);
  50.    }
  51. /**
  52.  * undocumented function
  53.  *
  54.  * @param unknown $val
  55.  * @return void
  56.  * @access public
  57.  */
  58.    static function true($val, $info = array()) {
  59.       return Assert::test($val, true, $info);
  60.    }
  61. /**
  62.  * undocumented function
  63.  *
  64.  * @param unknown $val
  65.  * @param unknown $info
  66.  * @return void
  67.  * @access public
  68.  */
  69.    static function false($val, $info = array()) {
  70.       return Assert::test($val, false, $info);
  71.    }
  72. /**
  73.  * undocumented function
  74.  *
  75.  * @param unknown $a
  76.  * @param unknown $b
  77.  * @param unknown $info
  78.  * @return void
  79.  * @access public
  80.  */
  81.    static function equal($a, $b, $info = array()) {
  82.       return Assert::test($a, $b, $info, false);
  83.    }
  84. /**
  85.  * undocumented function
  86.  *
  87.  * @param unknown $a
  88.  * @param unknown $b
  89.  * @param unknown $info
  90.  * @return void
  91.  * @access public
  92.  */
  93.    static function identical($a, $b, $info = array()) {
  94.       return Assert::test($a, $b, $info, true);
  95.    }
  96. /**
  97.  * undocumented function
  98.  *
  99.  * @return void
  100.  * @access public
  101.  */
  102.    static function pattern($pattern, $val, $info = array()) {
  103.       return Assert::test(preg_match($pattern, $val), true, am(array('pattern' => $pattern), $info));
  104.    }
  105. /**
  106.  * undocumented function
  107.  *
  108.  * @param unknown $val
  109.  * @param unknown $info
  110.  * @return void
  111.  * @access public
  112.  */
  113.    static function isEmpty($val, $info = array()) {
  114.       return Assert::test(empty($val), true, $info);
  115.    }
  116. /**
  117.  * undocumented function
  118.  *
  119.  * @param unknown $val
  120.  * @param unknown $info
  121.  * @return void
  122.  * @access public
  123.  */  
  124.    static function notEmpty($val, $info = array()) {
  125.       return Assert::test(empty($val), false, $info);
  126.    }
  127. /**
  128.  * undocumented function
  129.  *
  130.  * @param unknown $val
  131.  * @param unknown $info
  132.  * @return void
  133.  * @access public
  134.  */
  135.    static function isNumeric($val, $info = array()) {
  136.       return Assert::test(is_numeric($val), true, $info);
  137.    }
  138. /**
  139.  * undocumented function
  140.  *
  141.  * @param unknown $val
  142.  * @param unknown $info
  143.  * @return void
  144.  * @access public
  145.  */
  146.    static function notNumeric($val, $info = array()) {
  147.       return Assert::test(is_numeric($val), false, $info);
  148.    }
  149. /**
  150.  * undocumented function
  151.  *
  152.  * @param unknown $val
  153.  * @param unknown $info
  154.  * @return void
  155.  * @access public
  156.  */
  157.    static function isInteger($val, $info = array()) {
  158.       return Assert::test(is_int($val), true, $info);
  159.    }
  160. /**
  161.  * undocumented function
  162.  *
  163.  * @param unknown $val
  164.  * @param unknown $info
  165.  * @return void
  166.  * @access public
  167.  */
  168.    static function notInteger($val, $info = array()) {
  169.       return Assert::test(is_int($val), false, $info);
  170.    }
  171. /**
  172.  * undocumented function
  173.  *
  174.  * @return void
  175.  * @access public
  176.  */
  177.    static function isIntegerish($val, $info = array()) {
  178.       return Assert::test(is_int($val) || ctype_digit($val), true, $info);
  179.    }
  180. /**
  181.  * undocumented function
  182.  *
  183.  * @return void
  184.  * @access public
  185.  */
  186.    static function notIntegerish($val, $info = array()) {
  187.       return Assert::test(is_int($val) || ctype_digit($val), false, $info);
  188.    }
  189. /**
  190.  * undocumented function
  191.  *
  192.  * @param unknown $val
  193.  * @param unknown $info
  194.  * @return void
  195.  * @access public
  196.  */
  197.    static function isObject($val, $info = array()) {
  198.       return Assert::test(is_object($val), true, $info);
  199.    }
  200. /**
  201.  * undocumented function
  202.  *
  203.  * @param unknown $val
  204.  * @param unknown $info
  205.  * @return void
  206.  * @access public
  207.  */
  208.    static function notObject($val, $info = array()) {
  209.       return Assert::test(is_object($val), false, $info);
  210.    }
  211. /**
  212.  * undocumented function
  213.  *
  214.  * @param unknown $val
  215.  * @param unknown $info
  216.  * @return void
  217.  * @access public
  218.  */
  219.    static function isBoolean($val, $info = array()) {
  220.       return Assert::test(is_bool($val), true, $info);
  221.    }
  222. /**
  223.  * undocumented function
  224.  *
  225.  * @param unknown $val
  226.  * @param unknown $info
  227.  * @return void
  228.  * @access public
  229.  */
  230.    static function notBoolean($val, $info = array()) {
  231.       return Assert::test(is_bool($val), false, $info);
  232.    }
  233. /**
  234.  * undocumented function
  235.  *
  236.  * @param unknown $val
  237.  * @param unknown $info
  238.  * @return void
  239.  * @access public
  240.  */
  241.    static function isString($val, $info = array()) {
  242.       return Assert::test(is_string($val), true, $info);
  243.    }
  244. /**
  245.  * undocumented function
  246.  *
  247.  * @param unknown $val
  248.  * @param unknown $info
  249.  * @return void
  250.  * @access public
  251.  */
  252.    static function notString($val, $info = array()) {
  253.       return Assert::test(is_string($val), false, $info);
  254.    }
  255. /**
  256.  * undocumented function
  257.  *
  258.  * @param unknown $val
  259.  * @param unknown $info
  260.  * @return void
  261.  * @access public
  262.  */
  263.    static function isArray($val, $info = array()) {
  264.       return Assert::test(is_array($val), true, $info);
  265.    }
  266. /**
  267.  * undocumented function
  268.  *
  269.  * @param unknown $val
  270.  * @param unknown $info
  271.  * @return void
  272.  * @access public
  273.  */
  274.    static function notArray($val, $info = array()) {
  275.       return Assert::test(is_array($val), false, $info);
  276.    }
  277.  
  278. }
  279.  

This is probably still missing a couple kinds of assertions, I'll add whatever ones you guys think are needed to this. I'm also interested in what you guys think about this stuff in genera. I mean are there any opinions on whether you think applications should crash hard if something unexpected happens, or would you promote the idea of trying to recover from failures by providing default values and things as much as possible?

-- Felix Geisendörfer aka the_undefined

Print this Post | Digg This | Stumble It | Delicious

7 Comments

Grant Cox on Oct 22, 2007:

Very cool Felix.

I must admit seeing "assert" in non-test code is a little strange for me, but the overall solution is very elegant.

PHPDeveloper.org on Oct 25, 2007:

Felix Geisendorfer's Blog: Assert the yummyness of your cake...

...

[...] Felix Geisendorfer has posted an addition to his previous look at exceptions in CakePHP with a modification that uses assertions instead of an if to check the value of a variable. I suddenly remembered what assertions are and how they could make my code more readable, while making it shorter. I took a quick look at PHP’s assert function, but decided that I don’t like it. […] What I ended up doing was basically to create a class called Assert, and abuse it as a name-space for a whole bunch of useful assertion functions. [...]

[...] Assert the yummyness of your cake. Exceptions in CakePHP Posted in October 25th, 2007 by admin in PHP, Programming Felix Geisendorfer has posted an addition to his previous look at exceptions in CakePHP with a modification that uses assertions instead of an if to check the value of a variable. Hey folks, [...]

[...] Then you're in for a surprise. Because as of revision 5895 Model::save() now returns Model::data on success if its not empty. Now most of us do not use strict comparison for checking the return value of Model::save(), but I was stupid enough to do it as part of my new "fail hard fast" strategy : ). So suddenly I had stuff blowing up all over the place. [...]

Peter Goodman on Jun 19, 2008:

PHP's assert() doesn't actually need to take a string argument, that's just for when you have debugging turned off with the assert ini options so that the code in the asserts isn't needlessly executed.

Although I don't quite agree with the 'Assert' name, the implementation is elegant. It definitely lets you remove some annoying/redundant code.

Tim Koschützki on Jul 18, 2008:

Peter Goodman: What name would you propose instead? We are planning on releasing our Exception handler that makes use of the Asserter and are therefore open to name suggestions.

Add a comment