From: Chuck Hagenbuch Date: Sat, 13 Dec 2008 03:19:24 +0000 (-0500) Subject: import rdo to the hatchery while it's ported to Horde_Db X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=cb1da0720e0639c0a8ecda98f899df5db4acddc2;p=horde.git import rdo to the hatchery while it's ported to Horde_Db --- diff --git a/framework/Rdo/lib/Horde/Rdo.php b/framework/Rdo/lib/Horde/Rdo.php new file mode 100644 index 000000000..55052ad80 --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo.php @@ -0,0 +1,73 @@ + + * @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 + * @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; + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/Base.php b/framework/Rdo/lib/Horde/Rdo/Base.php new file mode 100644 index 000000000..305f4c3de --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/Base.php @@ -0,0 +1,292 @@ +_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 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; + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/Iterator.php b/framework/Rdo/lib/Horde/Rdo/Iterator.php new file mode 100644 index 000000000..27f4c8752 --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/Iterator.php @@ -0,0 +1,105 @@ +_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; + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/List.php b/framework/Rdo/lib/Horde/Rdo/List.php new file mode 100644 index 000000000..b8b56adfc --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/List.php @@ -0,0 +1,193 @@ +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; + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/Mapper.php b/framework/Rdo/lib/Horde/Rdo/Mapper.php new file mode 100644 index 000000000..e7cb7aafd --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/Mapper.php @@ -0,0 +1,534 @@ +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; + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/Model.php b/framework/Rdo/lib/Horde/Rdo/Model.php new file mode 100644 index 000000000..83d6a896b --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/Model.php @@ -0,0 +1,119 @@ +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; + } + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/Query.php b/framework/Rdo/lib/Horde/Rdo/Query.php new file mode 100644 index 000000000..5ed2353b9 --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/Query.php @@ -0,0 +1,313 @@ +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; + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/Query/Builder.php b/framework/Rdo/lib/Horde/Rdo/Query/Builder.php new file mode 100644 index 000000000..36eea1258 --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/Query/Builder.php @@ -0,0 +1,172 @@ +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; + } + } + } + +} diff --git a/framework/Rdo/lib/Horde/Rdo/Query/Literal.php b/framework/Rdo/lib/Horde/Rdo/Query/Literal.php new file mode 100644 index 000000000..1703e7742 --- /dev/null +++ b/framework/Rdo/lib/Horde/Rdo/Query/Literal.php @@ -0,0 +1,71 @@ +_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; + } + +}