summaryrefslogtreecommitdiffstats
path: root/library/Director/Data/Db/DbObject.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Director/Data/Db/DbObject.php')
-rw-r--r--library/Director/Data/Db/DbObject.php1487
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);
+ }
+}