@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<?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}