Path: blob/master/src/infrastructure/storage/lisk/LiskDAO.php
12241 views
<?php12/**3* Simple object-authoritative data access object that makes it easy to build4* stuff that you need to save to a database. Basically, it means that the5* amount of boilerplate code (and, particularly, boilerplate SQL) you need6* to write is greatly reduced.7*8* Lisk makes it fairly easy to build something quickly and end up with9* reasonably high-quality code when you're done (e.g., getters and setters,10* objects, transactions, reasonably structured OO code). It's also very thin:11* you can break past it and use MySQL and other lower-level tools when you12* need to in those couple of cases where it doesn't handle your workflow13* gracefully.14*15* However, Lisk won't scale past one database and lacks many of the features16* of modern DAOs like Hibernate: for instance, it does not support joins or17* polymorphic storage.18*19* This means that Lisk is well-suited for tools like Differential, but often a20* poor choice elsewhere. And it is strictly unsuitable for many projects.21*22* Lisk's model is object-authoritative: the PHP class definition is the23* master authority for what the object looks like.24*25* =Building New Objects=26*27* To create new Lisk objects, extend @{class:LiskDAO} and implement28* @{method:establishLiveConnection}. It should return an29* @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your30* objects.31*32* class Dog extends LiskDAO {33*34* protected $name;35* protected $breed;36*37* public function establishLiveConnection() {38* return $some_connection_object;39* }40* }41*42* Now, you should create your table:43*44* lang=sql45* CREATE TABLE dog (46* id int unsigned not null auto_increment primary key,47* name varchar(32) not null,48* breed varchar(32) not null,49* dateCreated int unsigned not null,50* dateModified int unsigned not null51* );52*53* For each property in your class, add a column with the same name to the table54* (see @{method:getConfiguration} for information about changing this mapping).55* Additionally, you should create the three columns `id`, `dateCreated` and56* `dateModified`. Lisk will automatically manage these, using them to implement57* autoincrement IDs and timestamps. If you do not want to use these features,58* see @{method:getConfiguration} for information on disabling them. At a bare59* minimum, you must normally have an `id` column which is a primary or unique60* key with a numeric type, although you can change its name by overriding61* @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to62* return null. Note that many methods rely on a single-part primary key and63* will no longer work (they will throw) if you disable it.64*65* As you add more properties to your class in the future, remember to add them66* to the database table as well.67*68* Lisk will now automatically handle these operations: getting and setting69* properties, saving objects, loading individual objects, loading groups70* of objects, updating objects, managing IDs, updating timestamps whenever71* an object is created or modified, and some additional specialized72* operations.73*74* = Creating, Retrieving, Updating, and Deleting =75*76* To create and persist a Lisk object, use @{method:save}:77*78* $dog = id(new Dog())79* ->setName('Sawyer')80* ->setBreed('Pug')81* ->save();82*83* Note that **Lisk automatically builds getters and setters for all of your84* object's protected properties** via @{method:__call}. If you want to add85* custom behavior to your getters or setters, you can do so by overriding the86* @{method:readField} and @{method:writeField} methods.87*88* Calling @{method:save} will persist the object to the database. After calling89* @{method:save}, you can call @{method:getID} to retrieve the object's ID.90*91* To load objects by ID, use the @{method:load} method:92*93* $dog = id(new Dog())->load($id);94*95* This will load the Dog record with ID $id into $dog, or `null` if no such96* record exists (@{method:load} is an instance method rather than a static97* method because PHP does not support late static binding, at least until PHP98* 5.3).99*100* To update an object, change its properties and save it:101*102* $dog->setBreed('Lab')->save();103*104* To delete an object, call @{method:delete}:105*106* $dog->delete();107*108* That's Lisk CRUD in a nutshell.109*110* = Queries =111*112* Often, you want to load a bunch of objects, or execute a more specialized113* query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:114*115* $pugs = $dog->loadAllWhere('breed = %s', 'Pug');116* $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');117*118* These methods work like @{function@arcanist:queryfx}, but only take half of119* a query (the part after the WHERE keyword). Lisk will handle the connection,120* columns, and object construction; you are responsible for the rest of it.121* @{method:loadAllWhere} returns a list of objects, while122* @{method:loadOneWhere} returns a single object (or `null`).123*124* There's also a @{method:loadRelatives} method which helps to prevent the 1+N125* queries problem.126*127* = Managing Transactions =128*129* Lisk uses a transaction stack, so code does not generally need to be aware130* of the transactional state of objects to implement correct transaction131* semantics:132*133* $obj->openTransaction();134* $obj->save();135* $other->save();136* // ...137* $other->openTransaction();138* $other->save();139* $another->save();140* if ($some_condition) {141* $other->saveTransaction();142* } else {143* $other->killTransaction();144* }145* // ...146* $obj->saveTransaction();147*148* Assuming ##$obj##, ##$other## and ##$another## live on the same database,149* this code will work correctly by establishing savepoints.150*151* Selects whose data are used later in the transaction should be included in152* @{method:beginReadLocking} or @{method:beginWriteLocking} block.153*154* @task conn Managing Connections155* @task config Configuring Lisk156* @task load Loading Objects157* @task info Examining Objects158* @task save Writing Objects159* @task hook Hooks and Callbacks160* @task util Utilities161* @task xaction Managing Transactions162* @task isolate Isolation for Unit Testing163*/164abstract class LiskDAO extends Phobject165implements AphrontDatabaseTableRefInterface {166167const CONFIG_IDS = 'id-mechanism';168const CONFIG_TIMESTAMPS = 'timestamps';169const CONFIG_AUX_PHID = 'auxiliary-phid';170const CONFIG_SERIALIZATION = 'col-serialization';171const CONFIG_BINARY = 'binary';172const CONFIG_COLUMN_SCHEMA = 'col-schema';173const CONFIG_KEY_SCHEMA = 'key-schema';174const CONFIG_NO_TABLE = 'no-table';175const CONFIG_NO_MUTATE = 'no-mutate';176177const SERIALIZATION_NONE = 'id';178const SERIALIZATION_JSON = 'json';179const SERIALIZATION_PHP = 'php';180181const IDS_AUTOINCREMENT = 'ids-auto';182const IDS_COUNTER = 'ids-counter';183const IDS_MANUAL = 'ids-manual';184185const COUNTER_TABLE_NAME = 'lisk_counter';186187private static $processIsolationLevel = 0;188private static $transactionIsolationLevel = 0;189190private $ephemeral = false;191private $forcedConnection;192193private static $connections = array();194195private static $liskMetadata = array();196197protected $id;198protected $phid;199protected $dateCreated;200protected $dateModified;201202/**203* Build an empty object.204*205* @return obj Empty object.206*/207public function __construct() {208$id_key = $this->getIDKey();209if ($id_key) {210$this->$id_key = null;211}212}213214215/* -( Managing Connections )----------------------------------------------- */216217218/**219* Establish a live connection to a database service. This method should220* return a new connection. Lisk handles connection caching and management;221* do not perform caching deeper in the stack.222*223* @param string Mode, either 'r' (reading) or 'w' (reading and writing).224* @return AphrontDatabaseConnection New database connection.225* @task conn226*/227abstract protected function establishLiveConnection($mode);228229230/**231* Return a namespace for this object's connections in the connection cache.232* Generally, the database name is appropriate. Two connections are considered233* equivalent if they have the same connection namespace and mode.234*235* @return string Connection namespace for cache236* @task conn237*/238protected function getConnectionNamespace() {239return $this->getDatabaseName();240}241242abstract protected function getDatabaseName();243244/**245* Get an existing, cached connection for this object.246*247* @param mode Connection mode.248* @return AphrontDatabaseConnection|null Connection, if it exists in cache.249* @task conn250*/251protected function getEstablishedConnection($mode) {252$key = $this->getConnectionNamespace().':'.$mode;253if (isset(self::$connections[$key])) {254return self::$connections[$key];255}256return null;257}258259260/**261* Store a connection in the connection cache.262*263* @param mode Connection mode.264* @param AphrontDatabaseConnection Connection to cache.265* @return this266* @task conn267*/268protected function setEstablishedConnection(269$mode,270AphrontDatabaseConnection $connection,271$force_unique = false) {272273$key = $this->getConnectionNamespace().':'.$mode;274275if ($force_unique) {276$key .= ':unique';277while (isset(self::$connections[$key])) {278$key .= '!';279}280}281282self::$connections[$key] = $connection;283return $this;284}285286287/**288* Force an object to use a specific connection.289*290* This overrides all connection management and forces the object to use291* a specific connection when interacting with the database.292*293* @param AphrontDatabaseConnection Connection to force this object to use.294* @task conn295*/296public function setForcedConnection(AphrontDatabaseConnection $connection) {297$this->forcedConnection = $connection;298return $this;299}300301302/* -( Configuring Lisk )--------------------------------------------------- */303304305/**306* Change Lisk behaviors, like ID configuration and timestamps. If you want307* to change these behaviors, you should override this method in your child308* class and change the options you're interested in. For example:309*310* protected function getConfiguration() {311* return array(312* Lisk_DataAccessObject::CONFIG_EXAMPLE => true,313* ) + parent::getConfiguration();314* }315*316* The available options are:317*318* CONFIG_IDS319* Lisk objects need to have a unique identifying ID. The three mechanisms320* available for generating this ID are IDS_AUTOINCREMENT (default, assumes321* the ID column is an autoincrement primary key), IDS_MANUAL (you are taking322* full responsibility for ID management), or IDS_COUNTER (see below).323*324* InnoDB does not persist the value of `auto_increment` across restarts,325* and instead initializes it to `MAX(id) + 1` during startup. This means it326* may reissue the same autoincrement ID more than once, if the row is deleted327* and then the database is restarted. To avoid this, you can set an object to328* use a counter table with IDS_COUNTER. This will generally behave like329* IDS_AUTOINCREMENT, except that the counter value will persist across330* restarts and inserts will be slightly slower. If a database stores any331* DAOs which use this mechanism, you must create a table there with this332* schema:333*334* CREATE TABLE lisk_counter (335* counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY,336* counterValue BIGINT UNSIGNED NOT NULL337* ) ENGINE=InnoDB DEFAULT CHARSET=utf8;338*339* CONFIG_TIMESTAMPS340* Lisk can automatically handle keeping track of a `dateCreated' and341* `dateModified' column, which it will update when it creates or modifies342* an object. If you don't want to do this, you may disable this option.343* By default, this option is ON.344*345* CONFIG_AUX_PHID346* This option can be enabled by being set to some truthy value. The meaning347* of this value is defined by your PHID generation mechanism. If this option348* is enabled, a `phid' property will be populated with a unique PHID when an349* object is created (or if it is saved and does not currently have one). You350* need to override generatePHID() and hook it into your PHID generation351* mechanism for this to work. By default, this option is OFF.352*353* CONFIG_SERIALIZATION354* You can optionally provide a column serialization map that will be applied355* to values when they are written to the database. For example:356*357* self::CONFIG_SERIALIZATION => array(358* 'complex' => self::SERIALIZATION_JSON,359* )360*361* This will cause Lisk to JSON-serialize the 'complex' field before it is362* written, and unserialize it when it is read.363*364* CONFIG_BINARY365* You can optionally provide a map of columns to a flag indicating that366* they store binary data. These columns will not raise an error when367* handling binary writes.368*369* CONFIG_COLUMN_SCHEMA370* Provide a map of columns to schema column types.371*372* CONFIG_KEY_SCHEMA373* Provide a map of key names to key specifications.374*375* CONFIG_NO_TABLE376* Allows you to specify that this object does not actually have a table in377* the database.378*379* CONFIG_NO_MUTATE380* Provide a map of columns which should not be included in UPDATE statements.381* If you have some columns which are always written to explicitly and should382* never be overwritten by a save(), you can specify them here. This is an383* advanced, specialized feature and there are usually better approaches for384* most locking/contention problems.385*386* @return dictionary Map of configuration options to values.387*388* @task config389*/390protected function getConfiguration() {391return array(392self::CONFIG_IDS => self::IDS_AUTOINCREMENT,393self::CONFIG_TIMESTAMPS => true,394);395}396397398/**399* Determine the setting of a configuration option for this class of objects.400*401* @param const Option name, one of the CONFIG_* constants.402* @return mixed Option value, if configured (null if unavailable).403*404* @task config405*/406public function getConfigOption($option_name) {407$options = $this->getLiskMetadata('config');408409if ($options === null) {410$options = $this->getConfiguration();411$this->setLiskMetadata('config', $options);412}413414return idx($options, $option_name);415}416417418/* -( Loading Objects )---------------------------------------------------- */419420421/**422* Load an object by ID. You need to invoke this as an instance method, not423* a class method, because PHP doesn't have late static binding (until424* PHP 5.3.0). For example:425*426* $dog = id(new Dog())->load($dog_id);427*428* @param int Numeric ID identifying the object to load.429* @return obj|null Identified object, or null if it does not exist.430*431* @task load432*/433public function load($id) {434if (is_object($id)) {435$id = (string)$id;436}437438if (!$id || (!is_int($id) && !ctype_digit($id))) {439return null;440}441442return $this->loadOneWhere(443'%C = %d',444$this->getIDKey(),445$id);446}447448449/**450* Loads all of the objects, unconditionally.451*452* @return dict Dictionary of all persisted objects of this type, keyed453* on object ID.454*455* @task load456*/457public function loadAll() {458return $this->loadAllWhere('1 = 1');459}460461462/**463* Load all objects which match a WHERE clause. You provide everything after464* the 'WHERE'; Lisk handles everything up to it. For example:465*466* $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);467*468* The pattern and arguments are as per queryfx().469*470* @param string queryfx()-style SQL WHERE clause.471* @param ... Zero or more conversions.472* @return dict Dictionary of matching objects, keyed on ID.473*474* @task load475*/476public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) {477$args = func_get_args();478$data = call_user_func_array(479array($this, 'loadRawDataWhere'),480$args);481return $this->loadAllFromArray($data);482}483484485/**486* Load a single object identified by a 'WHERE' clause. You provide487* everything after the 'WHERE', and Lisk builds the first half of the488* query. See loadAllWhere(). This method is similar, but returns a single489* result instead of a list.490*491* @param string queryfx()-style SQL WHERE clause.492* @param ... Zero or more conversions.493* @return obj|null Matching object, or null if no object matches.494*495* @task load496*/497public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) {498$args = func_get_args();499$data = call_user_func_array(500array($this, 'loadRawDataWhere'),501$args);502503if (count($data) > 1) {504throw new AphrontCountQueryException(505pht(506'More than one result from %s!',507__FUNCTION__.'()'));508}509510$data = reset($data);511if (!$data) {512return null;513}514515return $this->loadFromArray($data);516}517518519protected function loadRawDataWhere($pattern /* , $args... */) {520$conn = $this->establishConnection('r');521522if ($conn->isReadLocking()) {523$lock_clause = qsprintf($conn, 'FOR UPDATE');524} else if ($conn->isWriteLocking()) {525$lock_clause = qsprintf($conn, 'LOCK IN SHARE MODE');526} else {527$lock_clause = qsprintf($conn, '');528}529530$args = func_get_args();531$args = array_slice($args, 1);532533$pattern = 'SELECT * FROM %R WHERE '.$pattern.' %Q';534array_unshift($args, $this);535array_push($args, $lock_clause);536array_unshift($args, $pattern);537538return call_user_func_array(array($conn, 'queryData'), $args);539}540541542/**543* Reload an object from the database, discarding any changes to persistent544* properties. This is primarily useful after entering a transaction but545* before applying changes to an object.546*547* @return this548*549* @task load550*/551public function reload() {552if (!$this->getID()) {553throw new Exception(554pht("Unable to reload object that hasn't been loaded!"));555}556557$result = $this->loadOneWhere(558'%C = %d',559$this->getIDKey(),560$this->getID());561562if (!$result) {563throw new AphrontObjectMissingQueryException();564}565566return $this;567}568569570/**571* Initialize this object's properties from a dictionary. Generally, you572* load single objects with loadOneWhere(), but sometimes it may be more573* convenient to pull data from elsewhere directly (e.g., a complicated574* join via @{method:queryData}) and then load from an array representation.575*576* @param dict Dictionary of properties, which should be equivalent to577* selecting a row from the table or calling578* @{method:getProperties}.579* @return this580*581* @task load582*/583public function loadFromArray(array $row) {584$valid_map = $this->getLiskMetadata('validMap', array());585586$map = array();587$updated = false;588foreach ($row as $k => $v) {589// We permit (but ignore) extra properties in the array because a590// common approach to building the array is to issue a raw SELECT query591// which may include extra explicit columns or joins.592593// This pathway is very hot on some pages, so we're inlining a cache594// and doing some microoptimization to avoid a strtolower() call for each595// assignment. The common path (assigning a valid property which we've596// already seen) always incurs only one empty(). The second most common597// path (assigning an invalid property which we've already seen) costs598// an empty() plus an isset().599600if (empty($valid_map[$k])) {601if (isset($valid_map[$k])) {602// The value is set but empty, which means it's false, so we've603// already determined it's not valid. We don't need to check again.604continue;605}606$valid_map[$k] = $this->hasProperty($k);607$updated = true;608if (!$valid_map[$k]) {609continue;610}611}612613$map[$k] = $v;614}615616if ($updated) {617$this->setLiskMetadata('validMap', $valid_map);618}619620$this->willReadData($map);621622foreach ($map as $prop => $value) {623$this->$prop = $value;624}625626$this->didReadData();627628return $this;629}630631632/**633* Initialize a list of objects from a list of dictionaries. Usually you634* load lists of objects with @{method:loadAllWhere}, but sometimes that635* isn't flexible enough. One case is if you need to do joins to select the636* right objects:637*638* function loadAllWithOwner($owner) {639* $data = $this->queryData(640* 'SELECT d.*641* FROM owner o642* JOIN owner_has_dog od ON o.id = od.ownerID643* JOIN dog d ON od.dogID = d.id644* WHERE o.id = %d',645* $owner);646* return $this->loadAllFromArray($data);647* }648*649* This is a lot messier than @{method:loadAllWhere}, but more flexible.650*651* @param list List of property dictionaries.652* @return dict List of constructed objects, keyed on ID.653*654* @task load655*/656public function loadAllFromArray(array $rows) {657$result = array();658659$id_key = $this->getIDKey();660661foreach ($rows as $row) {662$obj = clone $this;663if ($id_key && isset($row[$id_key])) {664$row_id = $row[$id_key];665666if (isset($result[$row_id])) {667throw new Exception(668pht(669'Rows passed to "loadAllFromArray(...)" include two or more '.670'rows with the same ID ("%s"). Rows must have unique IDs. '.671'An underlying query may be missing a GROUP BY.',672$row_id));673}674675$result[$row_id] = $obj->loadFromArray($row);676} else {677$result[] = $obj->loadFromArray($row);678}679}680681return $result;682}683684685/* -( Examining Objects )-------------------------------------------------- */686687688/**689* Set unique ID identifying this object. You normally don't need to call this690* method unless with `IDS_MANUAL`.691*692* @param mixed Unique ID.693* @return this694* @task save695*/696public function setID($id) {697$id_key = $this->getIDKey();698$this->$id_key = $id;699return $this;700}701702703/**704* Retrieve the unique ID identifying this object. This value will be null if705* the object hasn't been persisted and you didn't set it manually.706*707* @return mixed Unique ID.708*709* @task info710*/711public function getID() {712$id_key = $this->getIDKey();713return $this->$id_key;714}715716717public function getPHID() {718return $this->phid;719}720721722/**723* Test if a property exists.724*725* @param string Property name.726* @return bool True if the property exists.727* @task info728*/729public function hasProperty($property) {730return (bool)$this->checkProperty($property);731}732733734/**735* Retrieve a list of all object properties. This list only includes736* properties that are declared as protected, and it is expected that737* all properties returned by this function should be persisted to the738* database.739* Properties that should not be persisted must be declared as private.740*741* @return dict Dictionary of normalized (lowercase) to canonical (original742* case) property names.743*744* @task info745*/746protected function getAllLiskProperties() {747$properties = $this->getLiskMetadata('properties');748749if ($properties === null) {750$class = new ReflectionClass(static::class);751$properties = array();752foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {753$properties[strtolower($p->getName())] = $p->getName();754}755756$id_key = $this->getIDKey();757if ($id_key != 'id') {758unset($properties['id']);759}760761if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) {762unset($properties['datecreated']);763unset($properties['datemodified']);764}765766if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {767unset($properties['phid']);768}769770$this->setLiskMetadata('properties', $properties);771}772773return $properties;774}775776777/**778* Check if a property exists on this object.779*780* @return string|null Canonical property name, or null if the property781* does not exist.782*783* @task info784*/785protected function checkProperty($property) {786$properties = $this->getAllLiskProperties();787788$property = strtolower($property);789if (empty($properties[$property])) {790return null;791}792793return $properties[$property];794}795796797/**798* Get or build the database connection for this object.799*800* @param string 'r' for read, 'w' for read/write.801* @param bool True to force a new connection. The connection will not802* be retrieved from or saved into the connection cache.803* @return AphrontDatabaseConnection Lisk connection object.804*805* @task info806*/807public function establishConnection($mode, $force_new = false) {808if ($mode != 'r' && $mode != 'w') {809throw new Exception(810pht(811"Unknown mode '%s', should be 'r' or 'w'.",812$mode));813}814815if ($this->forcedConnection) {816return $this->forcedConnection;817}818819if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) {820$mode = 'isolate-'.$mode;821822$connection = $this->getEstablishedConnection($mode);823if (!$connection) {824$connection = $this->establishIsolatedConnection($mode);825$this->setEstablishedConnection($mode, $connection);826}827828return $connection;829}830831if (self::shouldIsolateAllLiskEffectsToTransactions()) {832// If we're doing fixture transaction isolation, force the mode to 'w'833// so we always get the same connection for reads and writes, and thus834// can see the writes inside the transaction.835$mode = 'w';836}837838// TODO: There is currently no protection on 'r' queries against writing.839840$connection = null;841if (!$force_new) {842if ($mode == 'r') {843// If we're requesting a read connection but already have a write844// connection, reuse the write connection so that reads can take place845// inside transactions.846$connection = $this->getEstablishedConnection('w');847}848849if (!$connection) {850$connection = $this->getEstablishedConnection($mode);851}852}853854if (!$connection) {855$connection = $this->establishLiveConnection($mode);856if (self::shouldIsolateAllLiskEffectsToTransactions()) {857$connection->openTransaction();858}859$this->setEstablishedConnection(860$mode,861$connection,862$force_unique = $force_new);863}864865return $connection;866}867868869/**870* Convert this object into a property dictionary. This dictionary can be871* restored into an object by using @{method:loadFromArray} (unless you're872* using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you873* should just go ahead and die in a fire).874*875* @return dict Dictionary of object properties.876*877* @task info878*/879protected function getAllLiskPropertyValues() {880$map = array();881foreach ($this->getAllLiskProperties() as $p) {882// We may receive a warning here for properties we've implicitly added883// through configuration; squelch it.884$map[$p] = @$this->$p;885}886return $map;887}888889890/* -( Writing Objects )---------------------------------------------------- */891892893/**894* Make an object read-only.895*896* Making an object ephemeral indicates that you will be changing state in897* such a way that you would never ever want it to be written back to the898* storage.899*/900public function makeEphemeral() {901$this->ephemeral = true;902return $this;903}904905private function isEphemeralCheck() {906if ($this->ephemeral) {907throw new LiskEphemeralObjectException();908}909}910911/**912* Persist this object to the database. In most cases, this is the only913* method you need to call to do writes. If the object has not yet been914* inserted this will do an insert; if it has, it will do an update.915*916* @return this917*918* @task save919*/920public function save() {921if ($this->shouldInsertWhenSaved()) {922return $this->insert();923} else {924return $this->update();925}926}927928929/**930* Save this object, forcing the query to use REPLACE regardless of object931* state.932*933* @return this934*935* @task save936*/937public function replace() {938$this->isEphemeralCheck();939return $this->insertRecordIntoDatabase('REPLACE');940}941942943/**944* Save this object, forcing the query to use INSERT regardless of object945* state.946*947* @return this948*949* @task save950*/951public function insert() {952$this->isEphemeralCheck();953return $this->insertRecordIntoDatabase('INSERT');954}955956957/**958* Save this object, forcing the query to use UPDATE regardless of object959* state.960*961* @return this962*963* @task save964*/965public function update() {966$this->isEphemeralCheck();967968$this->willSaveObject();969$data = $this->getAllLiskPropertyValues();970971// Remove columns flagged as nonmutable from the update statement.972$no_mutate = $this->getConfigOption(self::CONFIG_NO_MUTATE);973if ($no_mutate) {974foreach ($no_mutate as $column) {975unset($data[$column]);976}977}978979$this->willWriteData($data);980981$map = array();982foreach ($data as $k => $v) {983$map[$k] = $v;984}985986$conn = $this->establishConnection('w');987$binary = $this->getBinaryColumns();988989foreach ($map as $key => $value) {990if (!empty($binary[$key])) {991$map[$key] = qsprintf($conn, '%C = %nB', $key, $value);992} else {993$map[$key] = qsprintf($conn, '%C = %ns', $key, $value);994}995}996997$id = $this->getID();998$conn->query(999'UPDATE %R SET %LQ WHERE %C = '.(is_int($id) ? '%d' : '%s'),1000$this,1001$map,1002$this->getIDKey(),1003$id);1004// We can't detect a missing object because updating an object without1005// changing any values doesn't affect rows. We could jiggle timestamps1006// to catch this for objects which track them if we wanted.10071008$this->didWriteData();10091010return $this;1011}101210131014/**1015* Delete this object, permanently.1016*1017* @return this1018*1019* @task save1020*/1021public function delete() {1022$this->isEphemeralCheck();1023$this->willDelete();10241025$conn = $this->establishConnection('w');1026$conn->query(1027'DELETE FROM %R WHERE %C = %d',1028$this,1029$this->getIDKey(),1030$this->getID());10311032$this->didDelete();10331034return $this;1035}10361037/**1038* Internal implementation of INSERT and REPLACE.1039*1040* @param const Either "INSERT" or "REPLACE", to force the desired mode.1041* @return this1042*1043* @task save1044*/1045protected function insertRecordIntoDatabase($mode) {1046$this->willSaveObject();1047$data = $this->getAllLiskPropertyValues();10481049$conn = $this->establishConnection('w');10501051$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);1052switch ($id_mechanism) {1053case self::IDS_AUTOINCREMENT:1054// If we are using autoincrement IDs, let MySQL assign the value for the1055// ID column, if it is empty. If the caller has explicitly provided a1056// value, use it.1057$id_key = $this->getIDKey();1058if (empty($data[$id_key])) {1059unset($data[$id_key]);1060}1061break;1062case self::IDS_COUNTER:1063// If we are using counter IDs, assign a new ID if we don't already have1064// one.1065$id_key = $this->getIDKey();1066if (empty($data[$id_key])) {1067$counter_name = $this->getTableName();1068$id = self::loadNextCounterValue($conn, $counter_name);1069$this->setID($id);1070$data[$id_key] = $id;1071}1072break;1073case self::IDS_MANUAL:1074break;1075default:1076throw new Exception(pht('Unknown %s mechanism!', 'CONFIG_IDs'));1077}10781079$this->willWriteData($data);10801081$columns = array_keys($data);1082$binary = $this->getBinaryColumns();10831084foreach ($data as $key => $value) {1085try {1086if (!empty($binary[$key])) {1087$data[$key] = qsprintf($conn, '%nB', $value);1088} else {1089$data[$key] = qsprintf($conn, '%ns', $value);1090}1091} catch (AphrontParameterQueryException $parameter_exception) {1092throw new PhutilProxyException(1093pht(1094"Unable to insert or update object of class %s, field '%s' ".1095"has a non-scalar value.",1096get_class($this),1097$key),1098$parameter_exception);1099}1100}11011102switch ($mode) {1103case 'INSERT':1104$verb = qsprintf($conn, 'INSERT');1105break;1106case 'REPLACE':1107$verb = qsprintf($conn, 'REPLACE');1108break;1109default:1110throw new Exception(1111pht(1112'Insert mode verb "%s" is not recognized, use INSERT or REPLACE.',1113$mode));1114}11151116$conn->query(1117'%Q INTO %R (%LC) VALUES (%LQ)',1118$verb,1119$this,1120$columns,1121$data);11221123// Only use the insert id if this table is using auto-increment ids1124if ($id_mechanism === self::IDS_AUTOINCREMENT) {1125$this->setID($conn->getInsertID());1126}11271128$this->didWriteData();11291130return $this;1131}113211331134/**1135* Method used to determine whether to insert or update when saving.1136*1137* @return bool true if the record should be inserted1138*/1139protected function shouldInsertWhenSaved() {1140$key_type = $this->getConfigOption(self::CONFIG_IDS);11411142if ($key_type == self::IDS_MANUAL) {1143throw new Exception(1144pht(1145'You are using manual IDs. You must override the %s method '.1146'to properly detect when to insert a new record.',1147__FUNCTION__.'()'));1148} else {1149return !$this->getID();1150}1151}115211531154/* -( Hooks and Callbacks )------------------------------------------------ */115511561157/**1158* Retrieve the database table name. By default, this is the class name.1159*1160* @return string Table name for object storage.1161*1162* @task hook1163*/1164public function getTableName() {1165return get_class($this);1166}116711681169/**1170* Retrieve the primary key column, "id" by default. If you can not1171* reasonably name your ID column "id", override this method.1172*1173* @return string Name of the ID column.1174*1175* @task hook1176*/1177public function getIDKey() {1178return 'id';1179}11801181/**1182* Generate a new PHID, used by CONFIG_AUX_PHID.1183*1184* @return phid Unique, newly allocated PHID.1185*1186* @task hook1187*/1188public function generatePHID() {1189$type = $this->getPHIDType();1190return PhabricatorPHID::generateNewPHID($type);1191}11921193public function getPHIDType() {1194throw new PhutilMethodNotImplementedException();1195}119611971198/**1199* Hook to apply serialization or validation to data before it is written to1200* the database. See also @{method:willReadData}.1201*1202* @task hook1203*/1204protected function willWriteData(array &$data) {1205$this->applyLiskDataSerialization($data, false);1206}120712081209/**1210* Hook to perform actions after data has been written to the database.1211*1212* @task hook1213*/1214protected function didWriteData() {}121512161217/**1218* Hook to make internal object state changes prior to INSERT, REPLACE or1219* UPDATE.1220*1221* @task hook1222*/1223protected function willSaveObject() {1224$use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);12251226if ($use_timestamps) {1227if (!$this->getDateCreated()) {1228$this->setDateCreated(time());1229}1230$this->setDateModified(time());1231}12321233if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) {1234$this->setPHID($this->generatePHID());1235}1236}123712381239/**1240* Hook to apply serialization or validation to data as it is read from the1241* database. See also @{method:willWriteData}.1242*1243* @task hook1244*/1245protected function willReadData(array &$data) {1246$this->applyLiskDataSerialization($data, $deserialize = true);1247}12481249/**1250* Hook to perform an action on data after it is read from the database.1251*1252* @task hook1253*/1254protected function didReadData() {}12551256/**1257* Hook to perform an action before the deletion of an object.1258*1259* @task hook1260*/1261protected function willDelete() {}12621263/**1264* Hook to perform an action after the deletion of an object.1265*1266* @task hook1267*/1268protected function didDelete() {}12691270/**1271* Reads the value from a field. Override this method for custom behavior1272* of @{method:getField} instead of overriding getField directly.1273*1274* @param string Canonical field name1275* @return mixed Value of the field1276*1277* @task hook1278*/1279protected function readField($field) {1280if (isset($this->$field)) {1281return $this->$field;1282}1283return null;1284}12851286/**1287* Writes a value to a field. Override this method for custom behavior of1288* setField($value) instead of overriding setField directly.1289*1290* @param string Canonical field name1291* @param mixed Value to write1292*1293* @task hook1294*/1295protected function writeField($field, $value) {1296$this->$field = $value;1297}129812991300/* -( Manging Transactions )----------------------------------------------- */130113021303/**1304* Increase transaction stack depth.1305*1306* @return this1307*/1308public function openTransaction() {1309$this->establishConnection('w')->openTransaction();1310return $this;1311}131213131314/**1315* Decrease transaction stack depth, saving work.1316*1317* @return this1318*/1319public function saveTransaction() {1320$this->establishConnection('w')->saveTransaction();1321return $this;1322}132313241325/**1326* Decrease transaction stack depth, discarding work.1327*1328* @return this1329*/1330public function killTransaction() {1331$this->establishConnection('w')->killTransaction();1332return $this;1333}133413351336/**1337* Begins read-locking selected rows with SELECT ... FOR UPDATE, so that1338* other connections can not read them (this is an enormous oversimplification1339* of FOR UPDATE semantics; consult the MySQL documentation for details). To1340* end read locking, call @{method:endReadLocking}. For example:1341*1342* $beach->openTransaction();1343* $beach->beginReadLocking();1344*1345* $beach->reload();1346* $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);1347* $beach->save();1348*1349* $beach->endReadLocking();1350* $beach->saveTransaction();1351*1352* @return this1353* @task xaction1354*/1355public function beginReadLocking() {1356$this->establishConnection('w')->beginReadLocking();1357return $this;1358}135913601361/**1362* Ends read-locking that began at an earlier @{method:beginReadLocking} call.1363*1364* @return this1365* @task xaction1366*/1367public function endReadLocking() {1368$this->establishConnection('w')->endReadLocking();1369return $this;1370}13711372/**1373* Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so1374* that other connections can not update or delete them (this is an1375* oversimplification of LOCK IN SHARE MODE semantics; consult the1376* MySQL documentation for details). To end write locking, call1377* @{method:endWriteLocking}.1378*1379* @return this1380* @task xaction1381*/1382public function beginWriteLocking() {1383$this->establishConnection('w')->beginWriteLocking();1384return $this;1385}138613871388/**1389* Ends write-locking that began at an earlier @{method:beginWriteLocking}1390* call.1391*1392* @return this1393* @task xaction1394*/1395public function endWriteLocking() {1396$this->establishConnection('w')->endWriteLocking();1397return $this;1398}139914001401/* -( Isolation )---------------------------------------------------------- */140214031404/**1405* @task isolate1406*/1407public static function beginIsolateAllLiskEffectsToCurrentProcess() {1408self::$processIsolationLevel++;1409}14101411/**1412* @task isolate1413*/1414public static function endIsolateAllLiskEffectsToCurrentProcess() {1415self::$processIsolationLevel--;1416if (self::$processIsolationLevel < 0) {1417throw new Exception(1418pht('Lisk process isolation level was reduced below 0.'));1419}1420}14211422/**1423* @task isolate1424*/1425public static function shouldIsolateAllLiskEffectsToCurrentProcess() {1426return (bool)self::$processIsolationLevel;1427}14281429/**1430* @task isolate1431*/1432private function establishIsolatedConnection($mode) {1433$config = array();1434return new AphrontIsolatedDatabaseConnection($config);1435}14361437/**1438* @task isolate1439*/1440public static function beginIsolateAllLiskEffectsToTransactions() {1441if (self::$transactionIsolationLevel === 0) {1442self::closeAllConnections();1443}1444self::$transactionIsolationLevel++;1445}14461447/**1448* @task isolate1449*/1450public static function endIsolateAllLiskEffectsToTransactions() {1451self::$transactionIsolationLevel--;1452if (self::$transactionIsolationLevel < 0) {1453throw new Exception(1454pht('Lisk transaction isolation level was reduced below 0.'));1455} else if (self::$transactionIsolationLevel == 0) {1456foreach (self::$connections as $key => $conn) {1457if ($conn) {1458$conn->killTransaction();1459}1460}1461self::closeAllConnections();1462}1463}14641465/**1466* @task isolate1467*/1468public static function shouldIsolateAllLiskEffectsToTransactions() {1469return (bool)self::$transactionIsolationLevel;1470}14711472/**1473* Close any connections with no recent activity.1474*1475* Long-running processes can use this method to clean up connections which1476* have not been used recently.1477*1478* @param int Close connections with no activity for this many seconds.1479* @return void1480*/1481public static function closeInactiveConnections($idle_window) {1482$connections = self::$connections;14831484$now = PhabricatorTime::getNow();1485foreach ($connections as $key => $connection) {1486// If the connection is not idle, never consider it inactive.1487if (!$connection->isIdle()) {1488continue;1489}14901491$last_active = $connection->getLastActiveEpoch();14921493$idle_duration = ($now - $last_active);1494if ($idle_duration <= $idle_window) {1495continue;1496}14971498self::closeConnection($key);1499}1500}150115021503public static function closeAllConnections() {1504$connections = self::$connections;15051506foreach ($connections as $key => $connection) {1507self::closeConnection($key);1508}1509}15101511public static function closeIdleConnections() {1512$connections = self::$connections;15131514foreach ($connections as $key => $connection) {1515if (!$connection->isIdle()) {1516continue;1517}15181519self::closeConnection($key);1520}1521}15221523private static function closeConnection($key) {1524if (empty(self::$connections[$key])) {1525throw new Exception(1526pht(1527'No database connection with connection key "%s" exists!',1528$key));1529}15301531$connection = self::$connections[$key];1532unset(self::$connections[$key]);15331534$connection->close();1535}153615371538/* -( Utilities )---------------------------------------------------------- */153915401541/**1542* Applies configured serialization to a dictionary of values.1543*1544* @task util1545*/1546protected function applyLiskDataSerialization(array &$data, $deserialize) {1547$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);1548if ($serialization) {1549foreach (array_intersect_key($serialization, $data) as $col => $format) {1550switch ($format) {1551case self::SERIALIZATION_NONE:1552break;1553case self::SERIALIZATION_PHP:1554if ($deserialize) {1555$data[$col] = unserialize($data[$col]);1556} else {1557$data[$col] = serialize($data[$col]);1558}1559break;1560case self::SERIALIZATION_JSON:1561if ($deserialize) {1562$data[$col] = json_decode($data[$col], true);1563} else {1564$data[$col] = phutil_json_encode($data[$col]);1565}1566break;1567default:1568throw new Exception(1569pht("Unknown serialization format '%s'.", $format));1570}1571}1572}1573}15741575/**1576* Black magic. Builds implied get*() and set*() for all properties.1577*1578* @param string Method name.1579* @param list Argument vector.1580* @return mixed get*() methods return the property value. set*() methods1581* return $this.1582* @task util1583*/1584public function __call($method, $args) {1585$dispatch_map = $this->getLiskMetadata('dispatchMap', array());15861587// NOTE: This method is very performance-sensitive (many thousands of calls1588// per page on some pages), and thus has some silliness in the name of1589// optimizations.15901591if ($method[0] === 'g') {1592if (isset($dispatch_map[$method])) {1593$property = $dispatch_map[$method];1594} else {1595if (substr($method, 0, 3) !== 'get') {1596throw new Exception(pht("Unable to resolve method '%s'!", $method));1597}1598$property = substr($method, 3);1599if (!($property = $this->checkProperty($property))) {1600throw new Exception(pht('Bad getter call: %s', $method));1601}1602$dispatch_map[$method] = $property;1603$this->setLiskMetadata('dispatchMap', $dispatch_map);1604}16051606return $this->readField($property);1607}16081609if ($method[0] === 's') {1610if (isset($dispatch_map[$method])) {1611$property = $dispatch_map[$method];1612} else {1613if (substr($method, 0, 3) !== 'set') {1614throw new Exception(pht("Unable to resolve method '%s'!", $method));1615}16161617$property = substr($method, 3);1618$property = $this->checkProperty($property);1619if (!$property) {1620throw new Exception(pht('Bad setter call: %s', $method));1621}1622$dispatch_map[$method] = $property;1623$this->setLiskMetadata('dispatchMap', $dispatch_map);1624}16251626$this->writeField($property, $args[0]);16271628return $this;1629}16301631throw new Exception(pht("Unable to resolve method '%s'.", $method));1632}16331634/**1635* Warns against writing to undeclared property.1636*1637* @task util1638*/1639public function __set($name, $value) {1640// Hack for policy system hints, see PhabricatorPolicyRule for notes.1641if ($name != '_hashKey') {1642phlog(1643pht(1644'Wrote to undeclared property %s.',1645get_class($this).'::$'.$name));1646}1647$this->$name = $value;1648}164916501651/**1652* Increments a named counter and returns the next value.1653*1654* @param AphrontDatabaseConnection Database where the counter resides.1655* @param string Counter name to create or increment.1656* @return int Next counter value.1657*1658* @task util1659*/1660public static function loadNextCounterValue(1661AphrontDatabaseConnection $conn_w,1662$counter_name) {16631664// NOTE: If an insert does not touch an autoincrement row or call1665// LAST_INSERT_ID(), MySQL normally does not change the value of1666// LAST_INSERT_ID(). This can cause a counter's value to leak to a1667// new counter if the second counter is created after the first one is1668// updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the1669// LAST_INSERT_ID() is always updated and always set correctly after the1670// query completes.16711672queryfx(1673$conn_w,1674'INSERT INTO %T (counterName, counterValue) VALUES1675(%s, LAST_INSERT_ID(1))1676ON DUPLICATE KEY UPDATE1677counterValue = LAST_INSERT_ID(counterValue + 1)',1678self::COUNTER_TABLE_NAME,1679$counter_name);16801681return $conn_w->getInsertID();1682}168316841685/**1686* Returns the current value of a named counter.1687*1688* @param AphrontDatabaseConnection Database where the counter resides.1689* @param string Counter name to read.1690* @return int|null Current value, or `null` if the counter does not exist.1691*1692* @task util1693*/1694public static function loadCurrentCounterValue(1695AphrontDatabaseConnection $conn_r,1696$counter_name) {16971698$row = queryfx_one(1699$conn_r,1700'SELECT counterValue FROM %T WHERE counterName = %s',1701self::COUNTER_TABLE_NAME,1702$counter_name);1703if (!$row) {1704return null;1705}17061707return (int)$row['counterValue'];1708}170917101711/**1712* Overwrite a named counter, forcing it to a specific value.1713*1714* If the counter does not exist, it is created.1715*1716* @param AphrontDatabaseConnection Database where the counter resides.1717* @param string Counter name to create or overwrite.1718* @return void1719*1720* @task util1721*/1722public static function overwriteCounterValue(1723AphrontDatabaseConnection $conn_w,1724$counter_name,1725$counter_value) {17261727queryfx(1728$conn_w,1729'INSERT INTO %T (counterName, counterValue) VALUES (%s, %d)1730ON DUPLICATE KEY UPDATE counterValue = VALUES(counterValue)',1731self::COUNTER_TABLE_NAME,1732$counter_name,1733$counter_value);1734}17351736private function getBinaryColumns() {1737return $this->getConfigOption(self::CONFIG_BINARY);1738}17391740public function getSchemaColumns() {1741$custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA);1742if (!$custom_map) {1743$custom_map = array();1744}17451746$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);1747if (!$serialization) {1748$serialization = array();1749}17501751$serialization_map = array(1752self::SERIALIZATION_JSON => 'text',1753self::SERIALIZATION_PHP => 'bytes',1754);17551756$binary_map = $this->getBinaryColumns();17571758$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);1759if ($id_mechanism == self::IDS_AUTOINCREMENT) {1760$id_type = 'auto';1761} else {1762$id_type = 'id';1763}17641765$builtin = array(1766'id' => $id_type,1767'phid' => 'phid',1768'viewPolicy' => 'policy',1769'editPolicy' => 'policy',1770'epoch' => 'epoch',1771'dateCreated' => 'epoch',1772'dateModified' => 'epoch',1773);17741775$map = array();1776foreach ($this->getAllLiskProperties() as $property) {1777// First, use types specified explicitly in the table configuration.1778if (array_key_exists($property, $custom_map)) {1779$map[$property] = $custom_map[$property];1780continue;1781}17821783// If we don't have an explicit type, try a builtin type for the1784// column.1785$type = idx($builtin, $property);1786if ($type) {1787$map[$property] = $type;1788continue;1789}17901791// If the column has serialization, we can infer the column type.1792if (isset($serialization[$property])) {1793$type = idx($serialization_map, $serialization[$property]);1794if ($type) {1795$map[$property] = $type;1796continue;1797}1798}17991800if (isset($binary_map[$property])) {1801$map[$property] = 'bytes';1802continue;1803}18041805if ($property === 'spacePHID') {1806$map[$property] = 'phid?';1807continue;1808}18091810// If the column is named `somethingPHID`, infer it is a PHID.1811if (preg_match('/[a-z]PHID$/', $property)) {1812$map[$property] = 'phid';1813continue;1814}18151816// If the column is named `somethingID`, infer it is an ID.1817if (preg_match('/[a-z]ID$/', $property)) {1818$map[$property] = 'id';1819continue;1820}18211822// We don't know the type of this column.1823$map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN;1824}18251826return $map;1827}18281829public function getSchemaKeys() {1830$custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA);1831if (!$custom_map) {1832$custom_map = array();1833}18341835$default_map = array();1836foreach ($this->getAllLiskProperties() as $property) {1837switch ($property) {1838case 'id':1839$default_map['PRIMARY'] = array(1840'columns' => array('id'),1841'unique' => true,1842);1843break;1844case 'phid':1845$default_map['key_phid'] = array(1846'columns' => array('phid'),1847'unique' => true,1848);1849break;1850case 'spacePHID':1851$default_map['key_space'] = array(1852'columns' => array('spacePHID'),1853);1854break;1855}1856}18571858return $custom_map + $default_map;1859}18601861public function getColumnMaximumByteLength($column) {1862$map = $this->getSchemaColumns();18631864if (!isset($map[$column])) {1865throw new Exception(1866pht(1867'Object (of class "%s") does not have a column "%s".',1868get_class($this),1869$column));1870}18711872$data_type = $map[$column];18731874return id(new PhabricatorStorageSchemaSpec())1875->getMaximumByteLengthForDataType($data_type);1876}18771878public function getSchemaPersistence() {1879return null;1880}188118821883/* -( AphrontDatabaseTableRefInterface )----------------------------------- */188418851886public function getAphrontRefDatabaseName() {1887return $this->getDatabaseName();1888}18891890public function getAphrontRefTableName() {1891return $this->getTableName();1892}189318941895private function getLiskMetadata($key, $default = null) {1896if (isset(self::$liskMetadata[static::class][$key])) {1897return self::$liskMetadata[static::class][$key];1898}18991900if (!isset(self::$liskMetadata[static::class])) {1901self::$liskMetadata[static::class] = array();1902}19031904return idx(self::$liskMetadata[static::class], $key, $default);1905}19061907private function setLiskMetadata($key, $value) {1908self::$liskMetadata[static::class][$key] = $value;1909}19101911}191219131914