Forms and Views | Sessions

This version, however, is not optimal, because most of the time the contents will be quite large, and will have to be output to a buffer, transformed to a string, then output back to the buffer, and finally sent to the user. If, instead of passing the contents as a string, we passed the function that generates the contents (such as LoginView::RenderContents()) and called it, the contents would be output directly to the correct location in the buffer, and would require no copying.

This is actually a common element in many programming projects: only perform a certain task when it really becomes necessary, to minimize unnecessary work. We’ve already seen this at work:

  • The autoload system which delays the loading of a class until it actually becomes necessary.
  • The .htaccess rules for files only run the files controller for a file if it is requested (and it can then be written to the file cache to avoid recomputing it).
  • The DomainConfig::Domain() and DomainConfig::Prefix() only compute the domain and prefix when they are used (and cache the result for later reuse).

The generic name for that is lazy evaluation: only compute a value when you need it (as opposed to eager evaluation, which computes a value when it’s defined.
I will take advantage of this situation to add a quick refresher about PHP functions and calling functions. First, a function is defined in one of two ways:

function foo($x) { return $x + $x; }
$bar = create_function('$x', 'return $x + $x;');

The first approach defines a function with the name ‘foo’, while the second approach defines a function with a randomly chosen name (to avoid colliding with any previously defined function) that is then stored in a variable. Once you have the name of a function, you can call it either directly (name followed by arguments) or indirectly (store name in variable, variable followed by arguments):

echo foo(1);
echo $bar(2);
$qux = 'foo';
echo $foo(3);

This does not apply to static functions. Obj::Func() calls the function Func of class Obj, not the function Obj::Func, so you cannot just assign ‘Obj::Func’ to a variable and use the $func() syntax. Even worse, it also does not apply to methods, since when calling a method you need to specify the object you want to call the method on. The good news is that (almost) all of these details have been handled for us by the function call_user_func_array.

So, we construct an object which represents a delayed function call. The constructor call will look like one of these:

  • new LazyObj(‘frobnicate’, 2, ‘foo’) is going to call frobnicate(2, ‘foo’). This is supported by call_user_func_array.
  • new LazyObj(‘class::frobnicate’, 2, ‘foo’) is going to call class::frobnicate(2,’foo’). This is not supported by call_user_func_array, so there will be a small bit of hacking to make sure this works.
  • new LazyObj(array($obj, ‘frobnicate’), 2, ‘foo’) is going to call $obj->frobnicate(2,’foo’). This is supported by call_user_func_array.
  • new LazyObj(LazyObj::value, 3.141592); is going to return 3.141592 without calling anything.

In addition to that, the lazy object should determine on its own, when it is created, whether the call is going to be correct (that is, it should check that the function/method exists). This is slightly underperforming, because if we call a static function this way, the class will have to be loaded in order for the check to be performed, but this is something I can live with (and besides, we can put the check in an assert() and disable assertions to improve performance on production platforms).

There will be two value-getting function: a function that only computes the value if necessary (for performance reasons, the value will be stored internally) and a function that always recomputes the value (in case the value changes every call, or we are interested in what the function does instead of what it returns, as in our PageView example).

The code looks like this:

<?php // objects/lazy.php

  class LazyObj
  {
      private $_executed = false;
      private $_value = null;
      private $_function = null;
      private $_arguments = null;

      const value = 0;

      public function __construct()
      {
          $this->_arguments = func_get_args();
          assert(count($this->_arguments) > 0);

          $func = array_shift($this->_arguments);

          // Is this a standalone function?
          if (is_string($func) && is_callable($func)) {
              $this->_function = $func;
              return;
          }

          // Is this a static function?
          if (is_string($func) && strstr($func, '::')) {
              $func = explode('::', $func, 2);
              assert(is_callable($func));
              $this->_function = $func;
              return;
          }

          // Is this a method?
          if (is_array($func) && count($func) == 2 && 
              is_object($func[0]) && is_string($func[1]) &&
              method_exists($func[0], $func[1]))
          {
              $this->_function = $func;
              return;
          }

          // Is this a constant?
          if ($func === LazyObj::value) {
              $this->_executed = true;
              $this->_value = $this->_arguments;

              if (count($this->_value) == 1)
                  $this->_value = $this->_value[0];

              return;
          }

          // Oops, could not find out how to call this...
          assert(false);
      }

      // Return value, execute only if necessary
      function get()
      {
          if (!$this->_executed)
              return $this->execute();
          else
              return $this->_value;
      }

      // Return value, always execute
      function execute()
      {
          $this->_executed = true;
          if ($this->_function)
              $this->_value = call_user_func_array($this->_function, 
                                                   $this->_arguments);
          return $this->_value;
      }
  }

Quite verbose, but it gets the job done. Notice how all the checks based on is_callable and method_exists are performed.

exclamationIn PHP, null is a special value conventionally used to denote the absence of a value. Note that it is not the absence of a value, merely a representation, so that if you run isset($array[$key]) what happens is that PHP looks at the array, extracts the key and, if it doesn’t exist, evaluates the subscript operation to null while throwing a low-level warning. This is why in classic PHP environments this will work as expected, but in paranoid PHP environments (like our own) it will instead fail with a “key not in array” error message. Use array_key_exists instead! The actual point of this aside is: why use both $this -> _value and $this -> _executed, instead of using merely $this -> _value as setting it to null until the function is executed? The answer is: because null cannot represent the absence of a value here. How can you distinguish the “function has not been called yet” from the “function has been called and has returned null” case? Since PHP does not have a clean option type like ML languages, you have to use a second variable as a crutch.

So, using this code, we can now rewrite the page template:

<?php

  class PlainPageView
  {
      public static function Render($title, $class, LazyObj $contents)
      {
          $title = htmlspecialchars($title);
          $class = htmlspecialchars($class);
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><?php
?><html><head><title><?=$title?></title></head><?php
?><body class="<?=$class?>"><?php
          $contents->execute();
?></body></html><?php
      }
  }
exclamationThere’s an unusual ‘LazyObj’ mention in the parameters now. If you were not familiar with this feature yet, now is a good time to do so: this is called type hinting, and lets you specify that a certain parameter is always an instance of a class (or interface), and takes inheritance into account as well. If you make a silly mistake later on and provide a bad parameter, you will get a clean “incorrect parameter type” error instead of something going bust in the function code itself.

I have also added a new argument I did not mention yet: the class of the body. This will be used by our stylesheet to add page-specific effects while keeping one single, gzipped, cached stylesheet for the entire website (and, arguably, moving all the per-page effects details from our controllers to the stylesheet, which clears the code).

Which leads us to the definition of the actual login page view:

<?php // views/loginpage.php

  class LoginPageView
  {
      // Values : login_mail, signup_mail, signup_name
      // Errors : Values + login_pass, signup_pass, signup_pass2
      public static function Render(array $values, array $errors)
      {
          $lazy = new LazyObj('LoginPageView::RenderContent', $values, $errors);
          PlainPageView::Render("Login", 'login-page', $lazy);
      }

      public static function RenderLoginForm(array $values, array $errors)
      {
          $C = new TwoColFormView();

          $login_url = DomainConfig::Url('/do-login');

          $reset_url DomainConfig::Url('/reset-password',
                                         array('for' => $values['login_mail']));

          $lostpass = sprintf('<a href="%s">%s</a>',
                              $reset_url, 'Lost your password?');

          $login_fields = array(
            $C->OneCell  ('<h2>Log in</h2>'),
            $C->Error    ($errors['login_mail']),             
            $C->Input    ('mail', 'E-mail:', $values['login_mail']), 
            $C->Error    ($errors['login_pass']), 
            $C->Password ('pass', 'Password:'), 
            $C->Submit   ('Let me in!'),
            $C->TwoCell  ('', $lostpass)
          );

          $C->Render($login_url, 'login-form', $login_fields);

          unset($C);
      }

      public static function RenderSignupForm(array $values, array $errors)
      {
          $C = new TwoColFormView();

          $signup_url = DomainConfig::Url('/do-signup');

          $signup_fields = array(
            $C->OneCell  ('<h2>Sign Up</h2>'), 
            $C->Error    ($errors['signup_mail']),
            $C->Input    ('mail', 'E-mail:', $values['signup_mail']),
            $C->Error    ($errors['signup_name']), 
            $C->Input    ('name', 'Display name:', $values['signup_name']), 
            $C->Error    ($errors['signup_pass']),
            $C->Password ('pass', 'Password:'), 
            $C->Error    ($errors['signup_pass2']), 
            $C->Password ('pass2', '(again)'), 
            $C->Submit   ('Sign me up!')
          );

          $C->Render($signup_url, 'signup-form', $signup_fields);
      }

      public static function RenderContent(array $values, array $errors)
      {
          self::RenderLoginForm($values, $errors);
          self::RenderSignupForm($values, $errors);
      }
  }

While just displaying two simple forms would have required no input data, here we are requiring two arrays from the user. What for? The first array is an elementary usability requirement: if you make an error when filling the form, you don’t want all your data to go away if you submit it. While critical information, like passwords, is not kept, non-critical information such as e-mail addresses or display names is kept as-is and reinjected into the forms by means of the $values array.

The second array is a requirement for displaying errors: each cell in the array matches the name of a field that can contain an error (invalid address, display name already taken, password too short, password does not match, and so on). Since the TwoColFormView::Error() function returns null if no error is provided, no error will appear in the form if none exists.

The ‘array’ type hint lets us check that the argument is an array, but not that all expected values are present. Depending on how safe you want your code to be:

  • Disable all checks. Forgetting a field in an array is equivalent to making that field null. Useful if you work alone and have a clear idea of what you want to do, but dangerous on a team (as providing the bad name for a certain value ends up with that value being null, so you have to test everything thoroughly).
  • Use the above approach. Forgetting a field in an array causes a runtime error (because ‘undefined index’ is a level 8 warning and our error handler aborts when it encounters one), which makes it a bit more of a hassle (no more isset($array[$key])) but has the benefit that you don’t need to test your pages manually, you can simply run a spider batch that visits all pages and determines if an error happened on one.
  • Use a class: this lets the type hinting detect the error for you in exactly the same way as the approach above, but it can do so earlier (you get undefined index warnings when the array is used, possibly ten function calls down the function stack from where you actually made a mistake, and possibly not every time either) and also documents the expected fields through a clean class definition. The downside is that it takes longer to write and requires creating a brand new class for every set of arguments you wish to pass.

Choose your own poison—I use the above method for data that is very local, and the third method for data that travels a lot.

Forms and Views | Sessions

0 Responses to “7. Lazy Objects”


  1. No Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



1208 feed subscribers
(readers who polled a feed this week)