--- /dev/null
+<?php
+/**
+ * Copyright 2006-2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package Horde_Db
+ */
+
+/**
+ * Horde_Db namespace - holds constants and global Db functions.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package Horde_Db
+ */
+class Horde_Rdo
+{
+ /**
+ * One-to-one relationships.
+ */
+ const ONE_TO_ONE = 1;
+
+ /**
+ * One-to-many relationships (this object has many children).
+ */
+ const ONE_TO_MANY = 2;
+
+ /**
+ * Many-to-one relationships (this object is one of many children
+ * of a single parent).
+ */
+ const MANY_TO_ONE = 3;
+
+ /**
+ * Many-to-many relationships (this object relates to many
+ * objects, each of which relate to many objects of this type).
+ */
+ const MANY_TO_MANY = 4;
+
+ /**
+ * Global inflector object.
+ *
+ * @var Horde_Support_Inflector
+ */
+ protected static $_inflector;
+
+ /**
+ * Get the global inflector object.
+ *
+ * @return Horde_Support_Inflector
+ */
+ public static function getInflector()
+ {
+ if (!self::$_inflector) {
+ self::$_inflector = new Horde_Support_Inflector;
+ }
+ return self::$_inflector;
+ }
+
+ /**
+ * Set a custom global inflector.
+ *
+ * @param Horde_Support_Inflector $inflector
+ */
+ public static function setInflector($inflector)
+ {
+ self::$_inflector = $inflector;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Rdo
+ */
+
+/**
+ * Horde_Rdo_Base abstract class (Rampage Data Objects). Entity
+ * classes extend this baseline.
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+abstract class Horde_Rdo_Base implements IteratorAggregate {
+
+ /**
+ * The Horde_Rdo_Mapper instance associated with this Rdo object. The
+ * Mapper takes care of all backend access.
+ *
+ * @see Horde_Rdo_Mapper
+ * @var Horde_Rdo_Mapper
+ */
+ protected $_mapper;
+
+ /**
+ * This object's fields.
+ *
+ * @var array
+ */
+ protected $_fields = array();
+
+ /**
+ * Constructor. Can be called directly by a programmer, or is
+ * called in Horde_Rdo_Mapper::map(). Takes an associative array
+ * of initial object values.
+ *
+ * @param array $fields Initial values for the new object.
+ *
+ * @see Horde_Rdo_Mapper::map()
+ */
+ public function __construct($fields = array())
+ {
+ $this->_fields = $fields;
+ }
+
+ /**
+ * When Rdo objects are cloned, unset the unique id that
+ * identifies them so that they can be modified and saved to the
+ * backend as new objects. If you don't really want a new object,
+ * don't clone.
+ */
+ public function __clone()
+ {
+ // @TODO Support composite primary keys
+ unset($this->{$this->getMapper()->model->key});
+
+ // @TODO What about associated objects?
+ }
+
+ /**
+ * Fetch fields that haven't yet been loaded. Lazy-loaded fields
+ * and lazy-loaded relationships are handled this way. Once a
+ * field is retrieved, it is cached in the $_fields array so it
+ * doesn't need to be fetched again.
+ *
+ * @param string $field The name of the field to access.
+ *
+ * @return mixed The value of $field or null.
+ */
+ public function __get($field)
+ {
+ // Honor any explicit getters.
+ $fieldMethod = 'get' . ucfirst($field);
+ // If an Rdo_Base subclass has a __call() method, is_callable
+ // returns true on every method name, so use method_exists
+ // instead.
+ if (method_exists($this, $fieldMethod)) {
+ return call_user_func(array($this, $fieldMethod));
+ }
+
+ if (isset($this->_fields[$field])) {
+ return $this->_fields[$field];
+ }
+
+ $mapper = $this->getMapper();
+
+ // Look for lazy fields first, then relationships.
+ if (in_array($field, $mapper->lazyFields)) {
+ // @TODO Support composite primary keys
+ $query = new Horde_Rdo_Query($mapper);
+ $query->setFields($field)
+ ->addTest($mapper->model->key, '=', $this->{$mapper->model->key});
+ $this->_fields[$field] = $mapper->adapter->queryOne($query);
+ return $this->_fields[$field];
+ } elseif (isset($mapper->lazyRelationships[$field])) {
+ $rel = $mapper->lazyRelationships[$field];
+ } else {
+ return null;
+ }
+
+ // Try to find the Mapper class for the object the
+ // relationship is with, and fail if we can't.
+ if (isset($rel['mapper'])) {
+ $m = new $rel['mapper']();
+ } else {
+ $m = $mapper->tableToMapper($field);
+ if (is_null($m)) {
+ return null;
+ }
+ }
+
+ // Based on the kind of relationship, fetch the appropriate
+ // objects and fill the cache.
+ switch ($rel['type']) {
+ case Horde_Rdo::ONE_TO_ONE:
+ case Horde_Rdo::MANY_TO_ONE:
+ if (isset($rel['query'])) {
+ $query = $this->_fillPlaceholders($rel['query']);
+ $this->_fields[$field] = $m->findOne($query);
+ } else {
+ $this->_fields[$field] = $m->find($this->{$rel['foreignKey']});
+ }
+ break;
+
+ case Horde_Rdo::ONE_TO_MANY:
+ $this->_fields[$field] = $this->cache($field,
+ $m->find(array($rel['foreignKey'] => $this->{$rel['foreignKey']})));
+ break;
+
+ case Horde_Rdo::MANY_TO_MANY:
+ $key = $mapper->model->key;
+ $query = new Horde_Rdo_Query();
+ $on = isset($rel['on']) ? $rel['on'] : $m->model->key;
+ $query->addRelationship($field, array('mapper' => $mapper,
+ 'table' => $rel['through'],
+ 'type' => Horde_Rdo::MANY_TO_MANY,
+ 'query' => array($on => new Horde_Rdo_Query_Literal($on), $key => $this->$key)));
+ $this->_fields[$field] = $m->find($query);
+ break;
+ }
+
+ return $this->_fields[$field];
+ }
+
+ /**
+ * Set a field's value.
+ *
+ * @param string $field The field to set
+ * @param mixed $value The field's new value
+ */
+ public function __set($field, $value)
+ {
+ // Honor any explicit setters.
+ $fieldMethod = 'set' . ucfirst($field);
+ // If an Rdo_Base subclass has a __call() method, is_callable
+ // returns true on every method name, so use method_exists
+ // instead.
+ if (method_exists($this, $fieldMethod)) {
+ return call_user_func(array($this, $fieldMethod), $value);
+ }
+
+ $this->_fields[$field] = $value;
+ }
+
+ /**
+ * Allow using isset($rdo->foo) to check for field or
+ * relationship presence.
+ *
+ * @param string $field The field name to check existence of.
+ */
+ public function __isset($field)
+ {
+ $m = $this->getMapper();
+ return isset($m->fields[$field])
+ || isset($m->lazyFields[$field])
+ || isset($m->relationships[$field])
+ || isset($m->lazyRelationships[$field]);
+ }
+
+ /**
+ * Allow using unset($rdo->foo) to unset a basic
+ * field. Relationships cannot be unset in this way.
+ *
+ * @param string $field The field name to unset.
+ */
+ public function __unset($field)
+ {
+ // @TODO Should unsetting a MANY_TO_MANY relationship remove
+ // the relationship?
+ unset($this->_fields[$field]);
+ }
+
+ /**
+ * Implement the IteratorAggregate interface. Looping over an Rdo
+ * object goes through each property of the object in turn.
+ *
+ * @return Horde_Rdo_Iterator The Iterator instance.
+ */
+ public function getIterator()
+ {
+ return new Horde_Rdo_Iterator($this);
+ }
+
+ /**
+ * Get a Mapper instance that can be used to manage this
+ * object. The Mapper instance can come from a few places:
+ *
+ * - If the class <RdoClassName>Mapper exists, it will be used
+ * automatically.
+ *
+ * - Any Rdo instance created with Horde_Rdo_Mapper::map() will have a
+ * $mapper object set automatically.
+ *
+ * - Subclasses can override getMapper() to return the correct
+ * mapper object.
+ *
+ * - The programmer can call $rdoObject->setMapper($mapper) to provide a
+ * mapper object.
+ *
+ * A Horde_Rdo_Exception will be thrown if none of these
+ * conditions are met.
+ *
+ * @return Horde_Rdo_Mapper The Mapper instance managing this object.
+ */
+ public function getMapper()
+ {
+ if (!$this->_mapper) {
+ $class = get_class($this) . 'Mapper';
+ if (class_exists($class)) {
+ $this->_mapper = new $class();
+ } else {
+ throw new Horde_Rdo_Exception('No Horde_Rdo_Mapper object found. Override getMapper() or define the ' . get_class($this) . 'Mapper class.');
+ }
+ }
+
+ return $this->_mapper;
+ }
+
+ /**
+ * Associate this Rdo object with the Mapper instance that will
+ * manage it. Called automatically by Horde_Rdo_Mapper:map().
+ *
+ * @param Horde_Rdo_Mapper $mapper The Mapper to manage this Rdo object.
+ *
+ * @see Horde_Rdo_Mapper::map()
+ */
+ public function setMapper($mapper)
+ {
+ $this->_mapper = $mapper;
+ }
+
+ /**
+ * Save any changes to the backend.
+ *
+ * @return boolean Success.
+ */
+ public function save()
+ {
+ return $this->getMapper()->update($this) == 1;
+ }
+
+ /**
+ * Delete this object from the backend.
+ *
+ * @return boolean Success or failure.
+ */
+ public function delete()
+ {
+ return $this->getMapper()->delete($this) == 1;
+ }
+
+ /**
+ * Take a query array and replace @field@ placeholders with values
+ * from this object.
+ *
+ * @param array $query The query to process placeholders on.
+ *
+ * @return array The query with placeholders filled in.
+ */
+ protected function _fillPlaceholders($query)
+ {
+ foreach (array_keys($query) as $field) {
+ $value = $query[$field];
+ if (preg_match('/^@(.*)@$/', $value, $matches)) {
+ $query[$field] = $this->{$matches[1]};
+ }
+ }
+
+ return $query;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Rdo
+ */
+
+/**
+ * Iterator for Horde_Rdo_Base objects that allows relationships and
+ * decorated objects to be handled gracefully.
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+class Horde_Rdo_Iterator implements Iterator {
+
+ /**
+ * @var Horde_Rdo_Base
+ */
+ private $_rdo;
+
+ /**
+ * List of keys that we'll iterator over. This is the combined
+ * list of the fields, lazyFields, relationships, and
+ * lazyRelationships properties from the objects Horde_Rdo_Mapper.
+ */
+ private $_keys = array();
+
+ /**
+ * Current index
+ *
+ * @var mixed
+ */
+ private $_index = null;
+
+ /**
+ * Are we inside the array bounds?
+ *
+ * @var boolean
+ */
+ private $_valid = false;
+
+ /**
+ * New Horde_Rdo_Iterator for iterating over Rdo objects.
+ *
+ * @param Horde_Rdo_Base $rdo The object to iterate over
+ */
+ public function __construct($rdo)
+ {
+ $this->_rdo = $rdo;
+
+ $m = $rdo->getMapper();
+ $this->_keys = array_merge($m->fields,
+ $m->lazyFields,
+ array_keys($m->relationships),
+ array_keys($m->lazyRelationships));
+ }
+
+ /**
+ * Reset to the first key.
+ */
+ public function rewind()
+ {
+ $this->_valid = (false !== reset($this->_keys));
+ }
+
+ /**
+ * Return the current value.
+ *
+ * @return mixed The current value
+ */
+ public function current()
+ {
+ $key = $this->key();
+ return $this->_rdo->$key;
+ }
+
+ /**
+ * Return the current key.
+ *
+ * @return mixed The current key
+ */
+ public function key()
+ {
+ return current($this->_keys);
+ }
+
+ /**
+ * Move to the next key in the iterator.
+ */
+ public function next()
+ {
+ $this->_valid = (false !== next($this->_keys));
+ }
+
+ /**
+ * Check array bounds.
+ *
+ * @return boolean Inside array bounds?
+ */
+ public function valid()
+ {
+ return $this->_valid;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Rdo
+ */
+
+/**
+ * Iterator for collections of Rdo objects.
+ *
+ * @TODO implement ArrayAccess as well?
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+class Horde_Rdo_List implements Iterator {
+
+ /**
+ * Rdo Mapper
+ * @var Horde_Rdo_Mapper
+ */
+ protected $_mapper;
+
+ /**
+ * SQL query to run
+ * @var string
+ */
+ protected $_sql;
+
+ /**
+ * Bind parameters
+ * @var array
+ */
+ protected $_bindParams = array();
+
+ /**
+ * Result resource
+ * @var Iterator
+ */
+ protected $_result;
+
+ /**
+ * Current object
+ * @var Horde_Rdo_Base
+ */
+ protected $_current;
+
+ /**
+ * Current list offset.
+ * @var integer
+ */
+ protected $_index;
+
+ /**
+ * Are we at the end of the list?
+ * @var boolean
+ */
+ protected $_eof;
+
+ /**
+ * Constructor.
+ *
+ * @param mixed $query The query to run when results are
+ * requested. Can be a Horde_Rdo_Query object, a literal SQL
+ * query, or a tuple containing an SQL string and an array of bind
+ * parameters to use.
+ * @param Horde_Rdo_Mapper $mapper Mapper to create objects for this list from.
+ */
+ public function __construct($query, $mapper = null)
+ {
+ if ($query instanceof Horde_Rdo_Query) {
+ // Make sure we have a Mapper object, which can be passed
+ // implicitly by being set on the Query.
+ if (!$mapper) {
+ if (!$query->mapper) {
+ throw new Horde_Rdo_Exception('Mapper must be set on the Query object or explicitly passed.');
+ }
+ $mapper = $query->mapper;
+ }
+
+ // Convert the query into a SQL statement and an array of
+ // bind parameters.
+ list($this->_sql, $this->_bindParams) = $mapper->adapter->dml->getQuery($query);
+ } elseif (is_string($query)) {
+ // Straight SQL query, empty bind parameters array.
+ $this->_sql = $query;
+ $this->_bindParams = array();
+ } else {
+ // $query is already an array with SQL and bind parameters.
+ list($this->_sql, $this->_bindParams) = $query;
+ }
+
+ // Keep a handle on the Mapper object for running the query.
+ $this->_mapper = $mapper;
+ }
+
+ /**
+ * Destructor - release any resources.
+ */
+ public function __destruct()
+ {
+ if ($this->_result) {
+ unset($this->_result);
+ }
+ }
+
+ /**
+ * Implementation of the rewind() method for iterator.
+ */
+ public function rewind()
+ {
+ if ($this->_result) {
+ unset($this->_result);
+ }
+ $this->_current = null;
+ $this->_index = null;
+ $this->_eof = true;
+ $this->_result = $this->_mapper->adapter->select($this->_sql, $this->_bindParams);
+
+ $this->next();
+ }
+
+ /**
+ * Implementation of the current() method for iterator.
+ *
+ * @return mixed The current row, or null if no rows.
+ */
+ public function current()
+ {
+ if (is_null($this->_result)) {
+ $this->rewind();
+ }
+ return $this->_current;
+ }
+
+ /**
+ * Implementation of the key() method for iterator.
+ *
+ * @return mixed The current row number (starts at 0), or NULL if no rows
+ */
+ public function key()
+ {
+ if (is_null($this->_result)) {
+ $this->rewind();
+ }
+ return $this->_index;
+ }
+
+ /**
+ * Implementation of the next() method.
+ *
+ * @return Horde_Rdo_Base|null The next Rdo object in the set or
+ * null if no more results.
+ */
+ public function next()
+ {
+ if (is_null($this->_result)) {
+ $this->rewind();
+ }
+
+ if ($this->_result) {
+ $row = $this->_result->fetch();
+ if (!$row) {
+ $this->_eof = true;
+ } else {
+ $this->_eof = false;
+
+ if (is_null($this->_index)) {
+ $this->_index = 0;
+ } else {
+ ++$this->_index;
+ }
+
+ $this->_current = $this->_mapper->map($row);
+ }
+ }
+
+ return $this->_current;
+ }
+
+ /**
+ * Implementation of the valid() method for iterator
+ *
+ * @return boolean Whether the iteration is valid
+ */
+ public function valid()
+ {
+ if (is_null($this->_result)) {
+ $this->rewind();
+ }
+ return !$this->_eof;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Rdo Mapper base class.
+ *
+ * @category Horde
+ * @package Horde_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.
+ *
+ * $model - The Horde_Rdo_Model object describing the main table of
+ * this entity.
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+abstract class Horde_Rdo_Mapper implements Countable {
+
+ /**
+ * If this is true and fields named created and updated 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 name of the database table (or view, etc.) that holds this
+ * Mapper's objects.
+ *
+ * @var string
+ */
+ protected $_table;
+
+ /**
+ * 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;
+
+ /**
+ * 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.
+ *
+ * inflector: The Horde_Support_Inflector this mapper uses to singularize
+ * and pluralize PHP class, database table, and database field/key names.
+ *
+ * model: The Horde_Rdo_Model object describing the table or view
+ * this Mapper manages.
+ *
+ * fields: Array of all field names that are loaded up front
+ * (eager loading) from the Model.
+ *
+ * lazyFields: Array of fields that are only loaded when accessed.
+ *
+ * relationships: Array of relationships to other Models.
+ *
+ * lazyRelationships: Array of relationships to other Models 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 'adapter':
+ $this->adapter = $this->getAdapter();
+ return $this->adapter;
+
+ case 'inflector':
+ return Horde_Rdo::getInflector();
+
+ case 'model':
+ $this->model = new Horde_Rdo_Model;
+ if ($this->_table) {
+ $this->model->table = $this->_table;
+ } else {
+ $this->model->table = $this->mapperToTable();
+ }
+ $this->model->load($this);
+ return $this->model;
+
+ case 'fields':
+ $this->fields = array_diff($this->model->listFields(), $this->_lazyFields);
+ return $this->fields;
+
+ case 'lazyFields':
+ case 'relationships':
+ case 'lazyRelationships':
+ case 'defaultSort':
+ return $this->{'_' . $key};
+ }
+
+ return null;
+ }
+
+ /**
+ * Associate an adapter with this mapper. Not needed in the
+ * general case if getAdapter() is overridden in the concrete
+ * Mapper implementation.
+ *
+ * @param Horde_Db_Adapter $adapter Horde_Db_Adapter to store objects.
+ *
+ * @see getAdapter()
+ */
+ public function setAdapter($adapter)
+ {
+ $this->adapter = $adapter;
+ }
+
+ /**
+ * getAdapter() must be overridden by Horde_Rdo_Mapper subclasses
+ * if they don't provide $adapter in some other way (by calling
+ * setAdapter() or on construction, for example), and there is no
+ * global Adapter.
+ *
+ * @see setAdapter()
+ *
+ * @return Horde_Db_Adapter The adapter for storing this Mapper's objects.
+ */
+ public function getAdapter()
+ {
+ $adapter = Horde_Db::getAdapter();
+ if ($adapter) {
+ return $adapter;
+ }
+ throw new Horde_Rdo_Exception('You must override getAdapter(), assign a Horde_Db_Adapter by calling setAdapter(), or set a global adapter by calling Horde_Db::setAdapter().');
+ }
+
+ /**
+ * 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();
+ }
+
+ $relationships = array();
+ foreach ($fields as $fieldName => $fieldValue) {
+ if (strpos($fieldName, '@') !== false) {
+ list($rel, $field) = explode('@', $fieldName, 2);
+ $relationships[$rel][$field] = $fieldValue;
+ unset($fields[$fieldName]);
+ }
+ }
+
+ $o = new $this->_classname($fields);
+ $o->setMapper($this);
+
+ if (count($relationships)) {
+ foreach ($this->relationships as $relationship => $rel) {
+ if (isset($rel['mapper'])) {
+ $m = new $rel['mapper']();
+ } else {
+ $m = $this->tableToMapper($relationship);
+ if (is_null($m)) {
+ // @TODO Throw an exception?
+ continue;
+ }
+ }
+
+ if (isset($relationships[$m->model->table])) {
+ $o->$relationship = $m->map($relationships[$m->model->table]);
+ }
+ }
+ }
+
+ if (is_callable(array($o, 'afterMap'))) {
+ $o->afterMap();
+ }
+
+ return $o;
+ }
+
+ /**
+ * 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 = 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(strtolower(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) = $this->adapter->dml->getCount($query);
+ return $this->adapter->selectOne($sql, $bindParams);
+ }
+
+ /**
+ * Check if at least one object matches $query.
+ *
+ * @param mixed $query Either a primary key, an array of keys
+ * => values, or an 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) = $this->adapter->dml->getQuery($query);
+ return (bool)$this->adapter->selectOne($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 to the initial creation time so it's
+ // always set.
+ if ($this->_setTimestamps) {
+ $time = gmmktime();
+ $fields['created'] = $time;
+ $fields['updated'] = $time;
+ }
+
+ // Filter out any extra fields.
+ $fields = array_intersect_key($fields, $this->model->getFields());
+
+ if (!$fields) {
+ throw new Horde_Rdo_Exception('create() requires at least one field value.');
+ }
+
+ $sql = 'INSERT INTO ' . $this->adapter->quoteColumnName($this->model->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(array($this->model->key => $id),
+ $fields));
+ }
+
+ /**
+ * 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->model->key;
+ $id = $object->$key;
+ $fields = iterator_to_array($object);
+
+ if (!$id) {
+ // Object doesn't exist yet; create it instead.
+ $object = $this->create($fields);
+ return 1;
+ }
+ } else {
+ $id = $object;
+ }
+
+ // If configured to record update time, set it here.
+ if ($this->_setTimestamps) {
+ $fields['updated'] = gmmktime();
+ }
+
+ // Filter out any extra fields.
+ $fields = array_intersect_key($fields, $this->model->getFields());
+
+ if (!$fields) {
+ // Nothing to change.
+ return true;
+ }
+
+ $sql = 'UPDATE ' . $this->adapter->quoteColumnName($this->model->table) . ' SET';
+ $bindParams = array();
+ foreach ($fields as $field => $value) {
+ $sql .= ' ' . $this->adapter->quoteColumnName($field) . ' = ?,';
+ $bindParams[] = $value;
+ }
+ $sql = substr($sql, 0, -1) . ' WHERE ' . $this->model->key . ' = ?';
+ $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->model->key;
+ $id = $object->$key;
+ $query = array($key => $id);
+ } elseif ($object instanceof Horde_Rdo_Query) {
+ $query = $object;
+ } else {
+ $key = $this->model->key;
+ $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->quoteColumnName($this->model->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.
+ $key = $this->model->key;
+ $query = new Horde_Rdo_Query();
+ $query->combineWith('OR');
+ foreach ($argv[0] as $id) {
+ $query->addTest($key, '=', $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);
+ }
+
+ /**
+ * 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->model->key => $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;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Model class for Rdo.
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Rdo
+ */
+class Horde_Rdo_Model {
+
+ /**
+ */
+ protected $_fields = array();
+
+ /**
+ */
+ public $table;
+
+ /**
+ */
+ const INTEGER = 'int';
+
+ /**
+ */
+ const NUMBER = 'number';
+
+ /**
+ */
+ const STRING = 'string';
+
+ /**
+ */
+ const TEXT = 'text';
+
+ /**
+ * Fill the model using the mapper's backend.
+ */
+ public function load($mapper)
+ {
+ $mapper->adapter->loadModel($this);
+ }
+
+ /**
+ */
+ public static function __set_state($properties)
+ {
+ $model = new Horde_Rdo_Model();
+ foreach ($properties as $key => $val) {
+ $model->$key = $val;
+ }
+ }
+
+ /**
+ */
+ public function hasField($field)
+ {
+ return isset($this->_fields[$field]);
+ }
+
+ /**
+ */
+ public function addField($field, $params = array())
+ {
+ $params = array_merge(array('key' => null, 'null' => false), $params);
+
+ if (!strncasecmp($params['null'], 'n', 1)) {
+ $params['null'] = false;
+ } elseif (!strncasecmp($params['null'], 'y', 1)) {
+ $params['null'] = true;
+ }
+
+ $this->_fields[$field] = $params;
+ if (isset($params['type'])) {
+ $this->setFieldType($field, $params['type']);
+ }
+ }
+
+ /**
+ */
+ public function getField($field)
+ {
+ return isset($this->_fields[$field]) ? $this->_fields[$field] : null;
+ }
+
+ /**
+ */
+ public function getFields()
+ {
+ return $this->_fields;
+ }
+
+ /**
+ */
+ public function listFields()
+ {
+ return array_keys($this->_fields);
+ }
+
+ /**
+ */
+ public function setFieldType($field, $rawtype)
+ {
+ if (stripos($rawtype, 'int') !== false) {
+ $this->_fields[$field]['type'] = self::INTEGER;
+ } elseif (stripos($rawtype, 'char') !== false) {
+ $this->_fields[$field]['type'] = self::STRING;
+ } elseif (stripos($rawtype, 'float') !== false
+ || stripos($rawtype, 'decimal') !== false) {
+ $this->_fields[$field]['type'] = self::NUMBER;
+ } elseif ($rawtype == 'text') {
+ $this->_fields[$field]['type'] = self::TEXT;
+ }
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Represent a single query or a tree of many query elements uniformly to clients.
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Rdo
+ */
+class Horde_Rdo_Query {
+
+ /**
+ * @var Horde_Rdo_Mapper
+ */
+ public $mapper;
+
+ /**
+ * @var string
+ */
+ public $conjunction = 'AND';
+
+ /**
+ * @var array
+ */
+ public $fields = array('*');
+
+ /**
+ * @var array
+ */
+ public $tests = array();
+
+ /**
+ * @var array
+ */
+ public $relationships = array();
+
+ /**
+ * @var array
+ */
+ protected $_sortby = array();
+
+ /**
+ * @var integer
+ */
+ public $limit;
+
+ /**
+ * @var integer
+ */
+ public $limitOffset = null;
+
+ /**
+ * Turn any of the acceptable query shorthands into a full
+ * Horde_Rdo_Query object. If you pass an existing Horde_Rdo_Query
+ * object in, it will be cloned before it's returned so that it
+ * can be safely modified.
+ *
+ * @param mixed $query The query to convert to an object.
+ * @param Horde_Rdo_Mapper $mapper The Mapper object governing this query.
+ *
+ * @return Horde_Rdo_Query The full Horde_Rdo_Query object.
+ */
+ public static function create($query, $mapper = null)
+ {
+ if ($query instanceof Horde_Rdo_Query ||
+ $query instanceof Horde_Rdo_Query_Literal) {
+ $query = clone($query);
+ if (!is_null($mapper)) {
+ $query->setMapper($mapper);
+ }
+ return $query;
+ }
+
+ $q = new Horde_Rdo_Query($mapper);
+
+ if (is_scalar($query)) {
+ $q->addTest($mapper->model->key, '=', $query);
+ } elseif ($query) {
+ $q->combineWith('AND');
+ foreach ($query as $key => $value) {
+ $q->addTest($key, '=', $value);
+ }
+ }
+
+ return $q;
+ }
+
+ /**
+ * @param Horde_Rdo_Mapper $mapper Rdo mapper base class
+ */
+ public function __construct($mapper = null)
+ {
+ $this->setMapper($mapper);
+ }
+
+ /**
+ * @param Horde_Rdo_Mapper $mapper Rdo mapper base class
+ *
+ * @return Horde_Rdo_Query Return the query object for fluent chaining.
+ */
+ public function setMapper($mapper)
+ {
+ if ($mapper === $this->mapper) {
+ return $this;
+ }
+
+ $this->mapper = $mapper;
+
+ // Fetch all non-lazy-loaded fields for the mapper.
+ $this->setFields($mapper->fields, $mapper->model->table . '.');
+
+ if (!is_null($mapper)) {
+ // Add all non-lazy relationships.
+ foreach ($mapper->relationships as $relationship => $rel) {
+ if (isset($rel['mapper'])) {
+ $m = new $rel['mapper']();
+ } else {
+ $m = $this->mapper->tableToMapper($relationship);
+ if (is_null($m)) {
+ throw new Horde_Rdo_Exception('Unable to find a Mapper class for eager-loading relationship ' . $relationship);
+ }
+ }
+
+ // Add the fields for this relationship to the query.
+ $this->addFields($m->fields, $m->model->table . '.@');
+
+ switch ($rel['type']) {
+ case Horde_Rdo::ONE_TO_ONE:
+ case Horde_Rdo::MANY_TO_ONE:
+ if (isset($rel['query'])) {
+ $query = $this->_fillJoinPlaceholders($m, $mapper, $rel['query']);
+ } else {
+ $query = array($mapper->model->table . '.' . $rel['foreignKey'] => new Horde_Rdo_Query_Literal($m->model->table . '.' . $m->model->key));
+ }
+ $this->addRelationship($relationship, array('mapper' => $m,
+ 'type' => $rel['type'],
+ 'query' => $query));
+ break;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param array $fields The fields to load with this query.
+ *
+ * @return Horde_Rdo_Query Returns self for fluent method chaining.
+ */
+ public function setFields($fields, $fieldPrefix = null)
+ {
+ if (!is_array($fields)) {
+ $fields = array($fields);
+ }
+ if (!is_null($fieldPrefix)) {
+ array_walk($fields, array($this, '_prefix'), $fieldPrefix);
+ }
+ $this->fields = $fields;
+ return $this;
+ }
+
+ /**
+ * @param array $fields Additional Fields to load with this query.
+ *
+ * @return Horde_Rdo_Query Returns self for fluent method chaining.
+ */
+ public function addFields($fields, $fieldPrefix = null)
+ {
+ if (!is_null($fieldPrefix)) {
+ array_walk($fields, array($this, '_prefix'), $fieldPrefix);
+ }
+ $this->fields = array_merge($this->fields, $fields);
+ }
+
+ /**
+ * @param string $conjunction SQL conjunction such as "AND", "OR".
+ */
+ public function combineWith($conjunction)
+ {
+ $this->conjunction = $conjunction;
+ return $this;
+ }
+
+ /**
+ */
+ public function addTest($field, $test, $value)
+ {
+ $this->tests[] = array('field' => $field,
+ 'test' => $test,
+ 'value' => $value);
+ return $this;
+ }
+
+ /**
+ */
+ public function addRelationship($relationship, $args)
+ {
+ if (!isset($args['mapper'])) {
+ throw new InvalidArgumentException('Relationships must contain a Horde_Rdo_Mapper object.');
+ }
+ if (!isset($args['table'])) {
+ $args['table'] = $args['mapper']->model->table;
+ }
+ if (!isset($args['type'])) {
+ $args['type'] = Horde_Rdo::MANY_TO_MANY;
+ }
+ if (!isset($args['join_type'])) {
+ switch ($args['type']) {
+ case Horde_Rdo::ONE_TO_ONE:
+ case Horde_Rdo::MANY_TO_ONE:
+ $args['join_type'] = 'INNER JOIN';
+ break;
+
+ default:
+ $args['join_type'] = 'LEFT JOIN';
+ }
+ }
+
+ $this->relationships[$relationship] = $args;
+ return $this;
+ }
+
+ /**
+ * Add a sorting rule.
+ *
+ * @param string $sort SQL sort fragment, such as 'updated DESC'
+ */
+ public function sortBy($sort)
+ {
+ $this->_sortby[] = $sort;
+ return $this;
+ }
+
+ /**
+ */
+ public function clearSort()
+ {
+ $this->_sortby = array();
+ return $this;
+ }
+
+ /**
+ * Restrict the query to a subset of the results.
+ *
+ * @param integer $limit Number of items to fetch.
+ * @param integer $offset Offset to start fetching at.
+ */
+ public function limit($limit, $offset = null)
+ {
+ $this->limit = $limit;
+ $this->limitOffset = $offset;
+ return $this;
+ }
+
+ /**
+ * Accessor for any fields that we want some logic around.
+ *
+ * @param string $key
+ */
+ public function __get($key)
+ {
+ switch ($key) {
+ case 'sortby':
+ if (!$this->_sortby && $this->mapper->defaultSort) {
+ // Add in any default sort values, if none are already
+ // set.
+ $this->sortBy($this->mapper->defaultSort);
+ }
+ return $this->_sortby;
+ }
+
+ throw new InvalidArgumentException('Undefined property ' . $key);
+ }
+
+ /**
+ * Callback for array_walk to prefix all elements of an array with
+ * a given prefix.
+ */
+ protected function _prefix(&$fieldName, $key, $prefix)
+ {
+ $fieldName = $prefix . $fieldName;
+ }
+
+ /**
+ * Take a query array and replace @field@ placeholders with values
+ * that will match in the load query.
+ *
+ * @param Horde_Rdo_Mapper $m1 Left-hand mapper
+ * @param Horde_Rdo_Mapper $m2 Right-hand mapper
+ * @param array $query The query to process placeholders on.
+ *
+ * @return array The query with placeholders filled in.
+ */
+ protected function _fillJoinPlaceholders($m1, $m2, $query)
+ {
+ $q = array();
+ foreach (array_keys($query) as $field) {
+ $value = $query[$field];
+ if (preg_match('/^@(.*)@$/', $value, $matches)) {
+ $q[$m1->model->table . '.' . $field] = new Horde_Rdo_Query_Literal($m2->model->table . '.' . $matches[1]);
+ } else {
+ $q[$m1->model->table . '.' . $field] = $value;
+ }
+ }
+
+ return $q;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Rdo
+ */
+
+/**
+ * Horde_Rdo query building abstract base
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+abstract class Horde_Rdo_Query_Builder {
+
+ /**
+ */
+ public function getCount($query)
+ {
+ return $this->getQuery($query);
+ }
+
+ /**
+ * Query generator.
+ *
+ * @param Horde_Rdo_Query $query The query object to turn into SQL.
+ *
+ * @return array A two-element array of the SQL query and an array
+ * of bind parameters.
+ */
+ public function getQuery($query)
+ {
+ if ($query instanceof Horde_Rdo_Query_Literal) {
+ return array((string)$query, array());
+ }
+
+ $bindParams = array();
+ $sql = '';
+
+ $this->_select($query, $sql, $bindParams);
+ $this->_from($query, $sql, $bindParams);
+ $this->_join($query, $sql, $bindParams);
+ $this->_where($query, $sql, $bindParams);
+ $this->_orderBy($query, $sql, $bindParams);
+ $this->_limit($query, $sql, $bindParams);
+
+ return array($sql, $bindParams);
+ }
+
+ /**
+ * Return the database-specific version of a test.
+ *
+ * @param string $test The test to "localize"
+ */
+ public function getTest($test)
+ {
+ return $test;
+ }
+
+ /**
+ */
+ protected function _select($query, &$sql, &$bindParams)
+ {
+ $fields = array();
+ foreach ($query->fields as $field) {
+ $parts = explode('.@', $field, 2);
+ if (count($parts) == 1) {
+ $fields[] = $field;
+ } else {
+ $fields[] = str_replace('.@', '.', $field) . ' AS ' . $this->quoteColumnName($parts[0] . '@' . $parts[1]);
+ }
+ }
+
+ $sql = 'SELECT ' . implode(', ', $fields);
+ }
+
+ /**
+ */
+ protected function _from($query, &$sql, &$bindParams)
+ {
+ $sql .= ' FROM ' . $query->mapper->model->table;
+ }
+
+ /**
+ */
+ protected function _join($query, &$sql, &$bindParams)
+ {
+ foreach ($query->relationships as $relationship) {
+ $relsql = array();
+ foreach ($relationship['query'] as $key => $value) {
+ if ($value instanceof Horde_Rdo_Query_Literal) {
+ $relsql[] = $key . ' = ' . (string)$value;
+ } else {
+ $relsql[] = $key . ' = ?';
+ $bindParams[] = $value;
+ }
+ }
+
+ $sql .= ' ' . $relationship['join_type'] . ' ' . $relationship['table'] . ' ON ' . implode(' AND ', $relsql);
+ }
+ }
+
+ /**
+ */
+ protected function _where($query, &$sql, &$bindParams)
+ {
+ $clauses = array();
+ foreach ($query->tests as $test) {
+ if (strpos($test['field'], '@') !== false) {
+ list($rel, $field) = explode('@', $test['field']);
+ if (!isset($query->relationships[$rel])) {
+ continue;
+ }
+ $clause = $query->relationships[$rel]['table'] . '.' . $field . ' ' . $this->getTest($test['test']);
+ } else {
+ $clause = $query->mapper->model->table . '.' . $this->quoteColumnName($test['field']) . ' ' . $this->getTest($test['test']);
+ }
+
+ if ($test['value'] instanceof Horde_Rdo_Query_Literal) {
+ $clauses[] = $clause . ' ' . (string)$test['value'];
+ } else {
+ if ($test['test'] == 'IN' && is_array($test['value'])) {
+ $clauses[] = $clause . '(?' . str_repeat(',?', count($test['value']) - 1) . ')';
+ $bindParams = array_merge($bindParams, array_values($test['value']));
+ } else {
+ $clauses[] = $clause . ' ?';
+ $bindParams[] = $test['value'];
+ }
+ }
+ }
+
+ if ($clauses) {
+ $sql .= ' WHERE ' . implode(' ' . $query->conjunction . ' ', $clauses);
+ }
+ }
+
+ /**
+ */
+ protected function _orderBy($query, &$sql, &$bindParams)
+ {
+ if ($query->sortby) {
+ $sql .= ' ORDER BY';
+ foreach ($query->sortby as $sort) {
+ if (strpos($sort, '@') !== false) {
+ /*@TODO parse these placeholders out, or drop them*/
+ list($field, $direction) = $sort;
+ list($rel, $field) = explode('@', $field);
+ if (!isset($query->relationships[$rel])) {
+ continue;
+ }
+ $sql .= ' ' . $query->relationships[$rel]['table'] . '.' . $field . ' ' . $direction . ',';
+ } else {
+ $sql .= " $sort,";
+ }
+ }
+
+ $sql = substr($sql, 0, -1);
+ }
+ }
+
+ /**
+ */
+ protected function _limit($query, &$sql, &$bindParams)
+ {
+ if ($query->limit) {
+ $sql .= ' LIMIT ' . $query->limit;
+ if (!is_null($query->limitOffset)) {
+ $sql .= ' OFFSET ' . $query->limitOffset;
+ }
+ }
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Rdo
+ */
+
+/**
+ * Horde_Rdo literal query string object.
+ *
+ * If you need to pass a string that should not be quoted into a
+ * Horde_Rdo_Query object, wrap it in a Horde_Rdo_Query_Literal object
+ * and it will not be quoted or escaped. Note that of course you need
+ * to be very careful about introducing user input or any other
+ * untrusted input into these objects.
+ *
+ * Example:
+ * $literal = new Horde_Rdo_Query_Literal('MAX(column_name)');
+ *
+ * @category Horde
+ * @package Horde_Rdo
+ */
+class Horde_Rdo_Query_Literal {
+
+ /**
+ * SQL literal string.
+ *
+ * @var string
+ */
+ protected $_string;
+
+ /**
+ * @var Horde_Rdo_Mapper
+ */
+ public $mapper;
+
+ /**
+ * Instantiate a literal, which is just a string stored as
+ * an instance member variable.
+ *
+ * @param string $string The string containing an SQL literal.
+ * @param Horde_Rdo_Mapper $mapper The Mapper object governing this query.
+ */
+ public function __construct($string, $mapper = null)
+ {
+ $this->_string = (string)$string;
+ $this->setMapper($mapper);
+ }
+
+ /**
+ * @return string The SQL literal stored in this object.
+ */
+ public function __toString()
+ {
+ return $this->_string;
+ }
+
+ /**
+ * @param Horde_Rdo_Mapper $mapper Rdo mapper base class
+ *
+ * @return Horde_Rdo_Query Return the query object for fluent chaining.
+ */
+ public function setMapper($mapper)
+ {
+ if ($mapper === $this->mapper) {
+ return $this;
+ }
+
+ $this->mapper = $mapper;
+ }
+
+}