1547 lines
41 KiB
PHP
1547 lines
41 KiB
PHP
<?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 = [];
|
|
|
|
/* key/value!! */
|
|
protected $booleans = [];
|
|
|
|
/**
|
|
* 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->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(), 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->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);
|
|
}
|
|
}
|