[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/infrastructure/storage/lisk/ -> LiskDAO.php (source)

   1  <?php
   2  
   3  /**
   4   * Simple object-authoritative data access object that makes it easy to build
   5   * stuff that you need to save to a database. Basically, it means that the
   6   * amount of boilerplate code (and, particularly, boilerplate SQL) you need
   7   * to write is greatly reduced.
   8   *
   9   * Lisk makes it fairly easy to build something quickly and end up with
  10   * reasonably high-quality code when you're done (e.g., getters and setters,
  11   * objects, transactions, reasonably structured OO code). It's also very thin:
  12   * you can break past it and use MySQL and other lower-level tools when you
  13   * need to in those couple of cases where it doesn't handle your workflow
  14   * gracefully.
  15   *
  16   * However, Lisk won't scale past one database and lacks many of the features
  17   * of modern DAOs like Hibernate: for instance, it does not support joins or
  18   * polymorphic storage.
  19   *
  20   * This means that Lisk is well-suited for tools like Differential, but often a
  21   * poor choice elsewhere. And it is strictly unsuitable for many projects.
  22   *
  23   * Lisk's model is object-authoritative: the PHP class definition is the
  24   * master authority for what the object looks like.
  25   *
  26   * =Building New Objects=
  27   *
  28   * To create new Lisk objects, extend @{class:LiskDAO} and implement
  29   * @{method:establishLiveConnection}. It should return an
  30   * @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your
  31   * objects.
  32   *
  33   *   class Dog extends LiskDAO {
  34   *
  35   *     protected $name;
  36   *     protected $breed;
  37   *
  38   *     public function establishLiveConnection() {
  39   *       return $some_connection_object;
  40   *     }
  41   *   }
  42   *
  43   * Now, you should create your table:
  44   *
  45   *   lang=sql
  46   *   CREATE TABLE dog (
  47   *     id int unsigned not null auto_increment primary key,
  48   *     name varchar(32) not null,
  49   *     breed varchar(32) not null,
  50   *     dateCreated int unsigned not null,
  51   *     dateModified int unsigned not null
  52   *   );
  53   *
  54   * For each property in your class, add a column with the same name to the table
  55   * (see @{method:getConfiguration} for information about changing this mapping).
  56   * Additionally, you should create the three columns `id`,  `dateCreated` and
  57   * `dateModified`. Lisk will automatically manage these, using them to implement
  58   * autoincrement IDs and timestamps. If you do not want to use these features,
  59   * see @{method:getConfiguration} for information on disabling them. At a bare
  60   * minimum, you must normally have an `id` column which is a primary or unique
  61   * key with a numeric type, although you can change its name by overriding
  62   * @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to
  63   * return null. Note that many methods rely on a single-part primary key and
  64   * will no longer work (they will throw) if you disable it.
  65   *
  66   * As you add more properties to your class in the future, remember to add them
  67   * to the database table as well.
  68   *
  69   * Lisk will now automatically handle these operations: getting and setting
  70   * properties, saving objects, loading individual objects, loading groups
  71   * of objects, updating objects, managing IDs, updating timestamps whenever
  72   * an object is created or modified, and some additional specialized
  73   * operations.
  74   *
  75   * = Creating, Retrieving, Updating, and Deleting =
  76   *
  77   * To create and persist a Lisk object, use @{method:save}:
  78   *
  79   *   $dog = id(new Dog())
  80   *     ->setName('Sawyer')
  81   *     ->setBreed('Pug')
  82   *     ->save();
  83   *
  84   * Note that **Lisk automatically builds getters and setters for all of your
  85   * object's protected properties** via @{method:__call}. If you want to add
  86   * custom behavior to your getters or setters, you can do so by overriding the
  87   * @{method:readField} and @{method:writeField} methods.
  88   *
  89   * Calling @{method:save} will persist the object to the database. After calling
  90   * @{method:save}, you can call @{method:getID} to retrieve the object's ID.
  91   *
  92   * To load objects by ID, use the @{method:load} method:
  93   *
  94   *   $dog = id(new Dog())->load($id);
  95   *
  96   * This will load the Dog record with ID $id into $dog, or `null` if no such
  97   * record exists (@{method:load} is an instance method rather than a static
  98   * method because PHP does not support late static binding, at least until PHP
  99   * 5.3).
 100   *
 101   * To update an object, change its properties and save it:
 102   *
 103   *   $dog->setBreed('Lab')->save();
 104   *
 105   * To delete an object, call @{method:delete}:
 106   *
 107   *   $dog->delete();
 108   *
 109   * That's Lisk CRUD in a nutshell.
 110   *
 111   * = Queries =
 112   *
 113   * Often, you want to load a bunch of objects, or execute a more specialized
 114   * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:
 115   *
 116   *   $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
 117   *   $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
 118   *
 119   * These methods work like @{function@libphutil:queryfx}, but only take half of
 120   * a query (the part after the WHERE keyword). Lisk will handle the connection,
 121   * columns, and object construction; you are responsible for the rest of it.
 122   * @{method:loadAllWhere} returns a list of objects, while
 123   * @{method:loadOneWhere} returns a single object (or `null`).
 124   *
 125   * There's also a @{method:loadRelatives} method which helps to prevent the 1+N
 126   * queries problem.
 127   *
 128   * = Managing Transactions =
 129   *
 130   * Lisk uses a transaction stack, so code does not generally need to be aware
 131   * of the transactional state of objects to implement correct transaction
 132   * semantics:
 133   *
 134   *   $obj->openTransaction();
 135   *     $obj->save();
 136   *     $other->save();
 137   *     // ...
 138   *     $other->openTransaction();
 139   *       $other->save();
 140   *       $another->save();
 141   *     if ($some_condition) {
 142   *       $other->saveTransaction();
 143   *     } else {
 144   *       $other->killTransaction();
 145   *     }
 146   *     // ...
 147   *   $obj->saveTransaction();
 148   *
 149   * Assuming ##$obj##, ##$other## and ##$another## live on the same database,
 150   * this code will work correctly by establishing savepoints.
 151   *
 152   * Selects whose data are used later in the transaction should be included in
 153   * @{method:beginReadLocking} or @{method:beginWriteLocking} block.
 154   *
 155   * @task   conn    Managing Connections
 156   * @task   config  Configuring Lisk
 157   * @task   load    Loading Objects
 158   * @task   info    Examining Objects
 159   * @task   save    Writing Objects
 160   * @task   hook    Hooks and Callbacks
 161   * @task   util    Utilities
 162   * @task   xaction Managing Transactions
 163   * @task   isolate Isolation for Unit Testing
 164   */
 165  abstract class LiskDAO {
 166  
 167    const CONFIG_IDS                  = 'id-mechanism';
 168    const CONFIG_TIMESTAMPS           = 'timestamps';
 169    const CONFIG_AUX_PHID             = 'auxiliary-phid';
 170    const CONFIG_SERIALIZATION        = 'col-serialization';
 171    const CONFIG_BINARY               = 'binary';
 172    const CONFIG_COLUMN_SCHEMA        = 'col-schema';
 173    const CONFIG_KEY_SCHEMA           = 'key-schema';
 174    const CONFIG_NO_TABLE             = 'no-table';
 175  
 176    const SERIALIZATION_NONE          = 'id';
 177    const SERIALIZATION_JSON          = 'json';
 178    const SERIALIZATION_PHP           = 'php';
 179  
 180    const IDS_AUTOINCREMENT           = 'ids-auto';
 181    const IDS_COUNTER                 = 'ids-counter';
 182    const IDS_MANUAL                  = 'ids-manual';
 183  
 184    const COUNTER_TABLE_NAME          = 'lisk_counter';
 185  
 186    private static $processIsolationLevel     = 0;
 187    private static $transactionIsolationLevel = 0;
 188  
 189    private $ephemeral = false;
 190  
 191    private static $connections       = array();
 192  
 193    private $inSet = null;
 194  
 195    protected $id;
 196    protected $phid;
 197    protected $dateCreated;
 198    protected $dateModified;
 199  
 200    /**
 201     *  Build an empty object.
 202     *
 203     *  @return obj Empty object.
 204     */
 205    public function __construct() {
 206      $id_key = $this->getIDKey();
 207      if ($id_key) {
 208        $this->$id_key = null;
 209      }
 210    }
 211  
 212  
 213  /* -(  Managing Connections  )----------------------------------------------- */
 214  
 215  
 216    /**
 217     * Establish a live connection to a database service. This method should
 218     * return a new connection. Lisk handles connection caching and management;
 219     * do not perform caching deeper in the stack.
 220     *
 221     * @param string Mode, either 'r' (reading) or 'w' (reading and writing).
 222     * @return AphrontDatabaseConnection New database connection.
 223     * @task conn
 224     */
 225    abstract protected function establishLiveConnection($mode);
 226  
 227  
 228    /**
 229     * Return a namespace for this object's connections in the connection cache.
 230     * Generally, the database name is appropriate. Two connections are considered
 231     * equivalent if they have the same connection namespace and mode.
 232     *
 233     * @return string Connection namespace for cache
 234     * @task conn
 235     */
 236    abstract protected function getConnectionNamespace();
 237  
 238  
 239    /**
 240     * Get an existing, cached connection for this object.
 241     *
 242     * @param mode Connection mode.
 243     * @return AprontDatabaseConnection|null  Connection, if it exists in cache.
 244     * @task conn
 245     */
 246    protected function getEstablishedConnection($mode) {
 247      $key = $this->getConnectionNamespace().':'.$mode;
 248      if (isset(self::$connections[$key])) {
 249        return self::$connections[$key];
 250      }
 251      return null;
 252    }
 253  
 254  
 255    /**
 256     * Store a connection in the connection cache.
 257     *
 258     * @param mode Connection mode.
 259     * @param AphrontDatabaseConnection Connection to cache.
 260     * @return this
 261     * @task conn
 262     */
 263    protected function setEstablishedConnection(
 264      $mode,
 265      AphrontDatabaseConnection $connection,
 266      $force_unique = false) {
 267  
 268      $key = $this->getConnectionNamespace().':'.$mode;
 269  
 270      if ($force_unique) {
 271        $key .= ':unique';
 272        while (isset(self::$connections[$key])) {
 273          $key .= '!';
 274        }
 275      }
 276  
 277      self::$connections[$key] = $connection;
 278      return $this;
 279    }
 280  
 281  
 282  /* -(  Configuring Lisk  )--------------------------------------------------- */
 283  
 284  
 285    /**
 286     * Change Lisk behaviors, like ID configuration and timestamps. If you want
 287     * to change these behaviors, you should override this method in your child
 288     * class and change the options you're interested in. For example:
 289     *
 290     *   public function getConfiguration() {
 291     *     return array(
 292     *       Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
 293     *     ) + parent::getConfiguration();
 294     *   }
 295     *
 296     * The available options are:
 297     *
 298     * CONFIG_IDS
 299     * Lisk objects need to have a unique identifying ID. The three mechanisms
 300     * available for generating this ID are IDS_AUTOINCREMENT (default, assumes
 301     * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking
 302     * full responsibility for ID management), or IDS_COUNTER (see below).
 303     *
 304     * InnoDB does not persist the value of `auto_increment` across restarts,
 305     * and instead initializes it to `MAX(id) + 1` during startup. This means it
 306     * may reissue the same autoincrement ID more than once, if the row is deleted
 307     * and then the database is restarted. To avoid this, you can set an object to
 308     * use a counter table with IDS_COUNTER. This will generally behave like
 309     * IDS_AUTOINCREMENT, except that the counter value will persist across
 310     * restarts and inserts will be slightly slower. If a database stores any
 311     * DAOs which use this mechanism, you must create a table there with this
 312     * schema:
 313     *
 314     *   CREATE TABLE lisk_counter (
 315     *     counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY,
 316     *     counterValue BIGINT UNSIGNED NOT NULL
 317     *   ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 318     *
 319     * CONFIG_TIMESTAMPS
 320     * Lisk can automatically handle keeping track of a `dateCreated' and
 321     * `dateModified' column, which it will update when it creates or modifies
 322     * an object. If you don't want to do this, you may disable this option.
 323     * By default, this option is ON.
 324     *
 325     * CONFIG_AUX_PHID
 326     * This option can be enabled by being set to some truthy value. The meaning
 327     * of this value is defined by your PHID generation mechanism. If this option
 328     * is enabled, a `phid' property will be populated with a unique PHID when an
 329     * object is created (or if it is saved and does not currently have one). You
 330     * need to override generatePHID() and hook it into your PHID generation
 331     * mechanism for this to work. By default, this option is OFF.
 332     *
 333     * CONFIG_SERIALIZATION
 334     * You can optionally provide a column serialization map that will be applied
 335     * to values when they are written to the database. For example:
 336     *
 337     *   self::CONFIG_SERIALIZATION => array(
 338     *     'complex' => self::SERIALIZATION_JSON,
 339     *   )
 340     *
 341     * This will cause Lisk to JSON-serialize the 'complex' field before it is
 342     * written, and unserialize it when it is read.
 343     *
 344     * CONFIG_BINARY
 345     * You can optionally provide a map of columns to a flag indicating that
 346     * they store binary data. These columns will not raise an error when
 347     * handling binary writes.
 348     *
 349     * CONFIG_COLUMN_SCHEMA
 350     * Provide a map of columns to schema column types.
 351     *
 352     * CONFIG_KEY_SCHEMA
 353     * Provide a map of key names to key specifications.
 354     *
 355     * CONFIG_NO_TABLE
 356     * Allows you to specify that this object does not actually have a table in
 357     * the database.
 358     *
 359     * @return dictionary  Map of configuration options to values.
 360     *
 361     * @task   config
 362     */
 363    protected function getConfiguration() {
 364      return array(
 365        self::CONFIG_IDS                      => self::IDS_AUTOINCREMENT,
 366        self::CONFIG_TIMESTAMPS               => true,
 367      );
 368    }
 369  
 370  
 371    /**
 372     *  Determine the setting of a configuration option for this class of objects.
 373     *
 374     *  @param  const       Option name, one of the CONFIG_* constants.
 375     *  @return mixed       Option value, if configured (null if unavailable).
 376     *
 377     *  @task   config
 378     */
 379    public function getConfigOption($option_name) {
 380      static $options = null;
 381  
 382      if (!isset($options)) {
 383        $options = $this->getConfiguration();
 384      }
 385  
 386      return idx($options, $option_name);
 387    }
 388  
 389  
 390  /* -(  Loading Objects  )---------------------------------------------------- */
 391  
 392  
 393    /**
 394     * Load an object by ID. You need to invoke this as an instance method, not
 395     * a class method, because PHP doesn't have late static binding (until
 396     * PHP 5.3.0). For example:
 397     *
 398     *   $dog = id(new Dog())->load($dog_id);
 399     *
 400     * @param  int       Numeric ID identifying the object to load.
 401     * @return obj|null  Identified object, or null if it does not exist.
 402     *
 403     * @task   load
 404     */
 405    public function load($id) {
 406      if (is_object($id)) {
 407        $id = (string)$id;
 408      }
 409  
 410      if (!$id || (!is_int($id) && !ctype_digit($id))) {
 411        return null;
 412      }
 413  
 414      return $this->loadOneWhere(
 415        '%C = %d',
 416        $this->getIDKeyForUse(),
 417        $id);
 418    }
 419  
 420  
 421    /**
 422     * Loads all of the objects, unconditionally.
 423     *
 424     * @return dict    Dictionary of all persisted objects of this type, keyed
 425     *                 on object ID.
 426     *
 427     * @task   load
 428     */
 429    public function loadAll() {
 430      return $this->loadAllWhere('1 = 1');
 431    }
 432  
 433  
 434    /**
 435     * Load all objects which match a WHERE clause. You provide everything after
 436     * the 'WHERE'; Lisk handles everything up to it. For example:
 437     *
 438     *   $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
 439     *
 440     * The pattern and arguments are as per queryfx().
 441     *
 442     * @param  string  queryfx()-style SQL WHERE clause.
 443     * @param  ...     Zero or more conversions.
 444     * @return dict    Dictionary of matching objects, keyed on ID.
 445     *
 446     * @task   load
 447     */
 448    public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) {
 449      $args = func_get_args();
 450      $data = call_user_func_array(
 451        array($this, 'loadRawDataWhere'),
 452        $args);
 453      return $this->loadAllFromArray($data);
 454    }
 455  
 456  
 457    /**
 458     * Load a single object identified by a 'WHERE' clause. You provide
 459     * everything after the 'WHERE', and Lisk builds the first half of the
 460     * query. See loadAllWhere(). This method is similar, but returns a single
 461     * result instead of a list.
 462     *
 463     * @param  string    queryfx()-style SQL WHERE clause.
 464     * @param  ...       Zero or more conversions.
 465     * @return obj|null  Matching object, or null if no object matches.
 466     *
 467     * @task   load
 468     */
 469    public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) {
 470      $args = func_get_args();
 471      $data = call_user_func_array(
 472        array($this, 'loadRawDataWhere'),
 473        $args);
 474  
 475      if (count($data) > 1) {
 476        throw new AphrontCountQueryException(
 477          'More than 1 result from loadOneWhere()!');
 478      }
 479  
 480      $data = reset($data);
 481      if (!$data) {
 482        return null;
 483      }
 484  
 485      return $this->loadFromArray($data);
 486    }
 487  
 488  
 489    protected function loadRawDataWhere($pattern /* , $args... */) {
 490      $connection = $this->establishConnection('r');
 491  
 492      $lock_clause = '';
 493      if ($connection->isReadLocking()) {
 494        $lock_clause = 'FOR UPDATE';
 495      } else if ($connection->isWriteLocking()) {
 496        $lock_clause = 'LOCK IN SHARE MODE';
 497      }
 498  
 499      $args = func_get_args();
 500      $args = array_slice($args, 1);
 501  
 502      $pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q';
 503      array_unshift($args, $this->getTableName());
 504      array_push($args, $lock_clause);
 505      array_unshift($args, $pattern);
 506  
 507      return call_user_func_array(
 508        array($connection, 'queryData'),
 509        $args);
 510    }
 511  
 512  
 513    /**
 514     * Reload an object from the database, discarding any changes to persistent
 515     * properties. This is primarily useful after entering a transaction but
 516     * before applying changes to an object.
 517     *
 518     * @return this
 519     *
 520     * @task   load
 521     */
 522    public function reload() {
 523      if (!$this->getID()) {
 524        throw new Exception("Unable to reload object that hasn't been loaded!");
 525      }
 526  
 527      $result = $this->loadOneWhere(
 528        '%C = %d',
 529        $this->getIDKeyForUse(),
 530        $this->getID());
 531  
 532      if (!$result) {
 533        throw new AphrontObjectMissingQueryException();
 534      }
 535  
 536      return $this;
 537    }
 538  
 539  
 540    /**
 541     * Initialize this object's properties from a dictionary. Generally, you
 542     * load single objects with loadOneWhere(), but sometimes it may be more
 543     * convenient to pull data from elsewhere directly (e.g., a complicated
 544     * join via @{method:queryData}) and then load from an array representation.
 545     *
 546     * @param  dict  Dictionary of properties, which should be equivalent to
 547     *               selecting a row from the table or calling
 548     *               @{method:getProperties}.
 549     * @return this
 550     *
 551     * @task   load
 552     */
 553    public function loadFromArray(array $row) {
 554      static $valid_properties = array();
 555  
 556      $map = array();
 557      foreach ($row as $k => $v) {
 558        // We permit (but ignore) extra properties in the array because a
 559        // common approach to building the array is to issue a raw SELECT query
 560        // which may include extra explicit columns or joins.
 561  
 562        // This pathway is very hot on some pages, so we're inlining a cache
 563        // and doing some microoptimization to avoid a strtolower() call for each
 564        // assignment. The common path (assigning a valid property which we've
 565        // already seen) always incurs only one empty(). The second most common
 566        // path (assigning an invalid property which we've already seen) costs
 567        // an empty() plus an isset().
 568  
 569        if (empty($valid_properties[$k])) {
 570          if (isset($valid_properties[$k])) {
 571            // The value is set but empty, which means it's false, so we've
 572            // already determined it's not valid. We don't need to check again.
 573            continue;
 574          }
 575          $valid_properties[$k] = $this->hasProperty($k);
 576          if (!$valid_properties[$k]) {
 577            continue;
 578          }
 579        }
 580  
 581        $map[$k] = $v;
 582      }
 583  
 584      $this->willReadData($map);
 585  
 586      foreach ($map as $prop => $value) {
 587        $this->$prop = $value;
 588      }
 589  
 590      $this->didReadData();
 591  
 592      return $this;
 593    }
 594  
 595  
 596    /**
 597     * Initialize a list of objects from a list of dictionaries. Usually you
 598     * load lists of objects with @{method:loadAllWhere}, but sometimes that
 599     * isn't flexible enough. One case is if you need to do joins to select the
 600     * right objects:
 601     *
 602     *   function loadAllWithOwner($owner) {
 603     *     $data = $this->queryData(
 604     *       'SELECT d.*
 605     *         FROM owner o
 606     *           JOIN owner_has_dog od ON o.id = od.ownerID
 607     *           JOIN dog d ON od.dogID = d.id
 608     *         WHERE o.id = %d',
 609     *       $owner);
 610     *     return $this->loadAllFromArray($data);
 611     *   }
 612     *
 613     * This is a lot messier than @{method:loadAllWhere}, but more flexible.
 614     *
 615     * @param  list  List of property dictionaries.
 616     * @return dict  List of constructed objects, keyed on ID.
 617     *
 618     * @task   load
 619     */
 620    public function loadAllFromArray(array $rows) {
 621      $result = array();
 622  
 623      $id_key = $this->getIDKey();
 624  
 625      foreach ($rows as $row) {
 626        $obj = clone $this;
 627        if ($id_key && isset($row[$id_key])) {
 628          $result[$row[$id_key]] = $obj->loadFromArray($row);
 629        } else {
 630          $result[] = $obj->loadFromArray($row);
 631        }
 632        if ($this->inSet) {
 633          $this->inSet->addToSet($obj);
 634        }
 635      }
 636  
 637      return $result;
 638    }
 639  
 640    /**
 641     * This method helps to prevent the 1+N queries problem. It happens when you
 642     * execute a query for each row in a result set. Like in this code:
 643     *
 644     *   COUNTEREXAMPLE, name=Easy to write but expensive to execute
 645     *   $diffs = id(new DifferentialDiff())->loadAllWhere(
 646     *     'revisionID = %d',
 647     *     $revision->getID());
 648     *   foreach ($diffs as $diff) {
 649     *     $changesets = id(new DifferentialChangeset())->loadAllWhere(
 650     *       'diffID = %d',
 651     *       $diff->getID());
 652     *     // Do something with $changesets.
 653     *   }
 654     *
 655     * One can solve this problem by reading all the dependent objects at once and
 656     * assigning them later:
 657     *
 658     *   COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain
 659     *   $diffs = id(new DifferentialDiff())->loadAllWhere(
 660     *     'revisionID = %d',
 661     *     $revision->getID());
 662     *   $all_changesets = id(new DifferentialChangeset())->loadAllWhere(
 663     *     'diffID IN (%Ld)',
 664     *     mpull($diffs, 'getID'));
 665     *   $all_changesets = mgroup($all_changesets, 'getDiffID');
 666     *   foreach ($diffs as $diff) {
 667     *     $changesets = idx($all_changesets, $diff->getID(), array());
 668     *     // Do something with $changesets.
 669     *   }
 670     *
 671     * The method @{method:loadRelatives} abstracts this approach which allows
 672     * writing a code which is simple and efficient at the same time:
 673     *
 674     *   name=Easy to write and cheap to execute
 675     *   $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
 676     *   foreach ($diffs as $diff) {
 677     *     $changesets = $diff->loadRelatives(
 678     *       new DifferentialChangeset(),
 679     *       'diffID');
 680     *     // Do something with $changesets.
 681     *   }
 682     *
 683     * This will load dependent objects for all diffs in the first call of
 684     * @{method:loadRelatives} and use this result for all following calls.
 685     *
 686     * The method supports working with set of sets, like in this code:
 687     *
 688     *   $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
 689     *   foreach ($diffs as $diff) {
 690     *     $changesets = $diff->loadRelatives(
 691     *       new DifferentialChangeset(),
 692     *       'diffID');
 693     *     foreach ($changesets as $changeset) {
 694     *       $hunks = $changeset->loadRelatives(
 695     *         new DifferentialHunk(),
 696     *         'changesetID');
 697     *       // Do something with hunks.
 698     *     }
 699     *   }
 700     *
 701     * This code will execute just three queries - one to load all diffs, one to
 702     * load all their related changesets and one to load all their related hunks.
 703     * You can try to write an equivalent code without using this method as
 704     * a homework.
 705     *
 706     * The method also supports retrieving referenced objects, for example authors
 707     * of all diffs (using shortcut @{method:loadOneRelative}):
 708     *
 709     *   foreach ($diffs as $diff) {
 710     *     $author = $diff->loadOneRelative(
 711     *       new PhabricatorUser(),
 712     *       'phid',
 713     *       'getAuthorPHID');
 714     *     // Do something with author.
 715     *   }
 716     *
 717     * It is also possible to specify additional conditions for the `WHERE`
 718     * clause. Similarly to @{method:loadAllWhere}, you can specify everything
 719     * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is
 720     * allowed to pass only a constant string (`%` doesn't have a special
 721     * meaning). This is intentional to avoid mistakes with using data from one
 722     * row in retrieving other rows. Example of a correct usage:
 723     *
 724     *   $status = $author->loadOneRelative(
 725     *     new PhabricatorCalendarEvent(),
 726     *     'userPHID',
 727     *     'getPHID',
 728     *     '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)');
 729     *
 730     * @param  LiskDAO  Type of objects to load.
 731     * @param  string   Name of the column in target table.
 732     * @param  string   Method name in this table.
 733     * @param  string   Additional constraints on returned rows. It supports no
 734     *                  placeholders and requires putting the WHERE part into
 735     *                  parentheses. It's not possible to use LIMIT.
 736     * @return list     Objects of type $object.
 737     *
 738     * @task   load
 739     */
 740    public function loadRelatives(
 741      LiskDAO $object,
 742      $foreign_column,
 743      $key_method = 'getID',
 744      $where = '') {
 745  
 746      if (!$this->inSet) {
 747        id(new LiskDAOSet())->addToSet($this);
 748      }
 749      $relatives = $this->inSet->loadRelatives(
 750        $object,
 751        $foreign_column,
 752        $key_method,
 753        $where);
 754      return idx($relatives, $this->$key_method(), array());
 755    }
 756  
 757    /**
 758     * Load referenced row. See @{method:loadRelatives} for details.
 759     *
 760     * @param  LiskDAO  Type of objects to load.
 761     * @param  string   Name of the column in target table.
 762     * @param  string   Method name in this table.
 763     * @param  string   Additional constraints on returned rows. It supports no
 764     *                  placeholders and requires putting the WHERE part into
 765     *                  parentheses. It's not possible to use LIMIT.
 766     * @return LiskDAO  Object of type $object or null if there's no such object.
 767     *
 768     * @task   load
 769     */
 770    final public function loadOneRelative(
 771      LiskDAO $object,
 772      $foreign_column,
 773      $key_method = 'getID',
 774      $where = '') {
 775  
 776      $relatives = $this->loadRelatives(
 777        $object,
 778        $foreign_column,
 779        $key_method,
 780        $where);
 781  
 782      if (!$relatives) {
 783        return null;
 784      }
 785  
 786      if (count($relatives) > 1) {
 787        throw new AphrontCountQueryException(
 788          'More than 1 result from loadOneRelative()!');
 789      }
 790  
 791      return reset($relatives);
 792    }
 793  
 794    final public function putInSet(LiskDAOSet $set) {
 795      $this->inSet = $set;
 796      return $this;
 797    }
 798  
 799    final protected function getInSet() {
 800      return $this->inSet;
 801    }
 802  
 803  
 804  /* -(  Examining Objects  )-------------------------------------------------- */
 805  
 806  
 807    /**
 808     * Set unique ID identifying this object. You normally don't need to call this
 809     * method unless with `IDS_MANUAL`.
 810     *
 811     * @param  mixed   Unique ID.
 812     * @return this
 813     * @task   save
 814     */
 815    public function setID($id) {
 816      static $id_key = null;
 817      if ($id_key === null) {
 818        $id_key = $this->getIDKeyForUse();
 819      }
 820      $this->$id_key = $id;
 821      return $this;
 822    }
 823  
 824  
 825    /**
 826     * Retrieve the unique ID identifying this object. This value will be null if
 827     * the object hasn't been persisted and you didn't set it manually.
 828     *
 829     * @return mixed   Unique ID.
 830     *
 831     * @task   info
 832     */
 833    public function getID() {
 834      static $id_key = null;
 835      if ($id_key === null) {
 836        $id_key = $this->getIDKeyForUse();
 837      }
 838      return $this->$id_key;
 839    }
 840  
 841  
 842    public function getPHID() {
 843      return $this->phid;
 844    }
 845  
 846  
 847    /**
 848     * Test if a property exists.
 849     *
 850     * @param   string    Property name.
 851     * @return  bool      True if the property exists.
 852     * @task info
 853     */
 854    public function hasProperty($property) {
 855      return (bool)$this->checkProperty($property);
 856    }
 857  
 858  
 859    /**
 860     * Retrieve a list of all object properties. This list only includes
 861     * properties that are declared as protected, and it is expected that
 862     * all properties returned by this function should be persisted to the
 863     * database.
 864     * Properties that should not be persisted must be declared as private.
 865     *
 866     * @return dict  Dictionary of normalized (lowercase) to canonical (original
 867     *               case) property names.
 868     *
 869     * @task   info
 870     */
 871    protected function getAllLiskProperties() {
 872      static $properties = null;
 873      if (!isset($properties)) {
 874        $class = new ReflectionClass(get_class($this));
 875        $properties = array();
 876        foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
 877          $properties[strtolower($p->getName())] = $p->getName();
 878        }
 879  
 880        $id_key = $this->getIDKey();
 881        if ($id_key != 'id') {
 882          unset($properties['id']);
 883        }
 884  
 885        if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
 886          unset($properties['datecreated']);
 887          unset($properties['datemodified']);
 888        }
 889  
 890        if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {
 891          unset($properties['phid']);
 892        }
 893      }
 894      return $properties;
 895    }
 896  
 897  
 898    /**
 899     * Check if a property exists on this object.
 900     *
 901     * @return string|null   Canonical property name, or null if the property
 902     *                       does not exist.
 903     *
 904     * @task   info
 905     */
 906    protected function checkProperty($property) {
 907      static $properties = null;
 908      if ($properties === null) {
 909        $properties = $this->getAllLiskProperties();
 910      }
 911  
 912      $property = strtolower($property);
 913      if (empty($properties[$property])) {
 914        return null;
 915      }
 916  
 917      return $properties[$property];
 918    }
 919  
 920  
 921    /**
 922     * Get or build the database connection for this object.
 923     *
 924     * @param  string 'r' for read, 'w' for read/write.
 925     * @param  bool True to force a new connection. The connection will not
 926     *              be retrieved from or saved into the connection cache.
 927     * @return LiskDatabaseConnection   Lisk connection object.
 928     *
 929     * @task   info
 930     */
 931    public function establishConnection($mode, $force_new = false) {
 932      if ($mode != 'r' && $mode != 'w') {
 933        throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'.");
 934      }
 935  
 936      if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) {
 937        $mode = 'isolate-'.$mode;
 938  
 939        $connection = $this->getEstablishedConnection($mode);
 940        if (!$connection) {
 941          $connection = $this->establishIsolatedConnection($mode);
 942          $this->setEstablishedConnection($mode, $connection);
 943        }
 944  
 945        return $connection;
 946      }
 947  
 948      if (self::shouldIsolateAllLiskEffectsToTransactions()) {
 949        // If we're doing fixture transaction isolation, force the mode to 'w'
 950        // so we always get the same connection for reads and writes, and thus
 951        // can see the writes inside the transaction.
 952        $mode = 'w';
 953      }
 954  
 955      // TODO: There is currently no protection on 'r' queries against writing.
 956  
 957      $connection = null;
 958      if (!$force_new) {
 959        if ($mode == 'r') {
 960          // If we're requesting a read connection but already have a write
 961          // connection, reuse the write connection so that reads can take place
 962          // inside transactions.
 963          $connection = $this->getEstablishedConnection('w');
 964        }
 965  
 966        if (!$connection) {
 967          $connection = $this->getEstablishedConnection($mode);
 968        }
 969      }
 970  
 971      if (!$connection) {
 972        $connection = $this->establishLiveConnection($mode);
 973        if (self::shouldIsolateAllLiskEffectsToTransactions()) {
 974          $connection->openTransaction();
 975        }
 976        $this->setEstablishedConnection(
 977          $mode,
 978          $connection,
 979          $force_unique = $force_new);
 980      }
 981  
 982      return $connection;
 983    }
 984  
 985  
 986    /**
 987     * Convert this object into a property dictionary. This dictionary can be
 988     * restored into an object by using @{method:loadFromArray} (unless you're
 989     * using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you
 990     * should just go ahead and die in a fire).
 991     *
 992     * @return dict  Dictionary of object properties.
 993     *
 994     * @task   info
 995     */
 996    protected function getAllLiskPropertyValues() {
 997      $map = array();
 998      foreach ($this->getAllLiskProperties() as $p) {
 999        // We may receive a warning here for properties we've implicitly added
1000        // through configuration; squelch it.
1001        $map[$p] = @$this->$p;
1002      }
1003      return $map;
1004    }
1005  
1006  
1007  /* -(  Writing Objects  )---------------------------------------------------- */
1008  
1009  
1010    /**
1011     * Make an object read-only.
1012     *
1013     * Making an object ephemeral indicates that you will be changing state in
1014     * such a way that you would never ever want it to be written back to the
1015     * storage.
1016     */
1017    public function makeEphemeral() {
1018      $this->ephemeral = true;
1019      return $this;
1020    }
1021  
1022    private function isEphemeralCheck() {
1023      if ($this->ephemeral) {
1024        throw new LiskEphemeralObjectException();
1025      }
1026    }
1027  
1028    /**
1029     * Persist this object to the database. In most cases, this is the only
1030     * method you need to call to do writes. If the object has not yet been
1031     * inserted this will do an insert; if it has, it will do an update.
1032     *
1033     * @return this
1034     *
1035     * @task   save
1036     */
1037    public function save() {
1038      if ($this->shouldInsertWhenSaved()) {
1039        return $this->insert();
1040      } else {
1041        return $this->update();
1042      }
1043    }
1044  
1045  
1046    /**
1047     * Save this object, forcing the query to use REPLACE regardless of object
1048     * state.
1049     *
1050     * @return this
1051     *
1052     * @task   save
1053     */
1054    public function replace() {
1055      $this->isEphemeralCheck();
1056      return $this->insertRecordIntoDatabase('REPLACE');
1057    }
1058  
1059  
1060    /**
1061     *  Save this object, forcing the query to use INSERT regardless of object
1062     *  state.
1063     *
1064     *  @return this
1065     *
1066     *  @task   save
1067     */
1068    public function insert() {
1069      $this->isEphemeralCheck();
1070      return $this->insertRecordIntoDatabase('INSERT');
1071    }
1072  
1073  
1074    /**
1075     *  Save this object, forcing the query to use UPDATE regardless of object
1076     *  state.
1077     *
1078     *  @return this
1079     *
1080     *  @task   save
1081     */
1082    public function update() {
1083      $this->isEphemeralCheck();
1084  
1085      $this->willSaveObject();
1086      $data = $this->getAllLiskPropertyValues();
1087      $this->willWriteData($data);
1088  
1089      $map = array();
1090      foreach ($data as $k => $v) {
1091        $map[$k] = $v;
1092      }
1093  
1094      $conn = $this->establishConnection('w');
1095      $binary = $this->getBinaryColumns();
1096  
1097      foreach ($map as $key => $value) {
1098        if (!empty($binary[$key])) {
1099          $map[$key] = qsprintf($conn, '%C = %nB', $key, $value);
1100        } else {
1101          $map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
1102        }
1103      }
1104      $map = implode(', ', $map);
1105  
1106      $id = $this->getID();
1107      $conn->query(
1108        'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'),
1109        $this->getTableName(),
1110        $map,
1111        $this->getIDKeyForUse(),
1112        $id);
1113      // We can't detect a missing object because updating an object without
1114      // changing any values doesn't affect rows. We could jiggle timestamps
1115      // to catch this for objects which track them if we wanted.
1116  
1117      $this->didWriteData();
1118  
1119      return $this;
1120    }
1121  
1122  
1123    /**
1124     * Delete this object, permanently.
1125     *
1126     * @return this
1127     *
1128     * @task   save
1129     */
1130    public function delete() {
1131      $this->isEphemeralCheck();
1132      $this->willDelete();
1133  
1134      $conn = $this->establishConnection('w');
1135      $conn->query(
1136        'DELETE FROM %T WHERE %C = %d',
1137        $this->getTableName(),
1138        $this->getIDKeyForUse(),
1139        $this->getID());
1140  
1141      $this->didDelete();
1142  
1143      return $this;
1144    }
1145  
1146    /**
1147     * Internal implementation of INSERT and REPLACE.
1148     *
1149     * @param  const   Either "INSERT" or "REPLACE", to force the desired mode.
1150     *
1151     * @task   save
1152     */
1153    protected function insertRecordIntoDatabase($mode) {
1154      $this->willSaveObject();
1155      $data = $this->getAllLiskPropertyValues();
1156  
1157      $conn = $this->establishConnection('w');
1158  
1159      $id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
1160      switch ($id_mechanism) {
1161        case self::IDS_AUTOINCREMENT:
1162          // If we are using autoincrement IDs, let MySQL assign the value for the
1163          // ID column, if it is empty. If the caller has explicitly provided a
1164          // value, use it.
1165          $id_key = $this->getIDKeyForUse();
1166          if (empty($data[$id_key])) {
1167            unset($data[$id_key]);
1168          }
1169          break;
1170        case self::IDS_COUNTER:
1171          // If we are using counter IDs, assign a new ID if we don't already have
1172          // one.
1173          $id_key = $this->getIDKeyForUse();
1174          if (empty($data[$id_key])) {
1175            $counter_name = $this->getTableName();
1176            $id = self::loadNextCounterID($conn, $counter_name);
1177            $this->setID($id);
1178            $data[$id_key] = $id;
1179          }
1180          break;
1181        case self::IDS_MANUAL:
1182          break;
1183        default:
1184          throw new Exception('Unknown CONFIG_IDs mechanism!');
1185      }
1186  
1187      $this->willWriteData($data);
1188  
1189      $columns = array_keys($data);
1190      $binary = $this->getBinaryColumns();
1191  
1192      foreach ($data as $key => $value) {
1193        try {
1194          if (!empty($binary[$key])) {
1195            $data[$key] = qsprintf($conn, '%nB', $value);
1196          } else {
1197            $data[$key] = qsprintf($conn, '%ns', $value);
1198          }
1199        } catch (AphrontParameterQueryException $parameter_exception) {
1200          throw new PhutilProxyException(
1201            pht(
1202              "Unable to insert or update object of class %s, field '%s' ".
1203              "has a nonscalar value.",
1204              get_class($this),
1205              $key),
1206            $parameter_exception);
1207        }
1208      }
1209      $data = implode(', ', $data);
1210  
1211      $conn->query(
1212        '%Q INTO %T (%LC) VALUES (%Q)',
1213        $mode,
1214        $this->getTableName(),
1215        $columns,
1216        $data);
1217  
1218      // Only use the insert id if this table is using auto-increment ids
1219      if ($id_mechanism === self::IDS_AUTOINCREMENT) {
1220        $this->setID($conn->getInsertID());
1221      }
1222  
1223      $this->didWriteData();
1224  
1225      return $this;
1226    }
1227  
1228  
1229    /**
1230     * Method used to determine whether to insert or update when saving.
1231     *
1232     * @return bool true if the record should be inserted
1233     */
1234    protected function shouldInsertWhenSaved() {
1235      $key_type = $this->getConfigOption(self::CONFIG_IDS);
1236  
1237      if ($key_type == self::IDS_MANUAL) {
1238        throw new Exception(
1239          'You are using manual IDs. You must override the '.
1240          'shouldInsertWhenSaved() method to properly detect '.
1241          'when to insert a new record.');
1242      } else {
1243        return !$this->getID();
1244      }
1245    }
1246  
1247  
1248  /* -(  Hooks and Callbacks  )------------------------------------------------ */
1249  
1250  
1251    /**
1252     * Retrieve the database table name. By default, this is the class name.
1253     *
1254     * @return string  Table name for object storage.
1255     *
1256     * @task   hook
1257     */
1258    public function getTableName() {
1259      return get_class($this);
1260    }
1261  
1262  
1263    /**
1264     * Retrieve the primary key column, "id" by default. If you can not
1265     * reasonably name your ID column "id", override this method.
1266     *
1267     * @return string  Name of the ID column.
1268     *
1269     * @task   hook
1270     */
1271    public function getIDKey() {
1272      return 'id';
1273    }
1274  
1275  
1276    protected function getIDKeyForUse() {
1277      $id_key = $this->getIDKey();
1278      if (!$id_key) {
1279        throw new Exception(
1280          'This DAO does not have a single-part primary key. The method you '.
1281          'called requires a single-part primary key.');
1282      }
1283      return $id_key;
1284    }
1285  
1286  
1287    /**
1288     * Generate a new PHID, used by CONFIG_AUX_PHID.
1289     *
1290     * @return phid    Unique, newly allocated PHID.
1291     *
1292     * @task   hook
1293     */
1294    protected function generatePHID() {
1295      throw new Exception(
1296        'To use CONFIG_AUX_PHID, you need to overload '.
1297        'generatePHID() to perform PHID generation.');
1298    }
1299  
1300  
1301    /**
1302     * Hook to apply serialization or validation to data before it is written to
1303     * the database. See also @{method:willReadData}.
1304     *
1305     * @task hook
1306     */
1307    protected function willWriteData(array &$data) {
1308      $this->applyLiskDataSerialization($data, false);
1309    }
1310  
1311  
1312    /**
1313     * Hook to perform actions after data has been written to the database.
1314     *
1315     * @task hook
1316     */
1317    protected function didWriteData() {}
1318  
1319  
1320    /**
1321     * Hook to make internal object state changes prior to INSERT, REPLACE or
1322     * UPDATE.
1323     *
1324     * @task hook
1325     */
1326    protected function willSaveObject() {
1327      $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
1328  
1329      if ($use_timestamps) {
1330        if (!$this->getDateCreated()) {
1331          $this->setDateCreated(time());
1332        }
1333        $this->setDateModified(time());
1334      }
1335  
1336      if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) {
1337        $this->setPHID($this->generatePHID());
1338      }
1339    }
1340  
1341  
1342    /**
1343     * Hook to apply serialization or validation to data as it is read from the
1344     * database. See also @{method:willWriteData}.
1345     *
1346     * @task hook
1347     */
1348    protected function willReadData(array &$data) {
1349      $this->applyLiskDataSerialization($data, $deserialize = true);
1350    }
1351  
1352    /**
1353     * Hook to perform an action on data after it is read from the database.
1354     *
1355     * @task hook
1356     */
1357    protected function didReadData() {}
1358  
1359    /**
1360     * Hook to perform an action before the deletion of an object.
1361     *
1362     * @task hook
1363     */
1364    protected function willDelete() {}
1365  
1366    /**
1367     * Hook to perform an action after the deletion of an object.
1368     *
1369     * @task hook
1370     */
1371    protected function didDelete() {}
1372  
1373    /**
1374     * Reads the value from a field. Override this method for custom behavior
1375     * of @{method:getField} instead of overriding getField directly.
1376     *
1377     * @param  string  Canonical field name
1378     * @return mixed   Value of the field
1379     *
1380     * @task hook
1381     */
1382    protected function readField($field) {
1383      if (isset($this->$field)) {
1384        return $this->$field;
1385      }
1386      return null;
1387    }
1388  
1389    /**
1390     * Writes a value to a field. Override this method for custom behavior of
1391     * setField($value) instead of overriding setField directly.
1392     *
1393     * @param  string  Canonical field name
1394     * @param  mixed   Value to write
1395     *
1396     * @task hook
1397     */
1398    protected function writeField($field, $value) {
1399      $this->$field = $value;
1400    }
1401  
1402  
1403  /* -(  Manging Transactions  )----------------------------------------------- */
1404  
1405  
1406    /**
1407     * Increase transaction stack depth.
1408     *
1409     * @return this
1410     */
1411    public function openTransaction() {
1412      $this->establishConnection('w')->openTransaction();
1413      return $this;
1414    }
1415  
1416  
1417    /**
1418     * Decrease transaction stack depth, saving work.
1419     *
1420     * @return this
1421     */
1422    public function saveTransaction() {
1423      $this->establishConnection('w')->saveTransaction();
1424      return $this;
1425    }
1426  
1427  
1428    /**
1429     * Decrease transaction stack depth, discarding work.
1430     *
1431     * @return this
1432     */
1433    public function killTransaction() {
1434      $this->establishConnection('w')->killTransaction();
1435      return $this;
1436    }
1437  
1438  
1439    /**
1440     * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
1441     * other connections can not read them (this is an enormous oversimplification
1442     * of FOR UPDATE semantics; consult the MySQL documentation for details). To
1443     * end read locking, call @{method:endReadLocking}. For example:
1444     *
1445     *   $beach->openTransaction();
1446     *     $beach->beginReadLocking();
1447     *
1448     *       $beach->reload();
1449     *       $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
1450     *       $beach->save();
1451     *
1452     *     $beach->endReadLocking();
1453     *   $beach->saveTransaction();
1454     *
1455     * @return this
1456     * @task xaction
1457     */
1458    public function beginReadLocking() {
1459      $this->establishConnection('w')->beginReadLocking();
1460      return $this;
1461    }
1462  
1463  
1464    /**
1465     * Ends read-locking that began at an earlier @{method:beginReadLocking} call.
1466     *
1467     * @return this
1468     * @task xaction
1469     */
1470    public function endReadLocking() {
1471      $this->establishConnection('w')->endReadLocking();
1472      return $this;
1473    }
1474  
1475    /**
1476     * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
1477     * that other connections can not update or delete them (this is an
1478     * oversimplification of LOCK IN SHARE MODE semantics; consult the
1479     * MySQL documentation for details). To end write locking, call
1480     * @{method:endWriteLocking}.
1481     *
1482     * @return this
1483     * @task xaction
1484     */
1485    public function beginWriteLocking() {
1486      $this->establishConnection('w')->beginWriteLocking();
1487      return $this;
1488    }
1489  
1490  
1491    /**
1492     * Ends write-locking that began at an earlier @{method:beginWriteLocking}
1493     * call.
1494     *
1495     * @return this
1496     * @task xaction
1497     */
1498    public function endWriteLocking() {
1499      $this->establishConnection('w')->endWriteLocking();
1500      return $this;
1501    }
1502  
1503  
1504  /* -(  Isolation  )---------------------------------------------------------- */
1505  
1506  
1507    /**
1508     * @task isolate
1509     */
1510    public static function beginIsolateAllLiskEffectsToCurrentProcess() {
1511      self::$processIsolationLevel++;
1512    }
1513  
1514    /**
1515     * @task isolate
1516     */
1517    public static function endIsolateAllLiskEffectsToCurrentProcess() {
1518      self::$processIsolationLevel--;
1519      if (self::$processIsolationLevel < 0) {
1520        throw new Exception(
1521          'Lisk process isolation level was reduced below 0.');
1522      }
1523    }
1524  
1525    /**
1526     * @task isolate
1527     */
1528    public static function shouldIsolateAllLiskEffectsToCurrentProcess() {
1529      return (bool)self::$processIsolationLevel;
1530    }
1531  
1532    /**
1533     * @task isolate
1534     */
1535    private function establishIsolatedConnection($mode) {
1536      $config = array();
1537      return new AphrontIsolatedDatabaseConnection($config);
1538    }
1539  
1540    /**
1541     * @task isolate
1542     */
1543    public static function beginIsolateAllLiskEffectsToTransactions() {
1544      if (self::$transactionIsolationLevel === 0) {
1545        self::closeAllConnections();
1546      }
1547      self::$transactionIsolationLevel++;
1548    }
1549  
1550    /**
1551     * @task isolate
1552     */
1553    public static function endIsolateAllLiskEffectsToTransactions() {
1554      self::$transactionIsolationLevel--;
1555      if (self::$transactionIsolationLevel < 0) {
1556        throw new Exception(
1557          'Lisk transaction isolation level was reduced below 0.');
1558      } else if (self::$transactionIsolationLevel == 0) {
1559        foreach (self::$connections as $key => $conn) {
1560          if ($conn) {
1561            $conn->killTransaction();
1562          }
1563        }
1564        self::closeAllConnections();
1565      }
1566    }
1567  
1568    /**
1569     * @task isolate
1570     */
1571    public static function shouldIsolateAllLiskEffectsToTransactions() {
1572      return (bool)self::$transactionIsolationLevel;
1573    }
1574  
1575    public static function closeAllConnections() {
1576      self::$connections = array();
1577    }
1578  
1579  /* -(  Utilities  )---------------------------------------------------------- */
1580  
1581  
1582    /**
1583     * Applies configured serialization to a dictionary of values.
1584     *
1585     * @task util
1586     */
1587    protected function applyLiskDataSerialization(array &$data, $deserialize) {
1588      $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
1589      if ($serialization) {
1590        foreach (array_intersect_key($serialization, $data) as $col => $format) {
1591          switch ($format) {
1592            case self::SERIALIZATION_NONE:
1593              break;
1594            case self::SERIALIZATION_PHP:
1595              if ($deserialize) {
1596                $data[$col] = unserialize($data[$col]);
1597              } else {
1598                $data[$col] = serialize($data[$col]);
1599              }
1600              break;
1601            case self::SERIALIZATION_JSON:
1602              if ($deserialize) {
1603                $data[$col] = json_decode($data[$col], true);
1604              } else {
1605                $data[$col] = json_encode($data[$col]);
1606              }
1607              break;
1608            default:
1609              throw new Exception("Unknown serialization format '{$format}'.");
1610          }
1611        }
1612      }
1613    }
1614  
1615    /**
1616     * Black magic. Builds implied get*() and set*() for all properties.
1617     *
1618     * @param  string  Method name.
1619     * @param  list    Argument vector.
1620     * @return mixed   get*() methods return the property value. set*() methods
1621     *                 return $this.
1622     * @task   util
1623     */
1624    public function __call($method, $args) {
1625      // NOTE: PHP has a bug that static variables defined in __call() are shared
1626      // across all children classes. Call a different method to work around this
1627      // bug.
1628      return $this->call($method, $args);
1629    }
1630  
1631    /**
1632     * @task   util
1633     */
1634    final protected function call($method, $args) {
1635      // NOTE: This method is very performance-sensitive (many thousands of calls
1636      // per page on some pages), and thus has some silliness in the name of
1637      // optimizations.
1638  
1639      static $dispatch_map = array();
1640  
1641      if ($method[0] === 'g') {
1642        if (isset($dispatch_map[$method])) {
1643          $property = $dispatch_map[$method];
1644        } else {
1645          if (substr($method, 0, 3) !== 'get') {
1646            throw new Exception("Unable to resolve method '{$method}'!");
1647          }
1648          $property = substr($method, 3);
1649          if (!($property = $this->checkProperty($property))) {
1650            throw new Exception("Bad getter call: {$method}");
1651          }
1652          $dispatch_map[$method] = $property;
1653        }
1654  
1655        return $this->readField($property);
1656      }
1657  
1658      if ($method[0] === 's') {
1659        if (isset($dispatch_map[$method])) {
1660          $property = $dispatch_map[$method];
1661        } else {
1662          if (substr($method, 0, 3) !== 'set') {
1663            throw new Exception("Unable to resolve method '{$method}'!");
1664          }
1665          $property = substr($method, 3);
1666          $property = $this->checkProperty($property);
1667          if (!$property) {
1668            throw new Exception("Bad setter call: {$method}");
1669          }
1670          $dispatch_map[$method] = $property;
1671        }
1672  
1673        $this->writeField($property, $args[0]);
1674  
1675        return $this;
1676      }
1677  
1678      throw new Exception("Unable to resolve method '{$method}'.");
1679    }
1680  
1681    /**
1682     * Warns against writing to undeclared property.
1683     *
1684     * @task   util
1685     */
1686    public function __set($name, $value) {
1687      phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.');
1688      $this->$name = $value;
1689    }
1690  
1691    /**
1692     * Increments a named counter and returns the next value.
1693     *
1694     * @param   AphrontDatabaseConnection   Database where the counter resides.
1695     * @param   string                      Counter name to create or increment.
1696     * @return  int                         Next counter value.
1697     *
1698     * @task util
1699     */
1700    public static function loadNextCounterID(
1701      AphrontDatabaseConnection $conn_w,
1702      $counter_name) {
1703  
1704      // NOTE: If an insert does not touch an autoincrement row or call
1705      // LAST_INSERT_ID(), MySQL normally does not change the value of
1706      // LAST_INSERT_ID(). This can cause a counter's value to leak to a
1707      // new counter if the second counter is created after the first one is
1708      // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the
1709      // LAST_INSERT_ID() is always updated and always set correctly after the
1710      // query completes.
1711  
1712      queryfx(
1713        $conn_w,
1714        'INSERT INTO %T (counterName, counterValue) VALUES
1715            (%s, LAST_INSERT_ID(1))
1716          ON DUPLICATE KEY UPDATE
1717            counterValue = LAST_INSERT_ID(counterValue + 1)',
1718        self::COUNTER_TABLE_NAME,
1719        $counter_name);
1720  
1721      return $conn_w->getInsertID();
1722    }
1723  
1724    private function getBinaryColumns() {
1725      return $this->getConfigOption(self::CONFIG_BINARY);
1726    }
1727  
1728  
1729    public function getSchemaColumns() {
1730      $custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA);
1731      if (!$custom_map) {
1732        $custom_map = array();
1733      }
1734  
1735      $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
1736      if (!$serialization) {
1737        $serialization = array();
1738      }
1739  
1740      $serialization_map = array(
1741        self::SERIALIZATION_JSON => 'text',
1742        self::SERIALIZATION_PHP => 'bytes',
1743      );
1744  
1745      $binary_map = $this->getBinaryColumns();
1746  
1747      $id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
1748      if ($id_mechanism == self::IDS_AUTOINCREMENT) {
1749        $id_type = 'auto';
1750      } else {
1751        $id_type = 'id';
1752      }
1753  
1754      $builtin = array(
1755        'id' => $id_type,
1756        'phid' => 'phid',
1757        'viewPolicy' => 'policy',
1758        'editPolicy' => 'policy',
1759        'epoch' => 'epoch',
1760        'dateCreated' => 'epoch',
1761        'dateModified' => 'epoch',
1762      );
1763  
1764      $map = array();
1765      foreach ($this->getAllLiskProperties() as $property) {
1766        // First, use types specified explicitly in the table configuration.
1767        if (array_key_exists($property, $custom_map)) {
1768          $map[$property] = $custom_map[$property];
1769          continue;
1770        }
1771  
1772        // If we don't have an explicit type, try a builtin type for the
1773        // column.
1774        $type = idx($builtin, $property);
1775        if ($type) {
1776          $map[$property] = $type;
1777          continue;
1778        }
1779  
1780        // If the column has serialization, we can infer the column type.
1781        if (isset($serialization[$property])) {
1782          $type = idx($serialization_map, $serialization[$property]);
1783          if ($type) {
1784            $map[$property] = $type;
1785            continue;
1786          }
1787        }
1788  
1789        if (isset($binary_map[$property])) {
1790          $map[$property] = 'bytes';
1791          continue;
1792        }
1793  
1794        // If the column is named `somethingPHID`, infer it is a PHID.
1795        if (preg_match('/[a-z]PHID$/', $property)) {
1796          $map[$property] = 'phid';
1797          continue;
1798        }
1799  
1800        // If the column is named `somethingID`, infer it is an ID.
1801        if (preg_match('/[a-z]ID$/', $property)) {
1802          $map[$property] = 'id';
1803          continue;
1804        }
1805  
1806        // We don't know the type of this column.
1807        $map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN;
1808      }
1809  
1810      return $map;
1811    }
1812  
1813    public function getSchemaKeys() {
1814      $custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA);
1815      if (!$custom_map) {
1816        $custom_map = array();
1817      }
1818  
1819      $default_map = array();
1820      foreach ($this->getAllLiskProperties() as $property) {
1821        switch ($property) {
1822          case 'id':
1823            $default_map['PRIMARY'] = array(
1824              'columns' => array('id'),
1825              'unique' => true,
1826            );
1827            break;
1828          case 'phid':
1829            $default_map['key_phid'] = array(
1830              'columns' => array('phid'),
1831              'unique' => true,
1832            );
1833            break;
1834        }
1835      }
1836  
1837      return $custom_map + $default_map;
1838    }
1839  
1840  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1