$config); } foreach ($config as $key => $value) { switch ($key) { case self::ADAPTER: $this->_setAdapter($value); break; case self::SCHEMA: $this->_schema = (string) $value; break; case self::NAME: $this->_name = (string) $value; break; case self::PRIMARY: $this->_primary = (array) $value; break; case self::ROW_CLASS: $this->setRowClass($value); break; case self::ROWSET_CLASS: $this->setRowsetClass($value); break; case self::REFERENCE_MAP: $this->setReferences($value); break; case self::DEPENDENT_TABLES: $this->setDependentTables($value); break; case self::METADATA_CACHE: $this->_setMetadataCache($value); break; case self::SEQUENCE: $this->_setSequence($value); break; default: // ignore unrecognized configuration directive break; } } $this->_setup(); } /** * @param string $classname * @return Zend_Db_Table_Abstract Provides a fluent interface */ public function setRowClass($classname) { $this->_rowClass = (string) $classname; return $this; } /** * @return string */ public function getRowClass() { return $this->_rowClass; } /** * @param string $classname * @return Zend_Db_Table_Abstract Provides a fluent interface */ public function setRowsetClass($classname) { $this->_rowsetClass = (string) $classname; return $this; } /** * @return string */ public function getRowsetClass() { return $this->_rowsetClass; } /** * @param array $referenceMap * @return Zend_Db_Table_Abstract Provides a fluent interface */ public function setReferences(array $referenceMap) { $this->_referenceMap = $referenceMap; return $this; } /** * @param string $tableClassname * @param string $ruleKey OPTIONAL * @return array * @throws Zend_Db_Table_Exception */ public function getReference($tableClassname, $ruleKey = null) { $thisClass = get_class($this); $refMap = $this->_getReferenceMapNormalized(); if ($ruleKey !== null) { if (!isset($refMap[$ruleKey])) { require_once "Zend/Db/Table/Exception.php"; throw new Zend_Db_Table_Exception("No reference rule \"$ruleKey\" from table $thisClass to table $tableClassname"); } if ($refMap[$ruleKey][self::REF_TABLE_CLASS] != $tableClassname) { require_once "Zend/Db/Table/Exception.php"; throw new Zend_Db_Table_Exception("Reference rule \"$ruleKey\" does not reference table $tableClassname"); } return $refMap[$ruleKey]; } foreach ($refMap as $reference) { if ($reference[self::REF_TABLE_CLASS] == $tableClassname) { return $reference; } } require_once "Zend/Db/Table/Exception.php"; throw new Zend_Db_Table_Exception("No reference from table $thisClass to table $tableClassname"); } /** * @param array $dependentTables * @return Zend_Db_Table_Abstract Provides a fluent interface */ public function setDependentTables(array $dependentTables) { $this->_dependentTables = $dependentTables; return $this; } /** * @return array */ public function getDependentTables() { return $this->_dependentTables; } /** * Sets the default Zend_Db_Adapter_Abstract for all Zend_Db_Table objects. * * @param mixed $db Either an Adapter object, or a string naming a Registry key * @return void */ public static final function setDefaultAdapter($db = null) { Zend_Db_Table_Abstract::$_defaultDb = self::_setupAdapter($db); } /** * Gets the default Zend_Db_Adapter_Abstract for all Zend_Db_Table objects. * * @return Zend_Db_Adapter_Abstract or null */ public static final function getDefaultAdapter() { return self::$_defaultDb; } /** * @param mixed $db Either an Adapter object, or a string naming a Registry key * @return Zend_Db_Table_Abstract Provides a fluent interface */ protected final function _setAdapter($db) { $this->_db = self::_setupAdapter($db); return $this; } /** * Gets the Zend_Db_Adapter_Abstract for this particular Zend_Db_Table object. * * @return Zend_Db_Adapter_Abstract */ public final function getAdapter() { return $this->_db; } /** * @param mixed $db Either an Adapter object, or a string naming a Registry key * @return Zend_Db_Adapter_Abstract * @throws Zend_Db_Table_Exception */ protected static final function _setupAdapter($db) { if ($db === null) { return null; } if (is_string($db)) { require_once 'Zend/Registry.php'; $db = Zend_Registry::get($db); } if (!$db instanceof Zend_Db_Adapter_Abstract) { require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception('Argument must be of type Zend_Db_Adapter_Abstract, or a Registry key where a Zend_Db_Adapter_Abstract object is stored'); } return $db; } /** * Sets the default metadata cache for information returned by Zend_Db_Adapter_Abstract::describeTable(). * * If $defaultMetadataCache is null, then no metadata cache is used by default. * * @param mixed $metadataCache Either a Cache object, or a string naming a Registry key * @return void */ public static function setDefaultMetadataCache($metadataCache = null) { self::$_defaultMetadataCache = self::_setupMetadataCache($metadataCache); } /** * Gets the default metadata cache for information returned by Zend_Db_Adapter_Abstract::describeTable(). * * @return Zend_Cache_Core or null */ public static function getDefaultMetadataCache() { return self::$_defaultMetadataCache; } /** * Sets the metadata cache for information returned by Zend_Db_Adapter_Abstract::describeTable(). * * If $metadataCache is null, then no metadata cache is used. Since there is no opportunity to reload metadata * after instantiation, this method need not be public, particularly because that it would have no effect * results in unnecessary API complexity. To configure the metadata cache, use the metadataCache configuration * option for the class constructor upon instantiation. * * @param mixed $metadataCache Either a Cache object, or a string naming a Registry key * @return Zend_Db_Table_Abstract Provides a fluent interface */ protected function _setMetadataCache($metadataCache) { $this->_metadataCache = self::_setupMetadataCache($metadataCache); return $this; } /** * Gets the metadata cache for information returned by Zend_Db_Adapter_Abstract::describeTable(). * * @return Zend_Cache_Core or null */ public function getMetadataCache() { return $this->_metadataCache; } /** * @param mixed $metadataCache Either a Cache object, or a string naming a Registry key * @return Zend_Cache_Core * @throws Zend_Db_Table_Exception */ protected static final function _setupMetadataCache($metadataCache) { if ($metadataCache === null) { return null; } if (is_string($metadataCache)) { require_once 'Zend/Registry.php'; $metadataCache = Zend_Registry::get($metadataCache); } if (!$metadataCache instanceof Zend_Cache_Core) { require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception('Argument must be of type Zend_Cache_Core, or a Registry key where a Zend_Cache_Core object is stored'); } return $metadataCache; } /** * Sets the sequence member, which defines the behavior for generating * primary key values in new rows. * - If this is a string, then the string names the sequence object. * - If this is boolean true, then the key uses an auto-incrementing * or identity mechanism. * - If this is boolean false, then the key is user-defined. * Use this for natural keys, for example. * * @param mixed $sequence * @return Zend_Db_Table_Adapter_Abstract Provides a fluent interface */ protected function _setSequence($sequence) { $this->_sequence = $sequence; return $this; } /** * Turnkey for initialization of a table object. * Calls other protected methods for individual tasks, to make it easier * for a subclass to override part of the setup logic. * * @return void */ protected function _setup() { $this->_setupDatabaseAdapter(); $this->_setupTableName(); $this->_setupMetadata(); $this->_setupPrimaryKey(); } /** * Initialize database adapter. * * @return void */ protected function _setupDatabaseAdapter() { if (! $this->_db) { $this->_db = self::getDefaultAdapter(); } } /** * Initialize table and schema names. * * If the table name is not set in the class definition, * use the class name itself as the table name. * * A schema name provided with the table name (e.g., "schema.table") overrides * any existing value for $this->_schema. * * @return void */ protected function _setupTableName() { if (! $this->_name) { $this->_name = get_class($this); } else if (strpos($this->_name, '.')) { list($this->_schema, $this->_name) = explode('.', $this->_name); } } /** * Initializes metadata. * * If metadata cannot be loaded from cache, adapter's describeTable() method is called to discover metadata * information. Returns true if and only if the metadata are loaded from cache. * * @return boolean * @throws Zend_Db_Table_Exception */ protected function _setupMetadata() { // Assume that metadata will be loaded from cache $isMetadataFromCache = true; // If $this has no metadata cache but the class has a default metadata cache if (null === $this->_metadataCache && null !== self::$_defaultMetadataCache) { // Make $this use the default metadata cache of the class $this->_setMetadataCache(self::$_defaultMetadataCache); } // If $this has a metadata cache if (null !== $this->_metadataCache) { // Define the cache identifier where the metadata are saved $cacheId = md5("$this->_schema.$this->_name"); } // If $this has no metadata cache or metadata cache misses if (null === $this->_metadataCache || !($metadata = $this->_metadataCache->load($cacheId))) { // Metadata are not loaded from cache $isMetadataFromCache = false; // Fetch metadata from the adapter's describeTable() method $metadata = $this->_db->describeTable($this->_name, $this->_schema); // If $this has a metadata cache, then cache the metadata if (null !== $this->_metadataCache && !$this->_metadataCache->save($metadata, $cacheId)) { /** * @see Zend_Db_Table_Exception */ require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception('Failed saving metadata to metadataCache'); } } // Assign the metadata to $this $this->_metadata = $metadata; // Update the columns $this->_cols = array_keys($this->_metadata); // Return whether the metadata were loaded from cache return $isMetadataFromCache; } /** * Initialize primary key from metadata. * If $_primary is not defined, discover primary keys * from the information returned by describeTable(). * * @return void * @throws Zend_Db_Table_Exception */ protected function _setupPrimaryKey() { if (!$this->_primary) { $this->_primary = array(); foreach ($this->_metadata as $col) { if ($col['PRIMARY']) { $this->_primary[ $col['PRIMARY_POSITION'] ] = $col['COLUMN_NAME']; if ($col['IDENTITY']) { $this->_identity = $col['PRIMARY_POSITION']; } } } // if no primary key was specified and none was found in the metadata // then throw an exception. if (empty($this->_primary)) { require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception('A table must have a primary key, but none was found'); } } else if (!is_array($this->_primary)) { $this->_primary = array(1 => $this->_primary); } else if (isset($this->_primary[0])) { array_unshift($this->_primary, null); unset($this->_primary[0]); } if (! array_intersect((array) $this->_primary, $this->_cols) == (array) $this->_primary) { require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception("Primary key column(s) (" . implode(',', (array) $this->_primary) . ") are not columns in this table (" . implode(',', $this->_cols) . ")"); } $primary = (array) $this->_primary; $pkIdentity = $primary[(int) $this->_identity]; /** * Special case for PostgreSQL: a SERIAL key implicitly uses a sequence * object whose name is "__seq". */ if ($this->_sequence === true && $this->_db instanceof Zend_Db_Adapter_Pdo_Pgsql) { $this->_sequence = "{$this->_name}_{$pkIdentity}_seq"; if ($this->_schema) { $this->_sequence = $this->_schema . '.' . $this->_sequence; } } } /** * Returns a normalized version of the reference map * * @return array */ protected function _getReferenceMapNormalized() { $referenceMapNormalized = array(); foreach ($this->_referenceMap as $rule => $map) { $referenceMapNormalized[$rule] = array(); foreach ($map as $key => $value) { switch ($key) { // normalize COLUMNS and REF_COLUMNS to arrays case self::COLUMNS: case self::REF_COLUMNS: if (!is_array($value)) { $referenceMapNormalized[$rule][$key] = array($value); } else { $referenceMapNormalized[$rule][$key] = $value; } break; // other values are copied as-is default: $referenceMapNormalized[$rule][$key] = $value; break; } } } return $referenceMapNormalized; } /** * Returns table information. * * @return array */ public function info() { return array( self::SCHEMA => $this->_schema, self::NAME => $this->_name, self::COLS => (array) $this->_cols, self::PRIMARY => (array) $this->_primary, self::METADATA => $this->_metadata, self::ROW_CLASS => $this->_rowClass, self::ROWSET_CLASS => $this->_rowsetClass, self::REFERENCE_MAP => $this->_referenceMap, self::DEPENDENT_TABLES => $this->_dependentTables, self::SEQUENCE => $this->_sequence ); } /** * Inserts a new row. * * @param array $data Column-value pairs. * @return mixed The primary key of the row inserted. */ public function insert(array $data) { /** * Zend_Db_Table assumes that if you have a compound primary key * and one of the columns in the key uses a sequence, * it's the _first_ column in the compound key. */ $primary = (array) $this->_primary; $pkIdentity = $primary[(int)$this->_identity]; /** * If this table uses a database sequence object and the data does not * specify a value, then get the next ID from the sequence and add it * to the row. We assume that only the first column in a compound * primary key takes a value from a sequence. */ if (is_string($this->_sequence) && !isset($data[$pkIdentity])) { $data[$pkIdentity] = $this->_db->nextSequenceId($this->_sequence); } /** * If the primary key can be generated automatically, and no value was * specified in the user-supplied data, then omit it from the tuple. */ if (array_key_exists($pkIdentity, $data) && $data[$pkIdentity] === null) { unset($data[$pkIdentity]); } /** * INSERT the new row. */ $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name; $this->_db->insert($tableSpec, $data); /** * Fetch the most recent ID generated by an auto-increment * or IDENTITY column, unless the user has specified a value, * overriding the auto-increment mechanism. */ if ($this->_sequence === true && !isset($data[$pkIdentity])) { $data[$pkIdentity] = $this->_db->lastInsertId(); } /** * Return the primary key value if the PK is a single column, * else return an associative array of the PK column/value pairs. */ $pkData = array_intersect_key($data, array_flip($primary)); if (count($primary) == 1) { reset($pkData); return current($pkData); } else { return $pkData; } /** * The last case: the user did not specify a value for the primary * key, nor is this table class declared to use an auto-increment key. * Since the insert did not fail, we can assume this is one of the edge * cases, which may include: * - the table has no primary key defined; * - the database table uses a trigger to set a primary key value; * - the RDBMS permits primary keys to be NULL or have a value set * to the column's DEFAULT */ return null; } /** * Updates existing rows. * * @param array $data Column-value pairs. * @param array|string $where An SQL WHERE clause, or an array of SQL WHERE clauses. * @return int The number of rows updated. */ public function update(array $data, $where) { $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name; return $this->_db->update($tableSpec, $data, $where); } /** * Called by a row object for the parent table's class during save() method. * * @param string $parentTableClassname * @param array $oldPrimaryKey * @param array $newPrimaryKey * @return int */ public function _cascadeUpdate($parentTableClassname, array $oldPrimaryKey, array $newPrimaryKey) { $rowsAffected = 0; foreach ($this->_getReferenceMapNormalized() as $rule => $map) { if ($map[self::REF_TABLE_CLASS] == $parentTableClassname && isset($map[self::ON_UPDATE])) { switch ($map[self::ON_UPDATE]) { case self::CASCADE: $newRefs = array(); for ($i = 0; $i < count($map[self::COLUMNS]); ++$i) { $col = $this->_db->foldCase($map[self::COLUMNS][$i]); $refCol = $this->_db->foldCase($map[self::REF_COLUMNS][$i]); if (array_key_exists($refCol, $newPrimaryKey)) { $newRefs[$col] = $newPrimaryKey[$refCol]; } $type = $this->_metadata[$col]['DATA_TYPE']; $where[] = $this->_db->quoteInto( $this->_db->quoteIdentifier($col, true) . ' = ?', $oldPrimaryKey[$refCol], $type); } $rowsAffected += $this->update($newRefs, $where); break; default: // no action break; } } } return $rowsAffected; } /** * Deletes existing rows. * * @param array|string $where SQL WHERE clause(s). * @return int The number of rows deleted. */ public function delete($where) { $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name; return $this->_db->delete($tableSpec, $where); } /** * Called by parent table's class during delete() method. * * @param string $parentTableClassname * @param array $primaryKey * @return int Number of affected rows */ public function _cascadeDelete($parentTableClassname, array $primaryKey) { $rowsAffected = 0; foreach ($this->_getReferenceMapNormalized() as $rule => $map) { if ($map[self::REF_TABLE_CLASS] == $parentTableClassname && isset($map[self::ON_DELETE])) { switch ($map[self::ON_DELETE]) { case self::CASCADE: for ($i = 0; $i < count($map[self::COLUMNS]); ++$i) { $col = $this->_db->foldCase($map[self::COLUMNS][$i]); $refCol = $this->_db->foldCase($map[self::REF_COLUMNS][$i]); $type = $this->_metadata[$col]['DATA_TYPE']; $where[] = $this->_db->quoteInto( $this->_db->quoteIdentifier($col, true) . ' = ?', $primaryKey[$refCol], $type); } $rowsAffected += $this->delete($where); break; default: // no action break; } } } return $rowsAffected; } /** * Fetches rows by primary key. The argument specifies one or more primary * key value(s). To find multiple rows by primary key, the argument must * be an array. * * This method accepts a variable number of arguments. If the table has a * multi-column primary key, the number of arguments must be the same as * the number of columns in the primary key. To find multiple rows in a * table with a multi-column primary key, each argument must be an array * with the same number of elements. * * The find() method always returns a Rowset object, even if only one row * was found. * * @param mixed $key The value(s) of the primary keys. * @return Zend_Db_Table_Rowset_Abstract Row(s) matching the criteria. * @throws Zend_Db_Table_Exception */ public function find($key) { $args = func_get_args(); $keyNames = array_values((array) $this->_primary); if (count($args) < count($keyNames)) { require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception("Too few columns for the primary key"); } if (count($args) > count($keyNames)) { require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception("Too many columns for the primary key"); } $whereList = array(); $numberTerms = 0; foreach ($args as $keyPosition => $keyValues) { // Coerce the values to an array. // Don't simply typecast to array, because the values // might be Zend_Db_Expr objects. if (!is_array($keyValues)) { $keyValues = array($keyValues); } if ($numberTerms == 0) { $numberTerms = count($keyValues); } else if (count($keyValues) != $numberTerms) { require_once 'Zend/Db/Table/Exception.php'; throw new Zend_Db_Table_Exception("Missing value(s) for the primary key"); } for ($i = 0; $i < count($keyValues); ++$i) { $whereList[$i][$keyPosition] = $keyValues[$i]; } } $whereClause = null; if (count($whereList)) { $whereOrTerms = array(); foreach ($whereList as $keyValueSets) { $whereAndTerms = array(); foreach ($keyValueSets as $keyPosition => $keyValue) { $type = $this->_metadata[$keyNames[$keyPosition]]['DATA_TYPE']; $whereAndTerms[] = $this->_db->quoteInto( $this->_db->quoteIdentifier($keyNames[$keyPosition], true) . ' = ?', $keyValue, $type); } $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')'; } $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')'; } return $this->fetchAll($whereClause); } /** * Fetches all rows. * * Honors the Zend_Db_Adapter fetch mode. * * @param string|array $where OPTIONAL An SQL WHERE clause. * @param string|array $order OPTIONAL An SQL ORDER clause. * @param int $count OPTIONAL An SQL LIMIT count. * @param int $offset OPTIONAL An SQL LIMIT offset. * @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode. */ public function fetchAll($where = null, $order = null, $count = null, $offset = null) { $data = array( 'table' => $this, 'data' => $this->_fetch($where, $order, $count, $offset), 'rowClass' => $this->_rowClass, 'stored' => true ); Zend_Loader::loadClass($this->_rowsetClass); return new $this->_rowsetClass($data); } /** * Fetches one row in an object of type Zend_Db_Table_Row_Abstract, * or returns Boolean false if no row matches the specified criteria. * * @param string|array $where OPTIONAL An SQL WHERE clause. * @param string|array $order OPTIONAL An SQL ORDER clause. * @return Zend_Db_Table_Row_Abstract The row results per the * Zend_Db_Adapter fetch mode, or null if no row found. */ public function fetchRow($where = null, $order = null) { $keys = array_values((array) $this->_primary); $vals = array_fill(0, count($keys), null); $primary = array_combine($keys, $vals); $rows = $this->_fetch($where, $order, 1); if (count($rows) == 0) { return null; } $data = array( 'table' => $this, 'data' => $rows[0], 'stored' => true ); Zend_Loader::loadClass($this->_rowClass); return new $this->_rowClass($data); } /** * Fetches a new blank row (not from the database). * * @return Zend_Db_Table_Row_Abstract * @deprecated since 0.9.3 - use createRow() instead. */ public function fetchNew() { return $this->createRow(); } /** * Fetches a new blank row (not from the database). * * @param array $data OPTIONAL data to populate in the new row. * @return Zend_Db_Table_Row_Abstract */ public function createRow(array $data = array()) { $defaults = array_combine($this->_cols, array_fill(0, count($this->_cols), null)); $keys = array_flip($this->_cols); $data = array_intersect_key($data, $keys); $data = array_merge($defaults, $data); $config = array( 'table' => $this, 'data' => $data, 'stored' => false ); Zend_Loader::loadClass($this->_rowClass); return new $this->_rowClass($config); } /** * Support method for fetching rows. * * @param string|array $where OPTIONAL An SQL WHERE clause. * @param string|array $order OPTIONAL An SQL ORDER clause. * @param int $count OPTIONAL An SQL LIMIT count. * @param int $offset OPTIONAL An SQL LIMIT offset. * @return array The row results, in FETCH_ASSOC mode. */ protected function _fetch($where = null, $order = null, $count = null, $offset = null) { // selection tool $select = $this->_db->select(); // the FROM clause $select->from($this->_name, $this->_cols, $this->_schema); // the WHERE clause $where = (array) $where; foreach ($where as $key => $val) { // is $key an int? if (is_int($key)) { // $val is the full condition $select->where($val); } else { // $key is the condition with placeholder, // and $val is quoted into the condition $select->where($key, $val); } } // the ORDER clause if (!is_array($order)) { $order = array($order); } foreach ($order as $val) { $select->order($val); } // the LIMIT clause $select->limit($count, $offset); // return the results $stmt = $this->_db->query($select); $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC); return $data; } }