id map for prefetched objects */ protected static $prefetchedNames = array(); protected static $prefetchStats = array(); /** @var ?DbObjectStore */ protected static $dbObjectStore; /** * Constructor is not accessible and should not be overridden */ protected function __construct() { if ($this->table === null || $this->keyName === null || $this->defaultProperties === null ) { throw new LogicException("Someone extending this class didn't RTFM"); } $this->properties = $this->defaultProperties; $this->beforeInit(); } public function getTableName() { return $this->table; } /************************************************************************\ * When extending this class one might want to override any of the * * following hooks. Try to use them whenever possible, especially * * instead of overriding other essential methods like store(). * \************************************************************************/ /** * One can override this to allow for cross checks and more before storing * the object. Please note that the method is public and allows to check * object consistence at any time. * * @return boolean Whether this object is valid */ public function validate() { return true; } /** * This is going to be executed before any initialization method takes * * (load from DB, populate from Array...) takes place * * @return void */ protected function beforeInit() { } /** * Will be executed every time an object has successfully been loaded from * Database * * @return void */ protected function onLoadFromDb() { } /** * Will be executed before an Object is going to be stored. In case you * want to prevent the store() operation from taking place, please throw * an Exception. * * @return void */ protected function beforeStore() { } /** * Wird ausgeführt, nachdem ein Objekt erfolgreich gespeichert worden ist * * @return void */ protected function onStore() { } /** * Wird ausgeführt, nachdem ein Objekt erfolgreich der Datenbank hinzu- * gefügt worden ist * * @return void */ protected function onInsert() { } /** * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich der Datenbank * geändert worden ist * * @return void */ protected function onUpdate() { } /** * Wird ausgeführt, bevor ein Objekt gelöscht wird. Die Operation wird * aber auf jeden Fall durchgeführt, außer man wirft eine Exception * * @return void */ protected function beforeDelete() { } /** * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich aud der * Datenbank gelöscht worden ist * * @return void */ protected function onDelete() { } /** * Set database connection * * @param DbConnection $connection Database connection * * @return self */ public function setConnection(DbConnection $connection) { $this->connection = $connection; $this->db = $connection->getDbAdapter(); return $this; } public static function setDbObjectStore(DbObjectStore $store) { self::$dbObjectStore = $store; } /** * Getter * * @param string $property Property * * @return mixed */ public function get($property) { $func = 'get' . ucfirst($property); if (substr($func, -2) === '[]') { $func = substr($func, 0, -2); } // TODO: id check avoids collision with getId. Rethink this. if ($property !== 'id' && method_exists($this, $func)) { return $this->$func(); } $this->assertPropertyExists($property); return $this->properties[$property]; } public function getProperty($key) { $this->assertPropertyExists($key); return $this->properties[$key]; } protected function assertPropertyExists($key) { if (! array_key_exists($key, $this->properties)) { throw new InvalidArgumentException(sprintf( 'Trying to get invalid property "%s"', $key )); } return $this; } public function hasProperty($key) { if (array_key_exists($key, $this->properties)) { return true; } elseif ($key === 'id') { // There is getId, would give false positive return false; } return $this->hasGetterForProperty($key); } protected function hasGetterForProperty($key) { $func = 'get' . ucfirst($key); if (\substr($func, -2) === '[]') { $func = substr($func, 0, -2); } return \method_exists($this, $func); } protected function hasSetterForProperty($key) { $func = 'set' . ucfirst($key); if (\substr($func, -2) === '[]') { $func = substr($func, 0, -2); } return \method_exists($this, $func); } /** * Generic setter * * @param string $key * @param mixed $value * * @return self */ public function set($key, $value) { $key = (string) $key; if ($value === '') { $value = null; } if (is_resource($value)) { $value = stream_get_contents($value); } $func = 'validate' . ucfirst($key); if (method_exists($this, $func) && $this->$func($value) !== true) { throw new InvalidArgumentException(sprintf( 'Got invalid value "%s" for "%s"', $value, $key )); } $func = 'munge' . ucfirst($key); if (method_exists($this, $func)) { $value = $this->$func($value); } $func = 'set' . ucfirst($key); if (substr($func, -2) === '[]') { $func = substr($func, 0, -2); } if (method_exists($this, $func)) { return $this->$func($value); } if ($this->getUuidColumn() === $key) { if (strlen($value) > 16) { $value = Uuid::fromString($value)->getBytes(); } } if ($this->propertyIsBoolean($key)) { $value = DbDataFormatter::normalizeBoolean($value); } if (! $this->hasProperty($key)) { throw new InvalidArgumentException(sprintf( 'Trying to set invalid key "%s"', $key )); } if ((is_numeric($value) || is_string($value)) && (string) $value === (string) $this->get($key) ) { return $this; } if ($key === $this->getAutoincKeyName() && $this->hasBeenLoadedFromDb()) { throw new InvalidArgumentException('Changing autoincremental key is not allowed'); } return $this->reallySet($key, $value); } protected function reallySet($key, $value) { if ($value === $this->properties[$key]) { return $this; } if ($key === 'id' || substr($key, -3) === '_id') { if ($value !== null && $this->properties[$key] !== null && (int) $value === (int) $this->properties[$key] ) { return $this; } } if ($this->hasBeenLoadedFromDb()) { if ($value === $this->loadedProperties[$key]) { unset($this->modifiedProperties[$key]); if (empty($this->modifiedProperties)) { $this->hasBeenModified = false; } } else { $this->hasBeenModified = true; $this->modifiedProperties[$key] = true; } } else { $this->hasBeenModified = true; $this->modifiedProperties[$key] = true; } $this->properties[$key] = $value; return $this; } /** * Magic getter * * @param mixed $key * * @return mixed */ public function __get($key) { return $this->get($key); } /** * Magic setter * * @param string $key Key * @param mixed $val Value * * @return void */ public function __set($key, $val) { $this->set($key, $val); } /** * Magic isset check * * @param string $key * @return boolean */ public function __isset($key) { return array_key_exists($key, $this->properties); } /** * Magic unsetter * * @param string $key * @return void */ public function __unset($key) { if (! array_key_exists($key, $this->properties)) { throw new InvalidArgumentException('Trying to unset invalid key'); } $this->properties[$key] = $this->defaultProperties[$key]; } /** * Runs set() for every key/value pair of the given Array * * @param array $props Array of properties * @return self */ public function setProperties($props) { if (! is_array($props)) { throw new InvalidArgumentException(sprintf( 'Array required, got %s', gettype($props) )); } foreach ($props as $key => $value) { $this->set($key, $value); } return $this; } /** * Return an array with all object properties * * @return array */ public function getProperties() { //return $this->properties; $res = array(); foreach ($this->listProperties() as $key) { $res[$key] = $this->get($key); } return $res; } protected function getPropertiesForDb() { return $this->properties; } public function listProperties() { return array_keys($this->properties); } public function getDefaultProperties() { return $this->defaultProperties; } /** * Return all properties that changed since object creation * * @return array */ public function getModifiedProperties() { $props = array(); foreach (array_keys($this->modifiedProperties) as $key) { if ($key === $this->autoincKeyName) { if ($this->protectAutoinc) { continue; } elseif ($this->properties[$key] === null) { continue; } } $props[$key] = $this->properties[$key]; } return $props; } /** * List all properties that changed since object creation * * @return array */ public function listModifiedProperties() { return array_keys($this->modifiedProperties); } /** * Whether this object has been modified * * @return bool */ public function hasBeenModified() { return $this->hasBeenModified; } /** * Whether the given property has been modified * * @param string $key Property name * @return boolean */ protected function hasModifiedProperty($key) { return array_key_exists($key, $this->modifiedProperties); } /** * Unique key name * * @return string|array */ public function getKeyName() { return $this->keyName; } /** * Autoinc key name * * @return string */ public function getAutoincKeyName() { return $this->autoincKeyName; } /** * @return ?string */ public function getUuidColumn() { return $this->uuidColumn; } /** * @return bool */ public function hasUuidColumn() { return $this->uuidColumn !== null; } /** * @return \Ramsey\Uuid\UuidInterface */ public function getUniqueId() { if ($this->hasUuidColumn()) { $binaryValue = $this->properties[$this->uuidColumn]; if (is_resource($binaryValue)) { throw new RuntimeException('Properties contain binary UUID, probably a programming error'); } if ($binaryValue === null) { $uuid = Uuid::uuid4(); $this->reallySet($this->uuidColumn, $uuid->getBytes()); return $uuid; } return Uuid::fromBytes($binaryValue); } throw new InvalidArgumentException(sprintf('%s has no UUID column', $this->getTableName())); } public function getKeyParams() { $params = array(); $key = $this->getKeyName(); if (is_array($key)) { foreach ($key as $k) { $params[$k] = $this->get($k); } } else { $params[$key] = $this->get($this->keyName); } return $params; } /** * Return the unique identifier * * // TODO: may conflict with ->id * * @throws InvalidArgumentException When key can not be calculated * * @return string|array */ public function getId() { if (is_array($this->keyName)) { $id = array(); foreach ($this->keyName as $key) { if (isset($this->properties[$key])) { $id[$key] = $this->properties[$key]; } } if (empty($id)) { throw new InvalidArgumentException('Could not evaluate id for multi-column object!'); } return $id; } else { if (isset($this->properties[$this->keyName])) { return $this->properties[$this->keyName]; } } return null; } /** * Get the autoinc value if set * * @return int */ public function getAutoincId() { if (isset($this->properties[$this->autoincKeyName])) { return (int) $this->properties[$this->autoincKeyName]; } return null; } protected function forgetAutoincId() { if (isset($this->properties[$this->autoincKeyName])) { $this->properties[$this->autoincKeyName] = null; } return $this; } /** * Liefert das benutzte Datenbank-Handle * * @return Zend_Db_Adapter_Abstract */ public function getDb() { return $this->db; } public function hasConnection() { return $this->connection !== null; } public function getConnection() { return $this->connection; } /** * Lädt einen Datensatz aus der Datenbank und setzt die entsprechenden * Eigenschaften dieses Objekts * * @throws NotFoundError * @return self */ protected function loadFromDb() { $properties = $this->db->fetchRow($this->prepareObjectQuery()); if (empty($properties)) { if (is_array($this->getKeyName())) { throw new NotFoundError( 'Failed to load %s for %s', $this->table, $this->createWhere() ); } else { throw new NotFoundError( 'Failed to load %s "%s"', $this->table, $this->getLogId() ); } } return $this->setDbProperties($properties); } public function prepareObjectQuery() { return $this->db->select()->from($this->table)->where($this->createWhere()); } /** * @param object|array $row * @param Db $db * @return self */ public static function fromDbRow($row, Db $db) { $self = (new static())->setConnection($db); if (is_object($row)) { return $self->setDbProperties((array) $row); } if (is_array($row)) { return $self->setDbProperties($row); } throw new InvalidDataException('array or object', $row); } protected function setDbProperties($properties) { foreach ($properties as $key => $val) { if (! array_key_exists($key, $this->properties)) { throw new LogicException(sprintf( 'Trying to set invalid %s key "%s". DB schema change?', $this->table, $key )); } if ($val === null) { $this->properties[$key] = null; } elseif (is_resource($val)) { $this->properties[$key] = stream_get_contents($val); } else { $this->properties[$key] = (string) $val; } } $this->setBeingLoadedFromDb(); $this->onLoadFromDb(); return $this; } public function setBeingLoadedFromDb() { $this->loadedFromDb = true; $this->loadedProperties = $this->properties; $this->hasBeenModified = false; $this->modifiedProperties = []; } public function setLoadedProperty($key, $value) { if ($this->hasBeenLoadedFromDb()) { $this->set($key, $value); $this->loadedProperties[$key] = $this->get($key); } else { throw new RuntimeException('Cannot set loaded property for new object'); } } public function getOriginalProperties() { return $this->loadedProperties; } public function getOriginalProperty($key) { $this->assertPropertyExists($key); if ($this->hasBeenLoadedFromDb()) { return $this->loadedProperties[$key]; } return null; } public function resetProperty($key) { $this->set($key, $this->getOriginalProperty($key)); if ($this->listModifiedProperties() === [$key]) { $this->hasBeenModified = false; } return $this; } public function hasBeenLoadedFromDb() { return $this->loadedFromDb; } /** * Ändert den entsprechenden Datensatz in der Datenbank * * @return int Anzahl der geänderten Zeilen * @throws \Zend_Db_Adapter_Exception */ protected function updateDb() { $properties = $this->getModifiedProperties(); if (empty($properties)) { // Fake true, we might have manually set this to "modified" return true; } $this->quoteBinaryProperties($properties); // TODO: Remember changed data for audit and log return $this->db->update( $this->table, $properties, $this->createWhere() ); } /** * Fügt der Datenbank-Tabelle einen entsprechenden Datensatz hinzu * * @return int Anzahl der betroffenen Zeilen * @throws \Zend_Db_Adapter_Exception */ protected function insertIntoDb() { $properties = $this->getPropertiesForDb(); if ($this->autoincKeyName !== null) { if ($this->protectAutoinc || $properties[$this->autoincKeyName] === null) { unset($properties[$this->autoincKeyName]); } } if ($column = $this->getUuidColumn()) { $properties[$column] = $this->getUniqueId()->getBytes(); } $this->quoteBinaryProperties($properties); return $this->db->insert($this->table, $properties); } protected function quoteBinaryProperties(&$properties) { foreach ($properties as $key => $value) { if ($this->isBinaryColumn($key)) { $properties[$key] = $this->getConnection()->quoteBinary($value); } } } protected function isBinaryColumn($column) { return in_array($column, $this->binaryProperties) || $this->getUuidColumn() === $column; } public function propertyIsBoolean($property) { return array_key_exists($property, $this->booleans); } /** * Store object to database * * @param DbConnection $db * @return bool Whether storing succeeded. Always true, throws otherwise * @throws DuplicateKeyException */ public function store(DbConnection $db = null) { if ($db !== null) { $this->setConnection($db); } if ($this->validate() !== true) { throw new InvalidArgumentException(sprintf( '%s[%s] validation failed', $this->table, $this->getLogId() )); } if ($this->hasBeenLoadedFromDb() && ! $this->hasBeenModified()) { return true; } $this->beforeStore(); $table = $this->table; $id = $this->getId(); try { if ($this->hasBeenLoadedFromDb()) { if ($this->updateDb() !== false) { $this->onUpdate(); } else { throw new RuntimeException(sprintf( 'FAILED storing %s "%s"', $table, $this->getLogId() )); } } else { if ($id && $this->existsInDb()) { $logId = '"' . $this->getLogId() . '"'; if ($autoId = $this->getAutoincId()) { $logId .= sprintf(', %s=%s', $this->autoincKeyName, $autoId); } throw new DuplicateKeyException( 'Trying to recreate %s (%s)', $table, $logId ); } if ($this->insertIntoDb()) { if ($this->autoincKeyName && $this->getProperty($this->autoincKeyName) === null) { if ($this->connection->isPgsql()) { $this->properties[$this->autoincKeyName] = $this->db->lastInsertId( $table, $this->autoincKeyName ); } else { $this->properties[$this->autoincKeyName] = $this->db->lastInsertId(); } } // $this->log(sprintf('New %s "%s" has been stored', $table, $id)); $this->onInsert(); } else { throw new RuntimeException(sprintf( 'FAILED to store new %s "%s"', $table, $this->getLogId() )); } } } catch (Zend_Db_Exception $e) { throw new RuntimeException(sprintf( 'Storing %s[%s] failed: %s {%s}', $this->table, $this->getLogId(), $e->getMessage(), var_export($this->getProperties(), true) // TODO: Remove properties )); } // Hint: order is differs from setBeingLoadedFromDb() as of the onStore hook $this->modifiedProperties = []; $this->hasBeenModified = false; $this->loadedProperties = $this->properties; $this->onStore(); $this->loadedFromDb = true; return true; } /** * Delete item from DB * * @return int Affected rows */ protected function deleteFromDb() { return $this->db->delete( $this->table, $this->createWhere() ); } /** * @param string $key * @return self * @throws InvalidArgumentException */ protected function setKey($key) { $keyname = $this->getKeyName(); if (is_array($keyname)) { if (! is_array($key)) { throw new InvalidArgumentException(sprintf( '%s has a multicolumn key, array required', $this->table )); } foreach ($keyname as $k) { if (! array_key_exists($k, $key)) { // We allow for null in multicolumn keys: $key[$k] = null; } $this->set($k, $key[$k]); } } else { $this->set($keyname, $key); } return $this; } protected function existsInDb() { $result = $this->db->fetchRow( $this->db->select()->from($this->table)->where($this->createWhere()) ); return $result !== false; } public function createWhere() { if ($this->hasUuidColumn() && $this->properties[$this->uuidColumn] !== null) { return $this->db->quoteInto( sprintf('%s = ?', $this->getUuidColumn()), $this->connection->quoteBinary($this->getOriginalProperty($this->uuidColumn)) ); } if ($id = $this->getAutoincId()) { if ($originalId = $this->getOriginalProperty($this->autoincKeyName)) { return $this->db->quoteInto( sprintf('%s = ?', $this->autoincKeyName), $originalId ); } return $this->db->quoteInto( sprintf('%s = ?', $this->autoincKeyName), $id ); } $key = $this->getKeyName(); if (is_array($key) && ! empty($key)) { $where = array(); foreach ($key as $k) { if ($this->hasBeenLoadedFromDb()) { if ($this->loadedProperties[$k] === null) { $where[] = sprintf('%s IS NULL', $k); } else { $where[] = $this->createQuotedWhere($k, $this->loadedProperties[$k]); } } else { if ($this->properties[$k] === null) { $where[] = sprintf('%s IS NULL', $k); } else { $where[] = $this->createQuotedWhere($k, $this->properties[$k]); } } } return implode(' AND ', $where); } else { if ($this->hasBeenLoadedFromDb()) { return $this->createQuotedWhere($key, $this->loadedProperties[$key]); } else { return $this->createQuotedWhere($key, $this->properties[$key]); } } } protected function createQuotedWhere($column, $value) { return $this->db->quoteInto( sprintf('%s = ?', $column), $this->eventuallyQuoteBinary($value, $column) ); } protected function eventuallyQuoteBinary($value, $column) { if ($this->isBinaryColumn($column)) { return $this->connection->quoteBinary($value); } else { return $value; } } protected function getLogId() { $id = $this->getId(); if (is_array($id)) { $logId = json_encode($id); } else { $logId = $id; } if ($logId === null && $this->autoincKeyName) { $logId = $this->getAutoincId(); } return $logId; } public function delete() { $table = $this->table; if (! $this->hasBeenLoadedFromDb()) { throw new LogicException(sprintf( 'Cannot delete %s "%s", it has not been loaded from Db', $table, $this->getLogId() )); } if (! $this->existsInDb()) { throw new InvalidArgumentException(sprintf( 'Cannot delete %s "%s", it does not exist', $table, $this->getLogId() )); } $this->beforeDelete(); if (! $this->deleteFromDb()) { throw new RuntimeException(sprintf( 'Deleting %s (%s) FAILED', $table, $this->getLogId() )); } // $this->log(sprintf('%s "%s" has been DELETED', $table, this->getLogId())); $this->onDelete(); $this->loadedFromDb = false; return true; } public function __clone() { $this->onClone(); $this->forgetAutoincId(); $this->loadedFromDb = false; $this->hasBeenModified = true; } protected function onClone() { } /** * @param array $properties * @param DbConnection|null $connection * * @return static */ public static function create($properties = array(), DbConnection $connection = null) { $obj = new static(); if ($connection !== null) { $obj->setConnection($connection); } $obj->setProperties($properties); return $obj; } protected static function classWasPrefetched() { $class = get_called_class(); return array_key_exists($class, self::$prefetched); } /** * @param $key * @return static|bool */ protected static function getPrefetched($key) { $class = get_called_class(); if (static::hasPrefetched($key)) { if (is_string($key) && array_key_exists($class, self::$prefetchedNames) && array_key_exists($key, self::$prefetchedNames[$class]) ) { return self::$prefetched[$class][ self::$prefetchedNames[$class][$key] ]; } else { return self::$prefetched[$class][$key]; } } else { return false; } } protected static function hasPrefetched($key) { $class = get_called_class(); if (! array_key_exists($class, self::$prefetchStats)) { self::$prefetchStats[$class] = (object) array( 'miss' => 0, 'hits' => 0, 'hitNames' => 0, 'combinedMiss' => 0 ); } if (is_array($key)) { self::$prefetchStats[$class]->combinedMiss++; return false; } if (array_key_exists($class, self::$prefetched)) { if (is_string($key) && array_key_exists($class, self::$prefetchedNames) && array_key_exists($key, self::$prefetchedNames[$class]) ) { self::$prefetchStats[$class]->hitNames++; return true; } elseif (array_key_exists($key, self::$prefetched[$class])) { self::$prefetchStats[$class]->hits++; return true; } else { self::$prefetchStats[$class]->miss++; return false; } } else { self::$prefetchStats[$class]->miss++; return false; } } public static function getPrefetchStats() { return self::$prefetchStats; } /** * @param $id * @param DbConnection $connection * @return static * @throws NotFoundError */ public static function loadWithAutoIncId($id, DbConnection $connection) { /* Need to cast to int, otherwise the id will be matched against * object_name, which may wreak havoc if an object has a * object_name matching some id. Note that DbObject::set() and * DbObject::setDbProperties() will convert any property to * string, including ids. */ $id = (int) $id; if ($prefetched = static::getPrefetched($id)) { return $prefetched; } $obj = new static; if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { $table = $obj->getTableName(); assert($connection instanceof Db); $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); return self::$dbObjectStore->load($table, $uuid); } $obj->setConnection($connection) ->set($obj->autoincKeyName, $id) ->loadFromDb(); return $obj; } /** * @param $id * @param DbConnection $connection * @return static * @throws NotFoundError */ public static function load($id, DbConnection $connection) { if ($prefetched = static::getPrefetched($id)) { return $prefetched; } /** @var DbObject $obj */ $obj = new static; if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { $table = $obj->getTableName(); assert($connection instanceof Db); $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); return self::$dbObjectStore->load($table, $uuid); } $obj->setConnection($connection)->setKey($id)->loadFromDb(); return $obj; } /** * @param $id * @param DbConnection $connection * @return static */ public static function loadOptional($id, DbConnection $connection): ?DbObject { if ($prefetched = static::getPrefetched($id)) { return $prefetched; } /** @var DbObject $obj */ $obj = new static(); if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { $table = $obj->getTableName(); assert($connection instanceof Db); $uuid = UuidLookup::findUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); if ($uuid) { return self::$dbObjectStore->load($table, $uuid); } return null; } $obj->setConnection($connection)->setKey($id); $properties = $connection->getDbAdapter()->fetchRow($obj->prepareObjectQuery()); if (empty($properties)) { return null; } $obj->setDbProperties($properties); return $obj; } /** * @param DbConnection $connection * @param \Zend_Db_Select $query * @param string|null $keyColumn * * @return static[] */ public static function loadAll(DbConnection $connection, $query = null, $keyColumn = null) { $objects = array(); $db = $connection->getDbAdapter(); if ($query === null) { $dummy = new static; $select = $db->select()->from($dummy->table); } else { $select = $query; } $rows = $db->fetchAll($select); foreach ($rows as $row) { /** @var DbObject $obj */ $obj = new static; $obj->setConnection($connection)->setDbProperties($row); if ($keyColumn === null) { $objects[] = $obj; } else { $objects[$row->$keyColumn] = $obj; } } return $objects; } /** * @param DbConnection $connection * @param bool $force * * @return static[] */ public static function prefetchAll(DbConnection $connection, $force = false) { $dummy = static::create(); $class = get_class($dummy); $autoInc = $dummy->getAutoincKeyName(); $keyName = $dummy->getKeyName(); if ($force || ! array_key_exists($class, self::$prefetched)) { self::$prefetched[$class] = static::loadAll($connection, null, $autoInc); if (! is_array($keyName) && $keyName !== $autoInc) { foreach (self::$prefetched[$class] as $k => $v) { self::$prefetchedNames[$class][$v->$keyName] = $k; } } } return self::$prefetched[$class]; } public static function clearPrefetchCache() { $class = get_called_class(); if (! array_key_exists($class, self::$prefetched)) { return; } unset(self::$prefetched[$class]); unset(self::$prefetchedNames[$class]); unset(self::$prefetchStats[$class]); } public static function clearAllPrefetchCaches() { self::$prefetched = array(); self::$prefetchedNames = array(); self::$prefetchStats = array(); } /** * @param $id * @param DbConnection $connection * @return bool */ public static function exists($id, DbConnection $connection) { if (static::getPrefetched($id)) { return true; } elseif (static::classWasPrefetched()) { return false; } /** @var DbObject $obj */ $obj = new static; if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { $table = $obj->getTableName(); assert($connection instanceof Db); $uuid = UuidLookup::findUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); if ($uuid) { return self::$dbObjectStore->exists($table, $uuid); } return false; } $obj->setConnection($connection)->setKey($id); return $obj->existsInDb(); } public static function uniqueIdExists(UuidInterface $uuid, DbConnection $connection) { $db = $connection->getDbAdapter(); $obj = new static; $column = $obj->getUuidColumn(); $query = $db->select() ->from($obj->getTableName(), $column) ->where("$column = ?", $connection->quoteBinary($uuid->getBytes())); $result = $db->fetchRow($query); return $result !== false; } public static function requireWithUniqueId(UuidInterface $uuid, DbConnection $connection) { if ($object = static::loadWithUniqueId($uuid, $connection)) { return $object; } throw new NotFoundError(sprintf( 'No %s with UUID=%s has been found', (new static)->getTableName(), $uuid->toString() )); } public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection): ?DbObject { $db = $connection->getDbAdapter(); $obj = new static; if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { $table = $obj->getTableName(); assert($connection instanceof Db); return self::$dbObjectStore->load($table, $uuid); } $query = $db->select() ->from($obj->getTableName()) ->where($obj->getUuidColumn() . ' = ?', $connection->quoteBinary($uuid->getBytes())); $result = $db->fetchRow($query); if ($result) { return $obj->setConnection($connection)->setDbProperties($result); } return null; } public function setUniqueId(UuidInterface $uuid) { if ($column = $this->getUuidColumn()) { $binary = $uuid->getBytes(); $current = $this->get($column); if ($current === null) { $this->set($column, $binary); } else { if ($current !== $binary) { throw new RuntimeException(sprintf( 'Changing the UUID (from %s to %s) is not allowed', Uuid::fromBytes($current)->toString(), Uuid::fromBytes($binary)->toString() )); } } } } public function __destruct() { unset($this->db); unset($this->connection); } }