@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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