diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php
index 0342982a1..e252b49b0 100644
--- a/src/infrastructure/storage/lisk/LiskDAO.php
+++ b/src/infrastructure/storage/lisk/LiskDAO.php
@@ -1,1770 +1,1780 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * Simple object-authoritative data access object that makes it easy to build
  * stuff that you need to save to a database. Basically, it means that the
  * amount of boilerplate code (and, particularly, boilerplate SQL) you need
  * to write is greatly reduced.
  *
  * Lisk makes it fairly easy to build something quickly and end up with
  * reasonably high-quality code when you're done (e.g., getters and setters,
  * objects, transactions, reasonably structured OO code). It's also very thin:
  * you can break past it and use MySQL and other lower-level tools when you
  * need to in those couple of cases where it doesn't handle your workflow
  * gracefully.
  *
  * However, Lisk won't scale past one database and lacks many of the features
  * of modern DAOs like Hibernate: for instance, it does not support joins or
  * polymorphic storage.
  *
  * This means that Lisk is well-suited for tools like Differential, but often a
  * poor choice elsewhere. And it is strictly unsuitable for many projects.
  *
  * Lisk's model is object-authoritative: the PHP class definition is the
  * master authority for what the object looks like.
  *
  * =Building New Objects=
  *
  * To create new Lisk objects, extend @{class:LiskDAO} and implement
  * @{method:establishLiveConnection}. It should return an
  * @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your
  * objects.
  *
  *   class Dog extends LiskDAO {
  *
  *     protected $name;
  *     protected $breed;
  *
  *     public function establishLiveConnection() {
  *       return $some_connection_object;
  *     }
  *   }
  *
  * Now, you should create your table:
  *
  *   lang=sql
  *   CREATE TABLE dog (
  *     id int unsigned not null auto_increment primary key,
  *     name varchar(32) not null,
  *     breed varchar(32) not null,
  *     dateCreated int unsigned not null,
  *     dateModified int unsigned not null
  *   );
  *
  * For each property in your class, add a column with the same name to the table
  * (see @{method:getConfiguration} for information about changing this mapping).
  * Additionally, you should create the three columns `id`,  `dateCreated` and
  * `dateModified`. Lisk will automatically manage these, using them to implement
  * autoincrement IDs and timestamps. If you do not want to use these features,
  * see @{method:getConfiguration} for information on disabling them. At a bare
  * minimum, you must normally have an `id` column which is a primary or unique
  * key with a numeric type, although you can change its name by overriding
  * @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to
  * return null. Note that many methods rely on a single-part primary key and
  * will no longer work (they will throw) if you disable it.
  *
  * As you add more properties to your class in the future, remember to add them
  * to the database table as well.
  *
  * Lisk will now automatically handle these operations: getting and setting
  * properties, saving objects, loading individual objects, loading groups
  * of objects, updating objects, managing IDs, updating timestamps whenever
  * an object is created or modified, and some additional specialized
  * operations.
  *
  * = Creating, Retrieving, Updating, and Deleting =
  *
  * To create and persist a Lisk object, use @{method:save}:
  *
  *   $dog = id(new Dog())
  *     ->setName('Sawyer')
  *     ->setBreed('Pug')
  *     ->save();
  *
  * Note that **Lisk automatically builds getters and setters for all of your
  * object's protected properties** via @{method:__call}. If you want to add
  * custom behavior to your getters or setters, you can do so by overriding the
  * @{method:readField} and @{method:writeField} methods.
  *
  * Calling @{method:save} will persist the object to the database. After calling
  * @{method:save}, you can call @{method:getID} to retrieve the object's ID.
  *
  * To load objects by ID, use the @{method:load} method:
  *
  *   $dog = id(new Dog())->load($id);
  *
  * This will load the Dog record with ID $id into $dog, or ##null## if no such
  * record exists (@{method:load} is an instance method rather than a static
  * method because PHP does not support late static binding, at least until PHP
  * 5.3).
  *
  * To update an object, change its properties and save it:
  *
  *   $dog->setBreed('Lab')->save();
  *
  * To delete an object, call @{method:delete}:
  *
  *   $dog->delete();
  *
  * That's Lisk CRUD in a nutshell.
  *
  * = Queries =
  *
  * Often, you want to load a bunch of objects, or execute a more specialized
  * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:
  *
  *   $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
  *   $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
  *
  * These methods work like @{function@libphutil:queryfx}, but only take half of
  * a query (the part after the WHERE keyword). Lisk will handle the connection,
  * columns, and object construction; you are responsible for the rest of it.
  * @{method:loadAllWhere} returns a list of objects, while
  * @{method:loadOneWhere} returns a single object (or `null`).
  *
  * There's also a @{method:loadRelatives} method which helps to prevent the 1+N
  * queries problem.
  *
  * = Managing Transactions =
  *
  * Lisk uses a transaction stack, so code does not generally need to be aware
  * of the transactional state of objects to implement correct transaction
  * semantics:
  *
  *   $obj->openTransaction();
  *     $obj->save();
  *     $other->save();
  *     // ...
  *     $other->openTransaction();
  *       $other->save();
  *       $another->save();
  *     if ($some_condition) {
  *       $other->saveTransaction();
  *     } else {
  *       $other->killTransaction();
  *     }
  *     // ...
  *   $obj->saveTransaction();
  *
  * Assuming ##$obj##, ##$other## and ##$another## live on the same database,
  * this code will work correctly by establishing savepoints.
  *
  * Selects whose data are used later in the transaction should be included in
  * @{method:beginReadLocking} or @{method:beginWriteLocking} block.
  *
  * @task   conn    Managing Connections
  * @task   config  Configuring Lisk
  * @task   load    Loading Objects
  * @task   info    Examining Objects
  * @task   save    Writing Objects
  * @task   hook    Hooks and Callbacks
  * @task   util    Utilities
  * @task   xaction Managing Transactions
  * @task   isolate Isolation for Unit Testing
  *
  * @group storage
  */
 abstract class LiskDAO {
 
   const CONFIG_IDS                  = 'id-mechanism';
   const CONFIG_TIMESTAMPS           = 'timestamps';
   const CONFIG_AUX_PHID             = 'auxiliary-phid';
   const CONFIG_SERIALIZATION        = 'col-serialization';
   const CONFIG_PARTIAL_OBJECTS      = 'partial-objects';
 
   const SERIALIZATION_NONE          = 'id';
   const SERIALIZATION_JSON          = 'json';
   const SERIALIZATION_PHP           = 'php';
 
   const IDS_AUTOINCREMENT           = 'ids-auto';
   const IDS_PHID                    = 'ids-phid';
   const IDS_MANUAL                  = 'ids-manual';
 
   private $__dirtyFields            = array();
   private $__missingFields          = array();
   private static $processIsolationLevel       = 0;
   private static $transactionIsolationLevel   = 0;
   private static $__checkedClasses = array();
 
   private $__ephemeral = false;
 
   private static $connections       = array();
 
   private $inSet = null;
 
   protected $id;
   protected $phid;
   protected $dateCreated;
   protected $dateModified;
 
   /**
    *  Build an empty object.
    *
    *  @return obj Empty object.
    */
   public function __construct() {
     $id_key = $this->getIDKey();
     if ($id_key) {
       $this->$id_key = null;
     }
 
     if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
       $this->resetDirtyFields();
 
       $this_class = get_class($this);
       if (empty(self::$__checkedClasses[$this_class])) {
         self::$__checkedClasses = true;
 
         if (PhabricatorEnv::getEnvConfig('lisk.check_property_methods')) {
           $class = new ReflectionClass(get_class($this));
           $methods = $class->getMethods();
           $properties = $this->getProperties();
           foreach ($methods as $method) {
             $name = strtolower($method->getName());
             if (!(strncmp($name, 'get', 3) && strncmp($name, 'set', 3))) {
               $name = substr($name, 3);
               $declaring_class_name = $method->getDeclaringClass()->getName();
               if (isset($properties[$name]) &&
                   $declaring_class_name !== 'LiskDAO') {
                 throw new Exception(
                   "Cannot implement method {$method->getName()} in ".
                   "{$declaring_class_name}.");
               }
             }
           }
         }
       }
     }
   }
 
 
 /* -(  Managing Connections  )----------------------------------------------- */
 
 
   /**
    * Establish a live connection to a database service. This method should
    * return a new connection. Lisk handles connection caching and management;
    * do not perform caching deeper in the stack.
    *
    * @param string Mode, either 'r' (reading) or 'w' (reading and writing).
    * @return AphrontDatabaseConnection New database connection.
    * @task conn
    */
   abstract protected function establishLiveConnection($mode);
 
 
   /**
    * Return a namespace for this object's connections in the connection cache.
    * Generally, the database name is appropriate. Two connections are considered
    * equivalent if they have the same connection namespace and mode.
    *
    * @return string Connection namespace for cache
    * @task conn
    */
   abstract protected function getConnectionNamespace();
 
 
   /**
    * Get an existing, cached connection for this object.
    *
    * @param mode Connection mode.
    * @return AprontDatabaseConnection|null  Connection, if it exists in cache.
    * @task conn
    */
   protected function getEstablishedConnection($mode) {
     $key = $this->getConnectionNamespace().':'.$mode;
     if (isset(self::$connections[$key])) {
       return self::$connections[$key];
     }
     return null;
   }
 
 
   /**
    * Store a connection in the connection cache.
    *
    * @param mode Connection mode.
    * @param AphrontDatabaseConnection Connection to cache.
    * @return this
    * @task conn
    */
   protected function setEstablishedConnection(
     $mode,
     AphrontDatabaseConnection $connection,
     $force_unique = false) {
 
     $key = $this->getConnectionNamespace().':'.$mode;
 
     if ($force_unique) {
       $key .= ':unique';
       while (isset(self::$connections[$key])) {
         $key .= '!';
       }
     }
 
     self::$connections[$key] = $connection;
     return $this;
   }
 
 
 /* -(  Configuring Lisk  )--------------------------------------------------- */
 
 
   /**
    * Change Lisk behaviors, like ID configuration and timestamps. If you want
    * to change these behaviors, you should override this method in your child
    * class and change the options you're interested in. For example:
    *
    *   public function getConfiguration() {
    *     return array(
    *       Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
    *     ) + parent::getConfiguration();
    *   }
    *
    * The available options are:
    *
    * CONFIG_IDS
    * Lisk objects need to have a unique identifying ID. The three mechanisms
    * available for generating this ID are IDS_AUTOINCREMENT (default, assumes
    * the ID column is an autoincrement primary key), IDS_PHID (to generate a
    * unique PHID for each object) or IDS_MANUAL (you are taking full
    * responsibility for ID management).
    *
    * CONFIG_TIMESTAMPS
    * Lisk can automatically handle keeping track of a `dateCreated' and
    * `dateModified' column, which it will update when it creates or modifies
    * an object. If you don't want to do this, you may disable this option.
    * By default, this option is ON.
    *
    * CONFIG_AUX_PHID
    * This option can be enabled by being set to some truthy value. The meaning
    * of this value is defined by your PHID generation mechanism. If this option
    * is enabled, a `phid' property will be populated with a unique PHID when an
    * object is created (or if it is saved and does not currently have one). You
    * need to override generatePHID() and hook it into your PHID generation
    * mechanism for this to work. By default, this option is OFF.
    *
    * CONFIG_SERIALIZATION
    * You can optionally provide a column serialization map that will be applied
    * to values when they are written to the database. For example:
    *
    *   self::CONFIG_SERIALIZATION => array(
    *     'complex' => self::SERIALIZATION_JSON,
    *   )
    *
    * This will cause Lisk to JSON-serialize the 'complex' field before it is
    * written, and unserialize it when it is read.
    *
    * CONFIG_PARTIAL_OBJECTS
    * Sometimes, it is useful to load only some fields of an object (such as
    * when you are loading all objects of a class, but only care about a few
    * fields). Turning on this option (by setting it to a truthy value) allows
    * users of the class to create/use partial objects, but it comes with some
    * side effects: your class cannot override the setters and getters provided
    * by Lisk (use readField and writeField instead), and you should not
    * directly access or assign protected members of your class (use the getters
    * and setters).
    *
    *
    * @return dictionary  Map of configuration options to values.
    *
    * @task   config
    */
   protected function getConfiguration() {
     return array(
       self::CONFIG_IDS                      => self::IDS_AUTOINCREMENT,
       self::CONFIG_TIMESTAMPS               => true,
       self::CONFIG_PARTIAL_OBJECTS          => false,
     );
   }
 
 
   /**
    *  Determine the setting of a configuration option for this class of objects.
    *
    *  @param  const       Option name, one of the CONFIG_* constants.
    *  @return mixed       Option value, if configured (null if unavailable).
    *
    *  @task   config
    */
   public function getConfigOption($option_name) {
     static $options = null;
 
     if (!isset($options)) {
       $options = $this->getConfiguration();
     }
 
     return idx($options, $option_name);
   }
 
 
 /* -(  Loading Objects  )---------------------------------------------------- */
 
 
   /**
    * Load an object by ID. You need to invoke this as an instance method, not
    * a class method, because PHP doesn't have late static binding (until
    * PHP 5.3.0). For example:
    *
    *   $dog = id(new Dog())->load($dog_id);
    *
    * @param  int       Numeric ID identifying the object to load.
    * @return obj|null  Identified object, or null if it does not exist.
    *
    * @task   load
    */
   public function load($id) {
     if (!$id || (!is_int($id) && !ctype_digit($id))) {
       return null;
     }
 
     return $this->loadOneWhere(
       '%C = %d',
       $this->getIDKeyForUse(),
       $id);
   }
 
 
   /**
    * Loads all of the objects, unconditionally.
    *
    * @return dict    Dictionary of all persisted objects of this type, keyed
    *                 on object ID.
    *
    * @task   load
    */
   public function loadAll() {
     return $this->loadAllWhere('1 = 1');
   }
 
   /**
    * Loads all objects, but only fetches the specified columns.
    *
    * @param  array  Array of canonical column names as strings
    * @return dict   Dictionary of all objects, keyed by ID.
    *
    * @task   load
    */
   public function loadColumns(array $columns) {
     return $this->loadColumnsWhere($columns, '1 = 1');
   }
 
 
   /**
    * Load all objects which match a WHERE clause. You provide everything after
    * the 'WHERE'; Lisk handles everything up to it. For example:
    *
    *   $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
    *
    * The pattern and arguments are as per queryfx().
    *
    * @param  string  queryfx()-style SQL WHERE clause.
    * @param  ...     Zero or more conversions.
    * @return dict    Dictionary of matching objects, keyed on ID.
    *
    * @task   load
    */
   public function loadAllWhere($pattern/* , $arg, $arg, $arg ... */) {
     $args = func_get_args();
     array_unshift($args, null);
     $data = call_user_func_array(
       array($this, 'loadRawDataWhere'),
       $args);
     return $this->loadAllFromArray($data);
   }
 
   /**
    * Loads selected columns from objects that match a WHERE clause. You must
    * provide everything after the WHERE. See loadAllWhere().
    *
    * @param  array   List of column names.
    * @param  string  queryfx()-style SQL WHERE clause.
    * @param  ...     Zero or more conversions.
    * @return dict    Dictionary of matching objecks, keyed by ID.
    *
    * @task   load
    */
   public function loadColumnsWhere(array $columns, $pattern/* , $args... */) {
     if (!$this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
       throw new BadMethodCallException(
         "This class does not support partial objects.");
     }
     $args = func_get_args();
     $data = call_user_func_array(
       array($this, 'loadRawDataWhere'),
       $args);
     return $this->loadAllFromArray($data);
   }
 
 
   /**
    * Load a single object identified by a 'WHERE' clause. You provide
    * everything  after the 'WHERE', and Lisk builds the first half of the
    * query. See loadAllWhere(). This method is similar, but returns a single
    * result instead of a list.
    *
    * @param  string    queryfx()-style SQL WHERE clause.
    * @param  ...       Zero or more conversions.
    * @return obj|null  Matching object, or null if no object matches.
    *
    * @task   load
    */
   public function loadOneWhere($pattern/* , $arg, $arg, $arg ... */) {
     $args = func_get_args();
     array_unshift($args, null);
     $data = call_user_func_array(
       array($this, 'loadRawDataWhere'),
       $args);
 
     if (count($data) > 1) {
       throw new AphrontQueryCountException(
         "More than 1 result from loadOneWhere()!");
     }
 
     $data = reset($data);
     if (!$data) {
       return null;
     }
 
     return $this->loadFromArray($data);
   }
 
 
   protected function loadRawDataWhere($columns, $pattern/* , $args... */) {
     $connection = $this->establishConnection('r');
 
     $lock_clause = '';
     if ($connection->isReadLocking()) {
       $lock_clause = 'FOR UPDATE';
     } else if ($connection->isWriteLocking()) {
       $lock_clause = 'LOCK IN SHARE MODE';
     }
 
     $args = func_get_args();
     $args = array_slice($args, 2);
 
     if (!$columns) {
       $column = '*';
     } else {
       $column = '%LC';
       $columns[] = $this->getIDKey();
 
       $properties = $this->getProperties();
       $this->__missingFields = array_diff_key(
         array_flip($properties),
         array_flip($columns));
     }
 
     $pattern = 'SELECT '.$column.' FROM %T WHERE '.$pattern.' %Q';
     array_unshift($args, $this->getTableName());
     if ($columns) {
       array_unshift($args, $columns);
     }
     array_push($args, $lock_clause);
     array_unshift($args, $pattern);
 
     return call_user_func_array(
       array($connection, 'queryData'),
       $args);
   }
 
 
   /**
    * Reload an object from the database, discarding any changes to persistent
    * properties. This is primarily useful after entering a transaction but
    * before applying changes to an object.
    *
    * @return this
    *
    * @task   load
    */
   public function reload() {
 
     if (!$this->getID()) {
       throw new Exception("Unable to reload object that hasn't been loaded!");
     }
 
     $result = $this->loadOneWhere(
       '%C = %d',
       $this->getIDKeyForUse(),
       $this->getID());
 
     if (!$result) {
       throw new AphrontQueryObjectMissingException();
     }
 
     return $this;
   }
 
 
   /**
    * Initialize this object's properties from a dictionary. Generally, you
    * load single objects with loadOneWhere(), but sometimes it may be more
    * convenient to pull data from elsewhere directly (e.g., a complicated
    * join via queryData()) and then load from an array representation.
    *
    * @param  dict  Dictionary of properties, which should be equivalent to
    *               selecting a row from the table or calling getProperties().
    * @return this
    *
    * @task   load
    */
   public function loadFromArray(array $row) {
     static $valid_properties = array();
 
     $map = array();
     foreach ($row as $k => $v) {
       // We permit (but ignore) extra properties in the array because a
       // common approach to building the array is to issue a raw SELECT query
       // which may include extra explicit columns or joins.
 
       // This pathway is very hot on some pages, so we're inlining a cache
       // and doing some microoptimization to avoid a strtolower() call for each
       // assignment. The common path (assigning a valid property which we've
       // already seen) always incurs only one empty(). The second most common
       // path (assigning an invalid property which we've already seen) costs
       // an empty() plus an isset().
 
       if (empty($valid_properties[$k])) {
         if (isset($valid_properties[$k])) {
           // The value is set but empty, which means it's false, so we've
           // already determined it's not valid. We don't need to check again.
           continue;
         }
         $valid_properties[$k] = $this->hasProperty($k);
         if (!$valid_properties[$k]) {
           continue;
         }
       }
 
       $map[$k] = $v;
     }
 
     $this->willReadData($map);
 
     foreach ($map as $prop => $value) {
       $this->$prop = $value;
     }
 
     $this->didReadData();
 
     return $this;
   }
 
 
   /**
    * Initialize a list of objects from a list of dictionaries. Usually you
    * load lists of objects with loadAllWhere(), but sometimes that isn't
    * flexible enough. One case is if you need to do joins to select the right
    * objects:
    *
    *   function loadAllWithOwner($owner) {
    *     $data = $this->queryData(
    *       'SELECT d.*
    *         FROM owner o
    *           JOIN owner_has_dog od ON o.id = od.ownerID
    *           JOIN dog d ON od.dogID = d.id
    *         WHERE o.id = %d',
    *       $owner);
    *     return $this->loadAllFromArray($data);
    *   }
    *
    * This is a lot messier than loadAllWhere(), but more flexible.
    *
    * @param  list  List of property dictionaries.
    * @return dict  List of constructed objects, keyed on ID.
    *
    * @task   load
    */
   public function loadAllFromArray(array $rows) {
     $result = array();
 
     $id_key = $this->getIDKey();
 
     foreach ($rows as $row) {
       $obj = clone $this;
       if ($id_key && isset($row[$id_key])) {
         $result[$row[$id_key]] = $obj->loadFromArray($row);
       } else {
         $result[] = $obj->loadFromArray($row);
       }
       if ($this->inSet) {
         $this->inSet->addToSet($obj);
       }
     }
 
     return $result;
   }
 
   /**
    * This method helps to prevent the 1+N queries problem. It happens when you
    * execute a query for each row in a result set. Like in this code:
    *
    *   COUNTEREXAMPLE, name=Easy to write but expensive to execute
    *   $diffs = id(new DifferentialDiff())->loadAllWhere(
    *     'revisionID = %d',
    *     $revision->getID());
    *   foreach ($diffs as $diff) {
    *     $changesets = id(new DifferentialChangeset())->loadAllWhere(
    *       'diffID = %d',
    *       $diff->getID());
    *     // Do something with $changesets.
    *   }
    *
    * One can solve this problem by reading all the dependent objects at once and
    * assigning them later:
    *
    *   COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain
    *   $diffs = id(new DifferentialDiff())->loadAllWhere(
    *     'revisionID = %d',
    *     $revision->getID());
    *   $all_changesets = id(new DifferentialChangeset())->loadAllWhere(
    *     'diffID IN (%Ld)',
    *     mpull($diffs, 'getID'));
    *   $all_changesets = mgroup($all_changesets, 'getDiffID');
    *   foreach ($diffs as $diff) {
    *     $changesets = idx($all_changesets, $diff->getID(), array());
    *     // Do something with $changesets.
    *   }
    *
    * The method @{method:loadRelatives} abstracts this approach which allows
    * writing a code which is simple and efficient at the same time:
    *
    *   name=Easy to write and cheap to execute
    *   $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
    *   foreach ($diffs as $diff) {
    *     $changesets = $diff->loadRelatives(
    *       new DifferentialChangeset(),
    *       'diffID');
    *     // Do something with $changesets.
    *   }
    *
    * This will load dependent objects for all diffs in the first call of
    * @{method:loadRelatives} and use this result for all following calls.
    *
    * The method supports working with set of sets, like in this code:
    *
    *   $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
    *   foreach ($diffs as $diff) {
    *     $changesets = $diff->loadRelatives(
    *       new DifferentialChangeset(),
    *       'diffID');
    *     foreach ($changesets as $changeset) {
    *       $hunks = $changeset->loadRelatives(
    *         new DifferentialHunk(),
    *         'changesetID');
    *       // Do something with hunks.
    *     }
    *   }
    *
    * This code will execute just three queries - one to load all diffs, one to
    * load all their related changesets and one to load all their related hunks.
    * You can try to write an equivalent code without using this method as
    * a homework.
    *
    * The method also supports retrieving referenced objects, for example authors
    * of all diffs (using shortcut @{method:loadOneRelative}):
    *
    *   foreach ($diffs as $diff) {
    *     $author = $diff->loadOneRelative(
    *       new PhabricatorUser(),
    *       'phid',
    *       'getAuthorPHID');
    *     // Do something with author.
    *   }
    *
    * It is also possible to specify additional conditions for the `WHERE`
    * clause. Similarly to @{method:loadAllWhere}, you can specify everything
    * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is
    * allowed to pass only a constant string (`%` doesn't have a special
    * meaning). This is intentional to avoid mistakes with using data from one
    * row in retrieving other rows. Example of a correct usage:
    *
    *   $status = $author->loadOneRelative(
    *     new PhabricatorUserStatus(),
    *     'userPHID',
    *     'getPHID',
    *     '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)');
    *
    * @param  LiskDAO  Type of objects to load.
    * @param  string   Name of the column in target table.
    * @param  string   Method name in this table.
    * @param  string   Additional constraints on returned rows. It supports no
    *                  placeholders and requires putting the WHERE part into
    *                  parentheses. It's not possible to use LIMIT.
    * @return list     Objects of type $object.
    *
    * @task   load
    */
   public function loadRelatives(
     LiskDAO $object,
     $foreign_column,
     $key_method = 'getID',
     $where = '') {
 
     if (!$this->inSet) {
       id(new LiskDAOSet())->addToSet($this);
     }
     $relatives = $this->inSet->loadRelatives(
       $object,
       $foreign_column,
       $key_method,
       $where);
     return idx($relatives, $this->$key_method(), array());
   }
 
   /**
    * Load referenced row. See @{method:loadRelatives} for details.
    *
    * @param  LiskDAO  Type of objects to load.
    * @param  string   Name of the column in target table.
    * @param  string   Method name in this table.
    * @param  string   Additional constraints on returned rows. It supports no
    *                  placeholders and requires putting the WHERE part into
    *                  parentheses. It's not possible to use LIMIT.
    * @return LiskDAO  Object of type $object or null if there's no such object.
    *
    * @task   load
    */
   final public function loadOneRelative(
     LiskDAO $object,
     $foreign_column,
     $key_method = 'getID',
     $where = '') {
 
     $relatives = $this->loadRelatives(
       $object,
       $foreign_column,
       $key_method,
       $where);
 
     if (!$relatives) {
       return null;
     }
 
     if (count($relatives) > 1) {
       throw new AphrontQueryCountException(
         "More than 1 result from loadOneRelative()!");
     }
 
     return reset($relatives);
   }
 
   final public function putInSet(LiskDAOSet $set) {
     $this->inSet = $set;
     return $this;
   }
 
   final protected function getInSet() {
     return $this->inSet;
   }
 
 
 /* -(  Examining Objects  )-------------------------------------------------- */
 
 
   /**
    * Retrieve the unique, numerical ID identifying this object. This value
    * will be null if the object hasn't been persisted.
    *
    * @return int   Unique numerical ID.
    *
    * @task   info
    */
   public function getID() {
     static $id_key = null;
     if ($id_key === null) {
       $id_key = $this->getIDKeyForUse();
     }
     return $this->$id_key;
   }
 
 
   /**
    * Test if a property exists.
    *
    * @param   string    Property name.
    * @return  bool      True if the property exists.
    * @task info
    */
   public function hasProperty($property) {
     return (bool)$this->checkProperty($property);
   }
 
 
   /**
    * Retrieve a list of all object properties. This list only includes
    * properties that are declared as protected, and it is expected that
    * all properties returned by this function should be persisted to the
    * database.
    * Properties that should not be persisted must be declared as private.
    *
    * @return dict  Dictionary of normalized (lowercase) to canonical (original
    *               case) property names.
    *
    * @task   info
    */
   protected function getProperties() {
     static $properties = null;
     if (!isset($properties)) {
       $class = new ReflectionClass(get_class($this));
       $properties = array();
       foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
         $properties[strtolower($p->getName())] = $p->getName();
       }
 
       $id_key = $this->getIDKey();
       if ($id_key != 'id') {
         unset($properties['id']);
       }
 
       if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
         unset($properties['datecreated']);
         unset($properties['datemodified']);
       }
 
       if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {
         unset($properties['phid']);
       }
     }
     return $properties;
   }
 
 
   /**
    * Check if a property exists on this object.
    *
    * @return string|null   Canonical property name, or null if the property
    *                       does not exist.
    *
    * @task   info
    */
   protected function checkProperty($property) {
     static $properties = null;
     if ($properties === null) {
       $properties = $this->getProperties();
     }
 
     $property = strtolower($property);
     if (empty($properties[$property])) {
       return null;
     }
 
     return $properties[$property];
   }
 
 
   /**
    * Get or build the database connection for this object.
    *
    * @param  string 'r' for read, 'w' for read/write.
    * @param  bool True to force a new connection. The connection will not
    *              be retrieved from or saved into the connection cache.
    * @return LiskDatabaseConnection   Lisk connection object.
    *
    * @task   info
    */
   public function establishConnection($mode, $force_new = false) {
     if ($mode != 'r' && $mode != 'w') {
       throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'.");
     }
 
     if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) {
       $mode = 'isolate-'.$mode;
 
       $connection = $this->getEstablishedConnection($mode);
       if (!$connection) {
         $connection = $this->establishIsolatedConnection($mode);
         $this->setEstablishedConnection($mode, $connection);
       }
 
       return $connection;
     }
 
     if (self::shouldIsolateAllLiskEffectsToTransactions()) {
       // If we're doing fixture transaction isolation, force the mode to 'w'
       // so we always get the same connection for reads and writes, and thus
       // can see the writes inside the transaction.
       $mode = 'w';
     }
 
     // TODO: There is currently no protection on 'r' queries against writing.
 
     $connection = null;
     if (!$force_new) {
       if ($mode == 'r') {
         // If we're requesting a read connection but already have a write
         // connection, reuse the write connection so that reads can take place
         // inside transactions.
         $connection = $this->getEstablishedConnection('w');
       }
 
       if (!$connection) {
         $connection = $this->getEstablishedConnection($mode);
       }
     }
 
     if (!$connection) {
       $connection = $this->establishLiveConnection($mode);
       if (self::shouldIsolateAllLiskEffectsToTransactions()) {
         $connection->openTransaction();
       }
       $this->setEstablishedConnection(
         $mode,
         $connection,
         $force_unique = $force_new);
     }
 
     return $connection;
   }
 
 
   /**
    * Convert this object into a property dictionary. This dictionary can be
    * restored into an object by using loadFromArray() (unless you're using
    * legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should
    * just go ahead and die in a fire).
    *
    * @return dict  Dictionary of object properties.
    *
    * @task   info
    */
   protected function getPropertyValues() {
     $map = array();
     foreach ($this->getProperties() as $p) {
       // We may receive a warning here for properties we've implicitly added
       // through configuration; squelch it.
       $map[$p] = @$this->$p;
     }
     return $map;
   }
 
 
 /* -(  Writing Objects  )---------------------------------------------------- */
 
 
   /**
    * Make an object read-only.
    *
    * Making an object ephemeral indicates that you will be changing state in
    * such a way that you would never ever want it to be written back to the
    * storage.
    */
   public function makeEphemeral() {
     $this->__ephemeral = true;
     return $this;
   }
 
   private function isEphemeralCheck() {
     if ($this->__ephemeral) {
       throw new LiskEphemeralObjectException();
     }
   }
 
   /**
    * Persist this object to the database. In most cases, this is the only
    * method you need to call to do writes. If the object has not yet been
    * inserted this will do an insert; if it has, it will do an update.
    *
    * @return this
    *
    * @task   save
    */
   public function save() {
     if ($this->shouldInsertWhenSaved()) {
       return $this->insert();
     } else {
       return $this->update();
     }
   }
 
 
   /**
    * Save this object, forcing the query to use REPLACE regardless of object
    * state.
    *
    * @return this
    *
    * @task   save
    */
   public function replace() {
     $this->isEphemeralCheck();
     return $this->insertRecordIntoDatabase('REPLACE');
   }
 
 
   /**
    *  Save this object, forcing the query to use INSERT regardless of object
    *  state.
    *
    *  @return this
    *
    *  @task   save
    */
   public function insert() {
     $this->isEphemeralCheck();
     return $this->insertRecordIntoDatabase('INSERT');
   }
 
 
   /**
    *  Save this object, forcing the query to use UPDATE regardless of object
    *  state.
    *
    *  @return this
    *
    *  @task   save
    */
   public function update() {
     $this->isEphemeralCheck();
 
     $this->willSaveObject();
     $data = $this->getPropertyValues();
     if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
       $data = array_intersect_key($data, $this->__dirtyFields);
     }
     $this->willWriteData($data);
 
     $map = array();
     foreach ($data as $k => $v) {
       $map[$k] = $v;
     }
 
     $conn = $this->establishConnection('w');
 
     foreach ($map as $key => $value) {
       $map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
     }
     $map = implode(', ', $map);
 
     $id = $this->getID();
     $conn->query(
       'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'),
       $this->getTableName(),
       $map,
       $this->getIDKeyForUse(),
       $id);
     // We can't detect a missing object because updating an object without
     // changing any values doesn't affect rows. We could jiggle timestamps
     // to catch this for objects which track them if we wanted.
 
     $this->didWriteData();
 
     if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
       $this->resetDirtyFields();
     }
 
     return $this;
   }
 
 
   /**
    * Delete this object, permanently.
    *
    * @return this
    *
    * @task   save
    */
   public function delete() {
     $this->isEphemeralCheck();
     $this->willDelete();
 
     $conn = $this->establishConnection('w');
     $conn->query(
       'DELETE FROM %T WHERE %C = %d',
       $this->getTableName(),
       $this->getIDKeyForUse(),
       $this->getID());
 
     $this->didDelete();
 
     return $this;
   }
 
 
   /**
    * Internal implementation of INSERT and REPLACE.
    *
    * @param  const   Either "INSERT" or "REPLACE", to force the desired mode.
    *
    * @task   save
    */
   protected function insertRecordIntoDatabase($mode) {
     $this->willSaveObject();
     $data = $this->getPropertyValues();
 
     $id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
     switch ($id_mechanism) {
       case self::IDS_AUTOINCREMENT:
         // If we are using autoincrement IDs, let MySQL assign the value for the
         // ID column, if it is empty. If the caller has explicitly provided a
         // value, use it.
         $id_key = $this->getIDKeyForUse();
         if (empty($data[$id_key])) {
           unset($data[$id_key]);
         }
         break;
       case self::IDS_PHID:
         if (empty($data[$this->getIDKeyForUse()])) {
           $phid = $this->generatePHID();
           $this->setID($phid);
           $data[$this->getIDKeyForUse()] = $phid;
         }
         break;
       case self::IDS_MANUAL:
         break;
       default:
         throw new Exception('Unknown CONFIG_IDs mechanism!');
     }
 
     $this->willWriteData($data);
 
     $conn = $this->establishConnection('w');
 
     $columns = array_keys($data);
 
     foreach ($data as $key => $value) {
       $data[$key] = qsprintf($conn, '%ns', $value);
     }
     $data = implode(', ', $data);
 
     $conn->query(
       '%Q INTO %T (%LC) VALUES (%Q)',
       $mode,
       $this->getTableName(),
       $columns,
       $data);
 
     // Only use the insert id if this table is using auto-increment ids
     if ($id_mechanism === self::IDS_AUTOINCREMENT) {
       $this->setID($conn->getInsertID());
     }
 
     $this->didWriteData();
 
     if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
       $this->resetDirtyFields();
     }
 
     return $this;
   }
 
 
   /**
    * Method used to determine whether to insert or update when saving.
    *
    * @return bool true if the record should be inserted
    */
   protected function shouldInsertWhenSaved() {
     $key_type = $this->getConfigOption(self::CONFIG_IDS);
 
     if ($key_type == self::IDS_MANUAL) {
       throw new Exception(
         'You are using manual IDs. You must override the '.
         'shouldInsertWhenSaved() method to properly detect '.
         'when to insert a new record.');
     } else {
       return !$this->getID();
     }
   }
 
 
 /* -(  Hooks and Callbacks  )------------------------------------------------ */
 
 
   /**
    * Retrieve the database table name. By default, this is the class name.
    *
    * @return string  Table name for object storage.
    *
    * @task   hook
    */
   public function getTableName() {
     return get_class($this);
   }
 
 
   /**
    * Helper: Whether this class is configured to use PHIDs as the primary ID.
    * @task internal
    */
   private function isPHIDPrimaryID() {
     return ($this->getConfigOption(self::CONFIG_IDS) === self::IDS_PHID);
   }
 
 
   /**
    * Retrieve the primary key column, "id" by default. If you can not
    * reasonably name your ID column "id", override this method.
    *
    * @return string  Name of the ID column.
    *
    * @task   hook
    */
   public function getIDKey() {
     return
       $this->isPHIDPrimaryID() ?
       'phid' :
       'id';
   }
 
 
   protected function getIDKeyForUse() {
     $id_key = $this->getIDKey();
     if (!$id_key) {
       throw new Exception(
         "This DAO does not have a single-part primary key. The method you ".
         "called requires a single-part primary key.");
     }
     return $id_key;
   }
 
 
   /**
    * Generate a new PHID, used by CONFIG_AUX_PHID and IDS_PHID.
    *
    * @return phid    Unique, newly allocated PHID.
    *
    * @task   hook
    */
   protected function generatePHID() {
     throw new Exception(
       "To use CONFIG_AUX_PHID or IDS_PHID, you need to overload ".
       "generatePHID() to perform PHID generation.");
   }
 
 
   /**
    * Hook to apply serialization or validation to data before it is written to
    * the database. See also willReadData().
    *
    * @task hook
    */
   protected function willWriteData(array &$data) {
     $this->applyLiskDataSerialization($data, false);
   }
 
 
   /**
    * Hook to perform actions after data has been written to the database.
    *
    * @task hook
    */
   protected function didWriteData() {}
 
 
   /**
    * Hook to make internal object state changes prior to INSERT, REPLACE or
    * UPDATE.
    *
    * @task hook
    */
   protected function willSaveObject() {
     $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
 
     if ($use_timestamps) {
       if (!$this->getDateCreated()) {
         $this->setDateCreated(time());
       }
       $this->setDateModified(time());
     }
 
     if (($this->isPHIDPrimaryID() && !$this->getID())) {
       // If PHIDs are the primary ID, the subclass could have overridden the
       // name of the ID column.
       $this->setID($this->generatePHID());
     } else if ($this->getConfigOption(self::CONFIG_AUX_PHID) &&
                !$this->getPHID()) {
       // The subclass could still want PHIDs.
       $this->setPHID($this->generatePHID());
     }
   }
 
 
   /**
    * Hook to apply serialization or validation to data as it is read from the
    * database. See also willWriteData().
    *
    * @task hook
    */
   protected function willReadData(array &$data) {
     $this->applyLiskDataSerialization($data, $deserialize = true);
   }
 
   /**
    * Hook to perform an action on data after it is read from the database.
    *
    * @task hook
    */
   protected function didReadData() {}
 
   /**
    * Hook to perform an action before the deletion of an object.
    *
    * @task hook
    */
   protected function willDelete() {}
 
   /**
    * Hook to perform an action after the deletion of an object.
    *
    * @task hook
    */
   protected function didDelete() {}
 
   /**
    * Reads the value from a field. Override this method for custom behavior
    * of getField() instead of overriding getField directly.
    *
    * @param  string  Canonical field name
    * @return mixed   Value of the field
    *
    * @task hook
    */
   protected function readField($field) {
     if (isset($this->$field)) {
       return $this->$field;
     }
     return null;
   }
 
   /**
    * Writes a value to a field. Override this method for custom behavior of
    * setField($value) instead of overriding setField directly.
    *
    * @param  string  Canonical field name
    * @param  mixed   Value to write
    *
    * @task hook
    */
   protected function writeField($field, $value) {
     $this->$field = $value;
   }
 
 
 /* -(  Manging Transactions  )----------------------------------------------- */
 
 
   /**
    * Increase transaction stack depth.
    *
    * @return this
    */
   public function openTransaction() {
     $this->establishConnection('w')->openTransaction();
     return $this;
   }
 
 
   /**
    * Decrease transaction stack depth, saving work.
    *
    * @return this
    */
   public function saveTransaction() {
     $this->establishConnection('w')->saveTransaction();
     return $this;
   }
 
 
   /**
    * Decrease transaction stack depth, discarding work.
    *
    * @return this
    */
   public function killTransaction() {
     $this->establishConnection('w')->killTransaction();
     return $this;
   }
 
 
   /**
    * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
    * other connections can not read them (this is an enormous oversimplification
    * of FOR UPDATE semantics; consult the MySQL documentation for details). To
    * end read locking, call @{method:endReadLocking}. For example:
    *
    *   $beach->openTransaction();
    *     $beach->beginReadLocking();
    *
    *       $beach->reload();
    *       $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
    *       $beach->save();
    *
    *     $beach->endReadLocking();
    *   $beach->saveTransaction();
    *
    * @return this
    * @task xaction
    */
   public function beginReadLocking() {
     $this->establishConnection('w')->beginReadLocking();
     return $this;
   }
 
 
   /**
    * Ends read-locking that began at an earlier @{method:beginReadLocking} call.
    *
    * @return this
    * @task xaction
    */
   public function endReadLocking() {
     $this->establishConnection('w')->endReadLocking();
     return $this;
   }
 
   /**
    * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
    * that other connections can not update or delete them (this is an
    * oversimplification of LOCK IN SHARE MODE semantics; consult the
    * MySQL documentation for details). To end write locking, call
    * @{method:endWriteLocking}.
    *
    * @return this
    * @task xaction
    */
   public function beginWriteLocking() {
     $this->establishConnection('w')->beginWriteLocking();
     return $this;
   }
 
 
   /**
    * Ends write-locking that began at an earlier @{method:beginWriteLocking}
    * call.
    *
    * @return this
    * @task xaction
    */
   public function endWriteLocking() {
     $this->establishConnection('w')->endWriteLocking();
     return $this;
   }
 
 
 /* -(  Isolation  )---------------------------------------------------------- */
 
 
   /**
    * @task isolate
    */
   public static function beginIsolateAllLiskEffectsToCurrentProcess() {
     self::$processIsolationLevel++;
   }
 
   /**
    * @task isolate
    */
   public static function endIsolateAllLiskEffectsToCurrentProcess() {
     self::$processIsolationLevel--;
     if (self::$processIsolationLevel < 0) {
       throw new Exception(
         "Lisk process isolation level was reduced below 0.");
     }
   }
 
   /**
    * @task isolate
    */
   public static function shouldIsolateAllLiskEffectsToCurrentProcess() {
     return (bool)self::$processIsolationLevel;
   }
 
   /**
    * @task isolate
    */
   private function establishIsolatedConnection($mode) {
     $config = array();
     return new AphrontIsolatedDatabaseConnection($config);
   }
 
   /**
    * @task isolate
    */
   public static function beginIsolateAllLiskEffectsToTransactions() {
     if (self::$transactionIsolationLevel === 0) {
       self::closeAllConnections();
     }
     self::$transactionIsolationLevel++;
   }
 
   /**
    * @task isolate
    */
   public static function endIsolateAllLiskEffectsToTransactions() {
     self::$transactionIsolationLevel--;
     if (self::$transactionIsolationLevel < 0) {
       throw new Exception(
         "Lisk transaction isolation level was reduced below 0.");
     } else if (self::$transactionIsolationLevel == 0) {
       foreach (self::$connections as $key => $conn) {
         if ($conn) {
           $conn->killTransaction();
         }
       }
       self::closeAllConnections();
     }
   }
 
   /**
    * @task isolate
    */
   public static function shouldIsolateAllLiskEffectsToTransactions() {
     return (bool)self::$transactionIsolationLevel;
   }
 
   public static function closeAllConnections() {
     self::$connections = array();
   }
 
 /* -(  Utilities  )---------------------------------------------------------- */
 
 
   /**
    * Applies configured serialization to a dictionary of values.
    *
    * @task util
    */
   protected function applyLiskDataSerialization(array &$data, $deserialize) {
     $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
     if ($serialization) {
       foreach (array_intersect_key($serialization, $data) as $col => $format) {
         switch ($format) {
           case self::SERIALIZATION_NONE:
             break;
           case self::SERIALIZATION_PHP:
             if ($deserialize) {
               $data[$col] = unserialize($data[$col]);
             } else {
               $data[$col] = serialize($data[$col]);
             }
             break;
           case self::SERIALIZATION_JSON:
             if ($deserialize) {
               $data[$col] = json_decode($data[$col], true);
             } else {
               $data[$col] = json_encode($data[$col]);
             }
             break;
           default:
             throw new Exception("Unknown serialization format '{$format}'.");
         }
       }
     }
   }
 
   /**
    * Resets the dirty fields (fields which need to be written on next save/
    * update/insert/replace). If this DAO has timestamps, the modified time
    * is always a dirty field.
    *
    * @task util
    */
   private function resetDirtyFields() {
     $this->__dirtyFields = array();
     if ($this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
       $this->__dirtyFields['dateModified'] = true;
     }
   }
 
   /**
    * Black magic. Builds implied get*() and set*() for all properties.
    *
    * @param  string  Method name.
    * @param  list    Argument vector.
    * @return mixed   get*() methods return the property value. set*() methods
    *                 return $this.
    * @task   util
    */
   public function __call($method, $args) {
 
+    // NOTE: PHP has a bug that static variables defined in __call() are shared
+    // across all children classes. Call a different method to work around this
+    // bug.
+    return $this->call($method, $args);
+  }
+
+  /**
+   * @task   util
+   */
+  final protected function call($method, $args) {
     // NOTE: This method is very performance-sensitive (many thousands of calls
     // per page on some pages), and thus has some silliness in the name of
     // optimizations.
 
     static $dispatch_map = array();
     static $partial = null;
     if ($partial === null) {
       $partial = $this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS);
     }
 
     if ($method[0] === 'g') {
       if (isset($dispatch_map[$method])) {
         $property = $dispatch_map[$method];
       } else {
         if (substr($method, 0, 3) !== 'get') {
           throw new Exception("Unable to resolve method '{$method}'!");
         }
         $property = substr($method, 3);
         if (!($property = $this->checkProperty($property))) {
           throw new Exception("Bad getter call: {$method}");
         }
         $dispatch_map[$method] = $property;
       }
 
       if ($partial && isset($this->__missingFields[$property])) {
         throw new Exception("Cannot get field that wasn't loaded: {$property}");
       }
 
       return $this->readField($property);
     }
 
     if ($method[0] === 's') {
       if (isset($dispatch_map[$method])) {
         $property = $dispatch_map[$method];
       } else {
         if (substr($method, 0, 3) !== 'set') {
           throw new Exception("Unable to resolve method '{$method}'!");
         }
         $property = substr($method, 3);
         $property = $this->checkProperty($property);
         if (!$property) {
           throw new Exception("Bad setter call: {$method}");
         }
         if ($property == 'ID') {
           $property = $this->getIDKeyForUse();
         }
         $dispatch_map[$method] = $property;
       }
       if ($partial) {
         // Accept writes to fields that weren't initially loaded
         unset($this->__missingFields[$property]);
         $this->__dirtyFields[$property] = true;
       }
 
       $this->writeField($property, $args[0]);
 
       return $this;
     }
 
     throw new Exception("Unable to resolve method '{$method}'.");
   }
 
   /**
    * Warns against writing to undeclared property.
    *
    * @task   util
    */
   public function __set($name, $value) {
     phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.');
     $this->$name = $value;
   }
 
 }