Changeset View
Changeset View
Standalone View
Standalone View
src/infrastructure/storage/lisk/LiskDAO.php
Show First 20 Lines • Show All 187 Lines • ▼ Show 20 Lines | abstract class LiskDAO extends Phobject | ||||
private static $processIsolationLevel = 0; | private static $processIsolationLevel = 0; | ||||
private static $transactionIsolationLevel = 0; | private static $transactionIsolationLevel = 0; | ||||
private $ephemeral = false; | private $ephemeral = false; | ||||
private $forcedConnection; | private $forcedConnection; | ||||
private static $connections = array(); | private static $connections = array(); | ||||
private static $liskMetadata = array(); | |||||
protected $id; | protected $id; | ||||
protected $phid; | protected $phid; | ||||
protected $dateCreated; | protected $dateCreated; | ||||
protected $dateModified; | protected $dateModified; | ||||
/** | /** | ||||
* Build an empty object. | * Build an empty object. | ||||
* | * | ||||
▲ Show 20 Lines • Show All 194 Lines • ▼ Show 20 Lines | /* -( Configuring Lisk )--------------------------------------------------- */ | ||||
* Determine the setting of a configuration option for this class of objects. | * Determine the setting of a configuration option for this class of objects. | ||||
* | * | ||||
* @param const Option name, one of the CONFIG_* constants. | * @param const Option name, one of the CONFIG_* constants. | ||||
* @return mixed Option value, if configured (null if unavailable). | * @return mixed Option value, if configured (null if unavailable). | ||||
* | * | ||||
* @task config | * @task config | ||||
*/ | */ | ||||
public function getConfigOption($option_name) { | public function getConfigOption($option_name) { | ||||
static $options = null; | $options = $this->getLiskMetadata('config'); | ||||
if (!isset($options)) { | if ($options === null) { | ||||
$options = $this->getConfiguration(); | $options = $this->getConfiguration(); | ||||
$this->setLiskMetadata('config', $options); | |||||
} | } | ||||
return idx($options, $option_name); | return idx($options, $option_name); | ||||
} | } | ||||
/* -( Loading Objects )---------------------------------------------------- */ | /* -( Loading Objects )---------------------------------------------------- */ | ||||
Show All 16 Lines | public function load($id) { | ||||
} | } | ||||
if (!$id || (!is_int($id) && !ctype_digit($id))) { | if (!$id || (!is_int($id) && !ctype_digit($id))) { | ||||
return null; | return null; | ||||
} | } | ||||
return $this->loadOneWhere( | return $this->loadOneWhere( | ||||
'%C = %d', | '%C = %d', | ||||
$this->getIDKeyForUse(), | $this->getIDKey(), | ||||
$id); | $id); | ||||
} | } | ||||
/** | /** | ||||
* Loads all of the objects, unconditionally. | * Loads all of the objects, unconditionally. | ||||
* | * | ||||
* @return dict Dictionary of all persisted objects of this type, keyed | * @return dict Dictionary of all persisted objects of this type, keyed | ||||
▲ Show 20 Lines • Show All 98 Lines • ▼ Show 20 Lines | /* -( Loading Objects )---------------------------------------------------- */ | ||||
public function reload() { | public function reload() { | ||||
if (!$this->getID()) { | if (!$this->getID()) { | ||||
throw new Exception( | throw new Exception( | ||||
pht("Unable to reload object that hasn't been loaded!")); | pht("Unable to reload object that hasn't been loaded!")); | ||||
} | } | ||||
$result = $this->loadOneWhere( | $result = $this->loadOneWhere( | ||||
'%C = %d', | '%C = %d', | ||||
$this->getIDKeyForUse(), | $this->getIDKey(), | ||||
$this->getID()); | $this->getID()); | ||||
if (!$result) { | if (!$result) { | ||||
throw new AphrontObjectMissingQueryException(); | throw new AphrontObjectMissingQueryException(); | ||||
} | } | ||||
return $this; | return $this; | ||||
} | } | ||||
/** | /** | ||||
* Initialize this object's properties from a dictionary. Generally, you | * Initialize this object's properties from a dictionary. Generally, you | ||||
* load single objects with loadOneWhere(), but sometimes it may be more | * load single objects with loadOneWhere(), but sometimes it may be more | ||||
* convenient to pull data from elsewhere directly (e.g., a complicated | * convenient to pull data from elsewhere directly (e.g., a complicated | ||||
* join via @{method:queryData}) and then load from an array representation. | * join via @{method:queryData}) and then load from an array representation. | ||||
* | * | ||||
* @param dict Dictionary of properties, which should be equivalent to | * @param dict Dictionary of properties, which should be equivalent to | ||||
* selecting a row from the table or calling | * selecting a row from the table or calling | ||||
* @{method:getProperties}. | * @{method:getProperties}. | ||||
* @return this | * @return this | ||||
* | * | ||||
* @task load | * @task load | ||||
*/ | */ | ||||
public function loadFromArray(array $row) { | public function loadFromArray(array $row) { | ||||
static $valid_properties = array(); | $valid_map = $this->getLiskMetadata('validMap', array()); | ||||
$map = array(); | $map = array(); | ||||
$updated = false; | |||||
foreach ($row as $k => $v) { | foreach ($row as $k => $v) { | ||||
// We permit (but ignore) extra properties in the array because a | // We permit (but ignore) extra properties in the array because a | ||||
// common approach to building the array is to issue a raw SELECT query | // common approach to building the array is to issue a raw SELECT query | ||||
// which may include extra explicit columns or joins. | // which may include extra explicit columns or joins. | ||||
// This pathway is very hot on some pages, so we're inlining a cache | // This pathway is very hot on some pages, so we're inlining a cache | ||||
// and doing some microoptimization to avoid a strtolower() call for each | // and doing some microoptimization to avoid a strtolower() call for each | ||||
// assignment. The common path (assigning a valid property which we've | // assignment. The common path (assigning a valid property which we've | ||||
// already seen) always incurs only one empty(). The second most common | // already seen) always incurs only one empty(). The second most common | ||||
// path (assigning an invalid property which we've already seen) costs | // path (assigning an invalid property which we've already seen) costs | ||||
// an empty() plus an isset(). | // an empty() plus an isset(). | ||||
if (empty($valid_properties[$k])) { | if (empty($valid_map[$k])) { | ||||
if (isset($valid_properties[$k])) { | if (isset($valid_map[$k])) { | ||||
// The value is set but empty, which means it's false, so we've | // The value is set but empty, which means it's false, so we've | ||||
// already determined it's not valid. We don't need to check again. | // already determined it's not valid. We don't need to check again. | ||||
continue; | continue; | ||||
} | } | ||||
$valid_properties[$k] = $this->hasProperty($k); | $valid_map[$k] = $this->hasProperty($k); | ||||
if (!$valid_properties[$k]) { | $updated = true; | ||||
if (!$valid_map[$k]) { | |||||
continue; | continue; | ||||
} | } | ||||
} | } | ||||
$map[$k] = $v; | $map[$k] = $v; | ||||
} | } | ||||
if ($updated) { | |||||
$this->setLiskMetadata('validMap', $valid_map); | |||||
} | |||||
$this->willReadData($map); | $this->willReadData($map); | ||||
foreach ($map as $prop => $value) { | foreach ($map as $prop => $value) { | ||||
$this->$prop = $value; | $this->$prop = $value; | ||||
} | } | ||||
$this->didReadData(); | $this->didReadData(); | ||||
▲ Show 20 Lines • Show All 61 Lines • ▼ Show 20 Lines | /* -( Examining Objects )-------------------------------------------------- */ | ||||
* Set unique ID identifying this object. You normally don't need to call this | * Set unique ID identifying this object. You normally don't need to call this | ||||
* method unless with `IDS_MANUAL`. | * method unless with `IDS_MANUAL`. | ||||
* | * | ||||
* @param mixed Unique ID. | * @param mixed Unique ID. | ||||
* @return this | * @return this | ||||
* @task save | * @task save | ||||
*/ | */ | ||||
public function setID($id) { | public function setID($id) { | ||||
static $id_key = null; | $id_key = $this->getIDKey(); | ||||
if ($id_key === null) { | |||||
$id_key = $this->getIDKeyForUse(); | |||||
} | |||||
$this->$id_key = $id; | $this->$id_key = $id; | ||||
return $this; | return $this; | ||||
} | } | ||||
/** | /** | ||||
* Retrieve the unique ID identifying this object. This value will be null if | * Retrieve the unique ID identifying this object. This value will be null if | ||||
* the object hasn't been persisted and you didn't set it manually. | * the object hasn't been persisted and you didn't set it manually. | ||||
* | * | ||||
* @return mixed Unique ID. | * @return mixed Unique ID. | ||||
* | * | ||||
* @task info | * @task info | ||||
*/ | */ | ||||
public function getID() { | public function getID() { | ||||
static $id_key = null; | $id_key = $this->getIDKey(); | ||||
if ($id_key === null) { | |||||
$id_key = $this->getIDKeyForUse(); | |||||
} | |||||
return $this->$id_key; | return $this->$id_key; | ||||
} | } | ||||
public function getPHID() { | public function getPHID() { | ||||
return $this->phid; | return $this->phid; | ||||
} | } | ||||
Show All 18 Lines | /* -( Examining Objects )-------------------------------------------------- */ | ||||
* Properties that should not be persisted must be declared as private. | * Properties that should not be persisted must be declared as private. | ||||
* | * | ||||
* @return dict Dictionary of normalized (lowercase) to canonical (original | * @return dict Dictionary of normalized (lowercase) to canonical (original | ||||
* case) property names. | * case) property names. | ||||
* | * | ||||
* @task info | * @task info | ||||
*/ | */ | ||||
protected function getAllLiskProperties() { | protected function getAllLiskProperties() { | ||||
static $properties = null; | $properties = $this->getLiskMetadata('properties'); | ||||
if (!isset($properties)) { | |||||
$class = new ReflectionClass(get_class($this)); | if ($properties === null) { | ||||
$class = new ReflectionClass(static::class); | |||||
$properties = array(); | $properties = array(); | ||||
foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { | foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { | ||||
$properties[strtolower($p->getName())] = $p->getName(); | $properties[strtolower($p->getName())] = $p->getName(); | ||||
} | } | ||||
$id_key = $this->getIDKey(); | $id_key = $this->getIDKey(); | ||||
if ($id_key != 'id') { | if ($id_key != 'id') { | ||||
unset($properties['id']); | unset($properties['id']); | ||||
} | } | ||||
if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { | if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { | ||||
unset($properties['datecreated']); | unset($properties['datecreated']); | ||||
unset($properties['datemodified']); | unset($properties['datemodified']); | ||||
} | } | ||||
if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { | if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { | ||||
unset($properties['phid']); | unset($properties['phid']); | ||||
} | } | ||||
$this->setLiskMetadata('properties', $properties); | |||||
} | } | ||||
return $properties; | return $properties; | ||||
} | } | ||||
/** | /** | ||||
* Check if a property exists on this object. | * Check if a property exists on this object. | ||||
* | * | ||||
* @return string|null Canonical property name, or null if the property | * @return string|null Canonical property name, or null if the property | ||||
* does not exist. | * does not exist. | ||||
* | * | ||||
* @task info | * @task info | ||||
*/ | */ | ||||
protected function checkProperty($property) { | protected function checkProperty($property) { | ||||
static $properties = null; | |||||
if ($properties === null) { | |||||
$properties = $this->getAllLiskProperties(); | $properties = $this->getAllLiskProperties(); | ||||
} | |||||
$property = strtolower($property); | $property = strtolower($property); | ||||
if (empty($properties[$property])) { | if (empty($properties[$property])) { | ||||
return null; | return null; | ||||
} | } | ||||
return $properties[$property]; | return $properties[$property]; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 199 Lines • ▼ Show 20 Lines | foreach ($map as $key => $value) { | ||||
} | } | ||||
} | } | ||||
$id = $this->getID(); | $id = $this->getID(); | ||||
$conn->query( | $conn->query( | ||||
'UPDATE %R SET %LQ WHERE %C = '.(is_int($id) ? '%d' : '%s'), | 'UPDATE %R SET %LQ WHERE %C = '.(is_int($id) ? '%d' : '%s'), | ||||
$this, | $this, | ||||
$map, | $map, | ||||
$this->getIDKeyForUse(), | $this->getIDKey(), | ||||
$id); | $id); | ||||
// We can't detect a missing object because updating an object without | // We can't detect a missing object because updating an object without | ||||
// changing any values doesn't affect rows. We could jiggle timestamps | // changing any values doesn't affect rows. We could jiggle timestamps | ||||
// to catch this for objects which track them if we wanted. | // to catch this for objects which track them if we wanted. | ||||
$this->didWriteData(); | $this->didWriteData(); | ||||
return $this; | return $this; | ||||
Show All 10 Lines | /* -( Writing Objects )---------------------------------------------------- */ | ||||
public function delete() { | public function delete() { | ||||
$this->isEphemeralCheck(); | $this->isEphemeralCheck(); | ||||
$this->willDelete(); | $this->willDelete(); | ||||
$conn = $this->establishConnection('w'); | $conn = $this->establishConnection('w'); | ||||
$conn->query( | $conn->query( | ||||
'DELETE FROM %R WHERE %C = %d', | 'DELETE FROM %R WHERE %C = %d', | ||||
$this, | $this, | ||||
$this->getIDKeyForUse(), | $this->getIDKey(), | ||||
$this->getID()); | $this->getID()); | ||||
$this->didDelete(); | $this->didDelete(); | ||||
return $this; | return $this; | ||||
} | } | ||||
/** | /** | ||||
Show All 11 Lines | protected function insertRecordIntoDatabase($mode) { | ||||
$conn = $this->establishConnection('w'); | $conn = $this->establishConnection('w'); | ||||
$id_mechanism = $this->getConfigOption(self::CONFIG_IDS); | $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); | ||||
switch ($id_mechanism) { | switch ($id_mechanism) { | ||||
case self::IDS_AUTOINCREMENT: | case self::IDS_AUTOINCREMENT: | ||||
// If we are using autoincrement IDs, let MySQL assign the value for the | // If we are using autoincrement IDs, let MySQL assign the value for the | ||||
// ID column, if it is empty. If the caller has explicitly provided a | // ID column, if it is empty. If the caller has explicitly provided a | ||||
// value, use it. | // value, use it. | ||||
$id_key = $this->getIDKeyForUse(); | $id_key = $this->getIDKey(); | ||||
if (empty($data[$id_key])) { | if (empty($data[$id_key])) { | ||||
unset($data[$id_key]); | unset($data[$id_key]); | ||||
} | } | ||||
break; | break; | ||||
case self::IDS_COUNTER: | case self::IDS_COUNTER: | ||||
// If we are using counter IDs, assign a new ID if we don't already have | // If we are using counter IDs, assign a new ID if we don't already have | ||||
// one. | // one. | ||||
$id_key = $this->getIDKeyForUse(); | $id_key = $this->getIDKey(); | ||||
if (empty($data[$id_key])) { | if (empty($data[$id_key])) { | ||||
$counter_name = $this->getTableName(); | $counter_name = $this->getTableName(); | ||||
$id = self::loadNextCounterValue($conn, $counter_name); | $id = self::loadNextCounterValue($conn, $counter_name); | ||||
$this->setID($id); | $this->setID($id); | ||||
$data[$id_key] = $id; | $data[$id_key] = $id; | ||||
} | } | ||||
break; | break; | ||||
case self::IDS_MANUAL: | case self::IDS_MANUAL: | ||||
▲ Show 20 Lines • Show All 99 Lines • ▼ Show 20 Lines | /* -( Hooks and Callbacks )------------------------------------------------ */ | ||||
* @return string Name of the ID column. | * @return string Name of the ID column. | ||||
* | * | ||||
* @task hook | * @task hook | ||||
*/ | */ | ||||
public function getIDKey() { | public function getIDKey() { | ||||
return 'id'; | return 'id'; | ||||
} | } | ||||
protected function getIDKeyForUse() { | |||||
$id_key = $this->getIDKey(); | |||||
if (!$id_key) { | |||||
throw new Exception( | |||||
pht( | |||||
'This DAO does not have a single-part primary key. The method you '. | |||||
'called requires a single-part primary key.')); | |||||
} | |||||
return $id_key; | |||||
} | |||||
/** | /** | ||||
* Generate a new PHID, used by CONFIG_AUX_PHID. | * Generate a new PHID, used by CONFIG_AUX_PHID. | ||||
* | * | ||||
* @return phid Unique, newly allocated PHID. | * @return phid Unique, newly allocated PHID. | ||||
* | * | ||||
* @task hook | * @task hook | ||||
*/ | */ | ||||
public function generatePHID() { | public function generatePHID() { | ||||
▲ Show 20 Lines • Show All 388 Lines • ▼ Show 20 Lines | /* -( Utilities )---------------------------------------------------------- */ | ||||
* | * | ||||
* @param string Method name. | * @param string Method name. | ||||
* @param list Argument vector. | * @param list Argument vector. | ||||
* @return mixed get*() methods return the property value. set*() methods | * @return mixed get*() methods return the property value. set*() methods | ||||
* return $this. | * return $this. | ||||
* @task util | * @task util | ||||
*/ | */ | ||||
public function __call($method, $args) { | public function __call($method, $args) { | ||||
// NOTE: PHP has a bug that static variables defined in __call() are shared | $dispatch_map = $this->getLiskMetadata('dispatchMap', array()); | ||||
// across all children classes. Call a different method to work around this | |||||
// bug. | |||||
return $this->call($method, $args); | |||||
} | |||||
/** | |||||
* @task util | |||||
*/ | |||||
final protected function call($method, $args) { | |||||
// NOTE: This method is very performance-sensitive (many thousands of calls | // NOTE: This method is very performance-sensitive (many thousands of calls | ||||
// per page on some pages), and thus has some silliness in the name of | // per page on some pages), and thus has some silliness in the name of | ||||
// optimizations. | // optimizations. | ||||
static $dispatch_map = array(); | |||||
if ($method[0] === 'g') { | if ($method[0] === 'g') { | ||||
if (isset($dispatch_map[$method])) { | if (isset($dispatch_map[$method])) { | ||||
$property = $dispatch_map[$method]; | $property = $dispatch_map[$method]; | ||||
} else { | } else { | ||||
if (substr($method, 0, 3) !== 'get') { | if (substr($method, 0, 3) !== 'get') { | ||||
throw new Exception(pht("Unable to resolve method '%s'!", $method)); | throw new Exception(pht("Unable to resolve method '%s'!", $method)); | ||||
} | } | ||||
$property = substr($method, 3); | $property = substr($method, 3); | ||||
if (!($property = $this->checkProperty($property))) { | if (!($property = $this->checkProperty($property))) { | ||||
throw new Exception(pht('Bad getter call: %s', $method)); | throw new Exception(pht('Bad getter call: %s', $method)); | ||||
} | } | ||||
$dispatch_map[$method] = $property; | $dispatch_map[$method] = $property; | ||||
$this->setLiskMetadata('dispatchMap', $dispatch_map); | |||||
} | } | ||||
return $this->readField($property); | return $this->readField($property); | ||||
} | } | ||||
if ($method[0] === 's') { | if ($method[0] === 's') { | ||||
if (isset($dispatch_map[$method])) { | if (isset($dispatch_map[$method])) { | ||||
$property = $dispatch_map[$method]; | $property = $dispatch_map[$method]; | ||||
} else { | } else { | ||||
if (substr($method, 0, 3) !== 'set') { | if (substr($method, 0, 3) !== 'set') { | ||||
throw new Exception(pht("Unable to resolve method '%s'!", $method)); | throw new Exception(pht("Unable to resolve method '%s'!", $method)); | ||||
} | } | ||||
$property = substr($method, 3); | $property = substr($method, 3); | ||||
$property = $this->checkProperty($property); | $property = $this->checkProperty($property); | ||||
if (!$property) { | if (!$property) { | ||||
throw new Exception(pht('Bad setter call: %s', $method)); | throw new Exception(pht('Bad setter call: %s', $method)); | ||||
} | } | ||||
$dispatch_map[$method] = $property; | $dispatch_map[$method] = $property; | ||||
$this->setLiskMetadata('dispatchMap', $dispatch_map); | |||||
} | } | ||||
$this->writeField($property, $args[0]); | $this->writeField($property, $args[0]); | ||||
return $this; | return $this; | ||||
} | } | ||||
throw new Exception(pht("Unable to resolve method '%s'.", $method)); | throw new Exception(pht("Unable to resolve method '%s'.", $method)); | ||||
▲ Show 20 Lines • Show All 255 Lines • ▼ Show 20 Lines | public function getAphrontRefDatabaseName() { | ||||
return $this->getDatabaseName(); | return $this->getDatabaseName(); | ||||
} | } | ||||
public function getAphrontRefTableName() { | public function getAphrontRefTableName() { | ||||
return $this->getTableName(); | return $this->getTableName(); | ||||
} | } | ||||
private function getLiskMetadata($key, $default = null) { | |||||
if (isset(self::$liskMetadata[static::class][$key])) { | |||||
return self::$liskMetadata[static::class][$key]; | |||||
} | |||||
if (!isset(self::$liskMetadata[static::class])) { | |||||
self::$liskMetadata[static::class] = array(); | |||||
} | |||||
return idx(self::$liskMetadata[static::class], $key, $default); | |||||
} | |||||
private function setLiskMetadata($key, $value) { | |||||
self::$liskMetadata[static::class][$key] = $value; | |||||
} | |||||
} | } |