diff options
Diffstat (limited to 'library/Director/Data/Db/DbObject.php')
-rw-r--r-- | library/Director/Data/Db/DbObject.php | 1487 |
1 files changed, 1487 insertions, 0 deletions
diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php new file mode 100644 index 0000000..6ecae8b --- /dev/null +++ b/library/Director/Data/Db/DbObject.php @@ -0,0 +1,1487 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Data\InvalidDataException; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Branch\UuidLookup; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use InvalidArgumentException; +use LogicException; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; +use RuntimeException; +use Zend_Db_Adapter_Abstract; +use Zend_Db_Exception; + +/** + * Base class for ... + */ +abstract class DbObject +{ + /** @var DbConnection $connection */ + protected $connection; + + /** @var string Table name. MUST be set when extending this class */ + protected $table; + + /** @var Zend_Db_Adapter_Abstract */ + protected $db; + + /** + * Default columns. MUST be set when extending this class. Each table + * column MUST be defined with a default value. Default value may be null. + * + * @var array + */ + protected $defaultProperties; + + /** + * Properties as loaded from db + */ + protected $loadedProperties; + + /** + * Whether at least one property has been modified + */ + protected $hasBeenModified = false; + + /** + * Whether this object has been loaded from db + */ + protected $loadedFromDb = false; + + /** + * Object properties + */ + protected $properties = array(); + + /** + * Property names that have been modified since object creation + */ + protected $modifiedProperties = array(); + + /** + * Unique key name, could be primary + */ + protected $keyName; + + /** + * Set this to an eventual autoincrementing column. May equal $keyName + */ + protected $autoincKeyName; + + /** @var string optional uuid column */ + protected $uuidColumn; + + /** @var bool forbid updates to autoinc values */ + protected $protectAutoinc = true; + + protected $binaryProperties = []; + + /** + * Filled with object instances when prefetchAll is used + */ + protected static $prefetched = array(); + + /** + * object_name => 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->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 ((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 + */ + 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() + { + $select = $this->db->select()->from($this->table)->where($this->createWhere()); + $properties = $this->db->fetchRow($select); + + 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); + } + + /** + * @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; + } + + /** + * 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(), 1) // 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->getUniqueId()->getBytes()) + ); + } + 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 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) + { + $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); + } +} |