[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |