Files
server/usr/share/psa-pear/pear/php/Horde/Rdo/Mapper.php
2026-01-07 20:52:11 +01:00

690 lines
22 KiB
PHP

<?php
/**
* @category Horde
* @package Rdo
*/
/**
* Rdo Mapper class.
*
* Controls mapping of entity obects (instances of Horde_Rdo_Base) from and to
* Horde_Db_Adapters.
*
* Public properties:
* $adapter - Horde_Db_Adapter that stores this Mapper's objects.
*
* $inflector - The Horde_Support_Inflector this mapper uses to singularize
* and pluralize PHP class, database table, and database field/key names.
*
* $table - The Horde_Db_Adapter_Base_TableDefinition object describing
* the main table of this entity.
*
* @category Horde
* @package Rdo
*/
abstract class Horde_Rdo_Mapper implements Countable
{
/**
* If this is true and fields named created_at and updated_at are present,
* Rdo will automatically set creation and last updated timestamps.
* Timestamps are always GMT for portability.
*
* @var boolean
*/
protected $_setTimestamps = true;
/**
* What class should this Mapper create for objects? Defaults to the Mapper
* subclass' name minus "Mapper". So if the Rdo_Mapper subclass is
* UserMapper, it will default to trying to create User objects.
*
* @var string
*/
protected $_classname;
/**
* The definition of the database table (or view, etc.) that holds this
* Mapper's objects.
*
* @var Horde_Db_Adapter_Base_TableDefinition
*/
protected $_tableDefinition;
/**
* Fields that should only be read from the database when they are
* accessed.
*
* @var array
*/
protected $_lazyFields = array();
/**
* Relationships for this entity.
*
* @var array
*/
protected $_relationships = array();
/**
* Relationships that should only be read from the database when
* they are accessed.
*
* @var array
*/
protected $_lazyRelationships = array();
/**
* Default sorting rule to use for all queries made with this mapper. This
* is a SQL ORDER BY fragment (without 'ORDER BY').
*
* @var string
*/
protected $_defaultSort;
/**
* The caching factory, if used
*
* @var Horde_Rdo_Factory
*/
protected $_factory = null;
public function __construct(Horde_Db_Adapter $adapter)
{
$this->adapter = $adapter;
}
/**
* Attach a Horde_Rdo_Factory to the mapper.
* If called without arguments, detaches the mapper from factory
*
* @param Horde_Rdo_Factory $factory A Factory instance or null
* @return Horde_Rdo_Mapper this mapper
*/
public function setFactory(Horde_Rdo_Factory $factory = null)
{
$this->_factory = $factory;
return $this;
}
/**
* Provide read-only, on-demand access to several properties. This
* method will only be called for properties that aren't already
* present; once a property is fetched once it is cached and
* returned directly on any subsequent access.
*
* These properties are available:
*
* adapter: The Horde_Db_Adapter this mapper is using to talk to
* the database.
*
* factory: The Horde_Rdo_Factory instance, if present
*
* inflector: The Horde_Support_Inflector this Mapper uses to singularize
* and pluralize PHP class, database table, and database field/key names.
*
* table: The database table or view that this Mapper manages.
*
* tableDefinition: The Horde_Db_Adapter_Base_TableDefinition object describing
* the table or view this Mapper manages.
*
* fields: Array of all field names that are loaded up front
* (eager loading) from the table.
*
* lazyFields: Array of fields that are only loaded when accessed.
*
* relationships: Array of relationships to other Mappers.
*
* lazyRelationships: Array of relationships to other Mappers which
* are only loaded when accessed.
*
* @param string $key Property name to fetch
*
* @return mixed Value of $key
*/
public function __get($key)
{
switch ($key) {
case 'inflector':
$this->inflector = new Horde_Support_Inflector();
return $this->inflector;
case 'primaryKey':
$this->primaryKey = (string)$this->tableDefinition->getPrimaryKey();
return $this->primaryKey;
case 'table':
$this->table = !empty($this->_table) ? $this->_table : $this->mapperToTable();
return $this->table;
case 'tableDefinition':
$this->tableDefinition = $this->adapter->table($this->table);
return $this->tableDefinition;
case 'fields':
$this->fields = array_diff($this->tableDefinition->getColumnNames(), $this->_lazyFields);
return $this->fields;
case 'lazyFields':
case 'relationships':
case 'lazyRelationships':
case 'factory':
case 'defaultSort':
return $this->{'_' . $key};
}
return null;
}
/**
* Create an instance of $this->_classname from a set of data.
*
* @param array $fields Field names/default values for the new object.
*
* @see $_classname
*
* @return Horde_Rdo_Base An instance of $this->_classname with $fields
* as initial data.
*/
public function map($fields = array())
{
// Guess a classname if one isn't explicitly set.
if (!$this->_classname) {
$this->_classname = $this->mapperToEntity();
if (!$this->_classname) {
throw new Horde_Rdo_Exception('Unable to find an entity class (extending Horde_Rdo_Base) for ' . get_class($this));
}
}
$o = new $this->_classname();
$o->setMapper($this);
$this->mapFields($o, $fields);
if (is_callable(array($o, 'afterMap'))) {
$o->afterMap();
}
return $o;
}
/**
* Update an instance of $this->_classname from a set of data.
*
* @param Horde_Rdo_Base $object The object to update
* @param array $fields Field names/default values for the object
*/
public function mapFields($object, $fields = array())
{
$relationships = array();
foreach ($fields as $fieldName => &$fieldValue) {
if (strpos($fieldName, '@') !== false) {
list($rel, $field) = explode('@', $fieldName, 2);
$relationships[$rel][$field] = $fieldValue;
unset($fields[$fieldName]);
}
if (isset($this->fields[$fieldName])) {
$fieldName = $this->fields[$fieldName];
}
$column = $this->tableDefinition->getColumn($fieldName);
if ($column) {
$fieldValue = $column->typeCast($fieldValue);
}
}
$object->setFields($fields);
if (count($relationships)) {
foreach ($this->relationships as $relationship => $rel) {
if (isset($rel['mapper'])) {
if ($this->_factory) {
$m = $this->_factory->create($rel['mapper']);
}
// @TODO - should be getting this instance from somewhere
// else external, and not passing the adapter along
// automatically.
else {
$m = new $rel['mapper']($this->adapter);
}
} else {
$m = $this->tableToMapper($relationship);
if (is_null($m)) {
// @TODO Throw an exception?
continue;
}
}
// Don't check the table only. If there was a match for LEFT
// JOINs, there may be a "empty" result
if (isset($relationships[$m->table], $relationships[$m->table][$m->primaryKey])) {
$object->$relationship = $m->map($relationships[$m->table]);
}
}
}
}
/**
* Transform a table name to a mapper class name.
*
* @param string $table The database table name to look up.
*
* @return Horde_Rdo_Mapper A new Mapper instance if it exists, else null.
*/
public function tableToMapper($table)
{
if (class_exists(($class = Horde_String::ucwords($table) . 'Mapper'))) {
return new $class;
}
return null;
}
/**
* Transform this mapper's class name to a database table name.
*
* @return string The database table name.
*/
public function mapperToTable()
{
return $this->inflector->pluralize(Horde_String::lower(str_replace('Mapper', '', get_class($this))));
}
/**
* Transform this mapper's class name to an entity class name.
*
* @return string A Horde_Rdo_Base concrete class name if the class exists, else null.
*/
public function mapperToEntity()
{
$class = str_replace('Mapper', '', get_class($this));
if (class_exists($class)) {
return $class;
}
return null;
}
/**
* Count objects that match $query.
*
* @param mixed $query The query to count matches of.
*
* @return integer All objects matching $query.
*/
public function count($query = null)
{
$query = Horde_Rdo_Query::create($query, $this);
$query->setFields('COUNT(*)')
->clearSort();
list($sql, $bindParams) = $query->getQuery();
return $this->adapter->selectValue($sql, $bindParams);
}
/**
* Check if at least one object matches $query.
*
* @param mixed $query Either a primary key, an array of keys
* => values, or a Horde_Rdo_Query object.
*
* @return boolean True or false.
*/
public function exists($query)
{
$query = Horde_Rdo_Query::create($query, $this);
$query->setFields(1)
->clearSort();
list($sql, $bindParams) = $query->getQuery();
return (bool)$this->adapter->selectValue($sql, $bindParams);
}
/**
* Create a new object in the backend with $fields as initial values.
*
* @param array $fields Array of field names => initial values.
*
* @return Horde_Rdo_Base The newly created object.
*/
public function create($fields)
{
// If configured to record creation and update times, set them
// here. We set updated_at to the initial creation time so it's
// always set.
if ($this->_setTimestamps) {
$time = time();
$fields['created_at'] = $time;
$fields['updated_at'] = $time;
}
// Filter out any extra fields.
$fields = array_intersect_key($fields, array_flip($this->tableDefinition->getColumnNames()));
if (!$fields) {
throw new Horde_Rdo_Exception('create() requires at least one field value.');
}
$sql = 'INSERT INTO ' . $this->adapter->quoteTableName($this->table);
$keys = array();
$placeholders = array();
$bindParams = array();
foreach ($fields as $field => $value) {
$keys[] = $this->adapter->quoteColumnName($field);
$placeholders[] = '?';
$bindParams[] = $value;
}
$sql .= ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $placeholders) . ')';
$id = $this->adapter->insert($sql, $bindParams);
return $this->map(array_merge($fields, array($this->primaryKey => $id)));
}
/**
* Updates a record in the backend. $object can be either a
* primary key or an Rdo object. If $object is an Rdo instance
* then $fields will be ignored as values will be pulled from the
* object.
*
* @param string|Rdo $object The Rdo instance or unique id to update.
* @param array $fields If passing a unique id, the array of field properties
* to set for $object.
*
* @return integer Number of objects updated.
*/
public function update($object, $fields = null)
{
if ($object instanceof Horde_Rdo_Base) {
$key = $this->primaryKey;
$id = $object->$key;
$fields = iterator_to_array($object);
if (!$id) {
// Object doesn't exist yet; create it instead.
$o = $this->create($fields);
$this->mapFields($object, iterator_to_array($o));
return 1;
}
} else {
$id = $object;
}
// If configured to record update time, set it here.
if ($this->_setTimestamps) {
$fields['updated_at'] = time();
}
// Filter out any extra fields.
$fields = array_intersect_key($fields, array_flip($this->tableDefinition->getColumnNames()));
if (!$fields) {
// Nothing to change.
return 0;
}
$sql = 'UPDATE ' . $this->adapter->quoteTableName($this->table) . ' SET';
$bindParams = array();
foreach ($fields as $field => $value) {
$sql .= ' ' . $this->adapter->quoteColumnName($field) . ' = ?,';
$bindParams[] = $value;
}
$sql = substr($sql, 0, -1) . ' WHERE ' . $this->primaryKey . ' = ?';
$bindParams[] = $id;
return $this->adapter->update($sql, $bindParams);
}
/**
* Deletes a record from the backend. $object can be either a
* primary key, an Rdo_Query object, or an Rdo object.
*
* @param string|Horde_Rdo_Base|Horde_Rdo_Query $object The Rdo object,
* Horde_Rdo_Query, or unique id to delete.
*
* @return integer Number of objects deleted.
*/
public function delete($object)
{
if ($object instanceof Horde_Rdo_Base) {
$key = $this->primaryKey;
$id = $object->$key;
$query = array($key => $id);
} elseif ($object instanceof Horde_Rdo_Query) {
$query = $object;
} else {
$key = $this->primaryKey;
$query = array($key => $object);
}
$query = Horde_Rdo_Query::create($query, $this);
$clauses = array();
$bindParams = array();
foreach ($query->tests as $test) {
$clauses[] = $this->adapter->quoteColumnName($test['field']) . ' ' . $test['test'] . ' ?';
$bindParams[] = $test['value'];
}
if (!$clauses) {
throw new Horde_Rdo_Exception('Refusing to delete the entire table.');
}
$sql = 'DELETE FROM ' . $this->adapter->quoteTableName($this->table) .
' WHERE ' . implode(' ' . $query->conjunction . ' ', $clauses);
return $this->adapter->delete($sql, $bindParams);
}
/**
* find() can be called in several ways.
*
* Primary key mode: pass find() a numerically indexed array of primary
* keys, and it will return a list of the objects that correspond to those
* keys.
*
* If you pass find() no arguments, all objects of this type will be
* returned.
*
* If you pass find() an associative array, it will be turned into a
* Horde_Rdo_Query object.
*
* If you pass find() a Horde_Rdo_Query, it will return a list of all
* objects matching that query.
*/
public function find($arg = null)
{
if (is_null($arg)) {
$query = null;
} elseif (is_array($arg)) {
if (!count($arg)) {
throw new Horde_Rdo_Exception('No criteria found');
}
if (is_numeric(key($arg))) {
// Numerically indexed arrays are assumed to be an array of
// primary keys.
$query = new Horde_Rdo_Query();
$query->combineWith('OR');
foreach ($argv[0] as $id) {
$query->addTest($this->primaryKey, '=', $id);
}
} else {
$query = $arg;
}
} else {
$query = $arg;
}
// Build a full Query object.
$query = Horde_Rdo_Query::create($query, $this);
return new Horde_Rdo_List($query);
}
/**
* Adds a relation.
*
* - For one-to-one relations, simply updates the relation field.
* - For one-to-many relations, updates the related object's relation field.
* - For many-to-many relations, adds an entry in the "through" table.
* - Performs a no-op if the peer is already related.
*
* @param string $relationship The relationship key in the mapper.
* @param Horde_Rdo_Base $ours The object from this mapper to add the
* relation.
* @param Horde_Rdo_Base $theirs The other object from any mapper to add
* the relation.
*
* @throws Horde_Rdo_Exception
*/
public function addRelation($relationship, Horde_Rdo_Base $ours,
Horde_Rdo_Base $theirs)
{
if ($ours->hasRelation($relationship, $theirs)) {
return;
}
$ourKey = $this->primaryKey;
$theirKey = $theirs->mapper->primaryKey;
if (isset($this->relationships[$relationship])) {
$rel = $this->relationships[$relationship];
} elseif (isset($this->lazyRelationships[$relationship])) {
$rel = $this->lazyRelationships[$relationship];
} else {
throw new Horde_Rdo_Exception('The requested relation is not defined in the mapper');
}
switch ($rel['type']) {
case Horde_Rdo::ONE_TO_ONE:
case Horde_Rdo::MANY_TO_ONE:
$ours->{$rel['foreignKey']} = $theirs->$theirKey;
$ours->save();
break;
case Horde_Rdo::ONE_TO_MANY:
$theirs->{$rel['foreignKey']} = $ours->$ourKey;
$theirs->save();
break;
case Horde_Rdo::MANY_TO_MANY:
$sql = sprintf('INSERT INTO %s (%s, %s) VALUES (?, ?)',
$this->adapter->quoteTableName($rel['through']),
$this->adapter->quoteColumnName($ourKey),
$this->adapter->quoteColumnName($theirKey));
try {
$this->adapter->insert($sql, array($ours->$ourKey, $theirs->$theirKey));
} catch (Horde_Db_Exception $e) {
throw new Horde_Rdo_Exception($e);
}
break;
}
}
/**
* Removes a relation to one of the relationships defined in the mapper.
*
* - For one-to-one and one-to-many relations, simply sets the relation
* field to 0.
* - For many-to-many, either deletes all relations to this object or just
* the relation to a given peer object.
* - Performs a no-op if the peer is already unrelated.
*
* This is a proxy to the mapper's removeRelation method.
*
* @param string $relationship The relationship key in the mapper.
* @param Horde_Rdo_Base $ours The object from this mapper.
* @param Horde_Rdo_Base $theirs The object to remove from the relation.
* @return integer the number of affected relations
*
* @throws Horde_Rdo_Exception
*/
public function removeRelation($relationship, Horde_Rdo_Base $ours,
Horde_Rdo_Base $theirs = null)
{
if (!$ours->hasRelation($relationship, $theirs)) {
return;
}
$ourKey = $this->primaryKey;
if (isset($this->relationships[$relationship])) {
$rel = $this->relationships[$relationship];
} elseif (isset($this->lazyRelationships[$relationship])) {
$rel = $this->lazyRelationships[$relationship];
} else {
throw new Horde_Rdo_Exception('The requested relation is not defined in the mapper');
}
switch ($rel['type']) {
case Horde_Rdo::ONE_TO_ONE:
case Horde_Rdo::MANY_TO_ONE:
$ours->{$rel['foreignKey']} = null;
$ours->save();
return 1;
break;
case Horde_Rdo::ONE_TO_MANY:
$theirs->{$rel['foreignKey']} = null;
$theirs->save();
return 1;
break;
case Horde_Rdo::MANY_TO_MANY:
$sql = sprintf('DELETE FROM %s WHERE %s = ? ',
$this->adapter->quoteTableName($rel['through']),
$this->adapter->quoteColumnName($ourKey));
$values = array($ours->$ourKey);
if (!empty($theirs)) {
$theirKey = $theirs->mapper->primaryKey;
$sql .= sprintf(' AND %s = ?',
$this->adapter->quoteColumnName($theirKey));
$values[] = $theirs->$theirKey;
}
try {
return $this->adapter->delete($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Rdo_Exception($e);
}
break;
}
}
/**
* findOne can be called in several ways.
*
* Primary key mode: pass find() a single primary key, and it will return a
* single object matching that primary key.
*
* If you pass findOne() no arguments, the first object of this type will be
* returned.
*
* If you pass findOne() an associative array, it will be turned into a
* Horde_Rdo_Query object.
*
* If you pass findOne() a Horde_Rdo_Query, it will return the first object
* matching that query.
*/
public function findOne($arg = null)
{
if (is_null($arg)) {
$query = null;
} elseif (is_scalar($arg)) {
$query = array($this->primaryKey => $arg);
} else {
$query = $arg;
}
// Build a full Query object, and limit it to one result.
$query = Horde_Rdo_Query::create($query, $this);
$query->limit(1);
$list = new Horde_Rdo_List($query);
return $list->current();
}
/**
* Set a default sort rule for all queries done with this Mapper.
*
* @param string $sort SQL sort fragment, such as 'updated DESC'
*/
public function sortBy($sort)
{
$this->_defaultSort = $sort;
return $this;
}
}