* $result = dibi::query('SELECT * FROM [table]'); * * $row = $result->fetch(); * $value = $result->fetchSingle(); * $table = $result->fetchAll(); * $pairs = $result->fetchPairs(); * $assoc = $result->fetchAssoc('id'); * $assoc = $result->fetchAssoc('active,#,id'); * * unset($result); * * * @author David Grudl * @package dibi * * @property-read mixed $resource * @property-read IDibiResultDriver $driver * @property-read int $rowCount * @property-read DibiResultIterator $iterator * @property string $rowClass * @property-read DibiResultInfo $info */ class DibiResult extends DibiObject implements IDataSource { /** @var array IDibiResultDriver */ private $driver; /** @var array Translate table */ private $types = array(); /** @var DibiResultInfo */ private $meta; /** @var bool Already fetched? Used for allowance for first seek(0) */ private $fetched = FALSE; /** @var string returned object class */ private $rowClass = 'DibiRow'; /** @var array format */ private $formats = array(); /** * @param IDibiResultDriver */ public function __construct($driver) { $this->driver = $driver; $this->detectTypes(); } /** * @deprecated */ final public function getResource() { return $this->getResultDriver()->getResultResource(); } /** * Frees the resources allocated for this result set. * @return void */ final public function free() { if ($this->driver !== NULL) { $this->driver->free(); $this->driver = $this->meta = NULL; } } /** * Safe access to property $driver. * @return IDibiResultDriver * @throws RuntimeException */ final public function getResultDriver() { if ($this->driver === NULL) { throw new RuntimeException('Result-set was released from memory.'); } return $this->driver; } /********************* rows ****************d*g**/ /** * Moves cursor position without fetching row. * @param int the 0-based cursor pos to seek to * @return boolean TRUE on success, FALSE if unable to seek to specified record * @throws DibiException */ final public function seek($row) { return ($row !== 0 || $this->fetched) ? (bool) $this->getResultDriver()->seek($row) : TRUE; } /** * Required by the Countable interface. * @return int */ final public function count() { return $this->getResultDriver()->getRowCount(); } /** * Returns the number of rows in a result set. * @return int */ final public function getRowCount() { return $this->getResultDriver()->getRowCount(); } /** * Returns the number of rows in a result set. Alias for getRowCount(). * @deprecated */ final public function rowCount() { trigger_error(__METHOD__ . '() is deprecated; use count($res) or $res->getRowCount() instead.', E_USER_WARNING); return $this->getResultDriver()->getRowCount(); } /** * Required by the IteratorAggregate interface. * @return DibiResultIterator */ final public function getIterator() { if (func_num_args()) { trigger_error(__METHOD__ . ' arguments $offset & $limit have been dropped; use SQL clauses instead.', E_USER_WARNING); } return new DibiResultIterator($this); } /********************* fetching rows ****************d*g**/ /** * Set fetched object class. This class should extend the DibiRow class. * @param string * @return DibiResult provides a fluent interface */ public function setRowClass($class) { $this->rowClass = $class; return $this; } /** * Returns fetched object class name. * @return string */ public function getRowClass() { return $this->rowClass; } /** * Fetches the row at current position, process optional type conversion. * and moves the internal cursor to the next position * @return DibiRow|FALSE array on success, FALSE if no next record */ final public function fetch() { $row = $this->getResultDriver()->fetch(TRUE); if (!is_array($row)) { return FALSE; } $this->fetched = TRUE; $this->normalize($row); if ($this->rowClass) { $row = new $this->rowClass($row); } return $row; } /** * Like fetch(), but returns only first field. * @return mixed value on success, FALSE if no next record */ final public function fetchSingle() { $row = $this->getResultDriver()->fetch(TRUE); if (!is_array($row)) { return FALSE; } $this->fetched = TRUE; $this->normalize($row); return reset($row); } /** * Fetches all records from table. * @param int offset * @param int limit * @return array of DibiRow */ final public function fetchAll($offset = NULL, $limit = NULL) { $limit = $limit === NULL ? -1 : (int) $limit; $this->seek((int) $offset); $row = $this->fetch(); if (!$row) return array(); // empty result set $data = array(); do { if ($limit === 0) break; $limit--; $data[] = $row; } while ($row = $this->fetch()); return $data; } /** * Fetches all records from table and returns associative tree. * Examples: * - associative descriptor: col1[]col2->col3 * builds a tree: $tree[$val1][$index][$val2]->col3[$val3] = {record} * - associative descriptor: col1|col2->col3=col4 * builds a tree: $tree[$val1][$val2]->col3[$val3] = val4 * @param string associative descriptor * @return DibiRow * @throws InvalidArgumentException */ final public function fetchAssoc($assoc) { if (strpos($assoc, ',') !== FALSE) { return $this->oldFetchAssoc($assoc); } $this->seek(0); $row = $this->fetch(); if (!$row) return array(); // empty result set $data = NULL; $assoc = preg_split('#(\[\]|->|=|\|)#', $assoc, NULL, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); // check columns foreach ($assoc as $as) { // offsetExists ignores NULL in PHP 5.2.1, isset() surprisingly NULL accepts if ($as !== '[]' && $as !== '=' && $as !== '->' && $as !== '|' && !property_exists($row, $as)) { throw new InvalidArgumentException("Unknown column '$as' in associative descriptor."); } } if ($as === '->') { // must not be last array_pop($assoc); } if (empty($assoc)) { $assoc[] = '[]'; } // make associative tree do { $x = & $data; // iterative deepening foreach ($assoc as $i => $as) { if ($as === '[]') { // indexed-array node $x = & $x[]; } elseif ($as === '=') { // "value" node $x = $row->{$assoc[$i+1]}; continue 2; } elseif ($as === '->') { // "object" node if ($x === NULL) { $x = clone $row; $x = & $x->{$assoc[$i+1]}; $x = NULL; // prepare child node } else { $x = & $x->{$assoc[$i+1]}; } } elseif ($as !== '|') { // associative-array node $x = & $x[$row->$as]; } } if ($x === NULL) { // build leaf $x = $row; } } while ($row = $this->fetch()); unset($x); return $data; } /** * @deprecated */ private function oldFetchAssoc($assoc) { $this->seek(0); $row = $this->fetch(); if (!$row) return array(); // empty result set $data = NULL; $assoc = explode(',', $assoc); // strip leading = and @ $leaf = '@'; // gap $last = count($assoc) - 1; while ($assoc[$last] === '=' || $assoc[$last] === '@') { $leaf = $assoc[$last]; unset($assoc[$last]); $last--; if ($last < 0) { $assoc[] = '#'; break; } } do { $x = & $data; foreach ($assoc as $i => $as) { if ($as === '#') { // indexed-array node $x = & $x[]; } elseif ($as === '=') { // "record" node if ($x === NULL) { $x = $row->toArray(); $x = & $x[ $assoc[$i+1] ]; $x = NULL; // prepare child node } else { $x = & $x[ $assoc[$i+1] ]; } } elseif ($as === '@') { // "object" node if ($x === NULL) { $x = clone $row; $x = & $x->{$assoc[$i+1]}; $x = NULL; // prepare child node } else { $x = & $x->{$assoc[$i+1]}; } } else { // associative-array node $x = & $x[$row->$as]; } } if ($x === NULL) { // build leaf if ($leaf === '=') { $x = $row->toArray(); } else { $x = $row; } } } while ($row = $this->fetch()); unset($x); return $data; } /** * Fetches all records from table like $key => $value pairs. * @param string associative key * @param string value * @return array * @throws InvalidArgumentException */ final public function fetchPairs($key = NULL, $value = NULL) { $this->seek(0); $row = $this->fetch(); if (!$row) return array(); // empty result set $data = array(); if ($value === NULL) { if ($key !== NULL) { throw new InvalidArgumentException("Either none or both columns must be specified."); } // autodetect $tmp = array_keys($row->toArray()); $key = $tmp[0]; if (count($row) < 2) { // indexed-array do { $data[] = $row[$key]; } while ($row = $this->fetch()); return $data; } $value = $tmp[1]; } else { if (!property_exists($row, $value)) { throw new InvalidArgumentException("Unknown value column '$value'."); } if ($key === NULL) { // indexed-array do { $data[] = $row[$value]; } while ($row = $this->fetch()); return $data; } if (!property_exists($row, $key)) { throw new InvalidArgumentException("Unknown key column '$key'."); } } do { $data[ $row[$key] ] = $row[$value]; } while ($row = $this->fetch()); return $data; } /********************* column types ****************d*g**/ /** * Autodetect column types. * @return void */ private function detectTypes() { $cache = DibiColumnInfo::getTypeCache(); try { foreach ($this->getResultDriver()->getResultColumns() as $col) { $this->types[$col['name']] = $cache->{$col['nativetype']}; } } catch (DibiNotSupportedException $e) {} } /** * Converts values to specified type and format. * @param array * @return void */ private function normalize(array & $row) { foreach ($this->types as $key => $type) { if (!isset($row[$key])) { // NULL continue; } $value = $row[$key]; if ($value === FALSE || $type === dibi::TEXT) { } elseif ($type === dibi::INTEGER) { $row[$key] = is_float($tmp = $value * 1) ? $value : $tmp; } elseif ($type === dibi::FLOAT) { $row[$key] = (string) ($tmp = (float) $value) === $value ? $tmp : $value; } elseif ($type === dibi::BOOL) { $row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F'; } elseif ($type === dibi::DATE || $type === dibi::DATETIME) { if ((int) $value === 0) { // '', NULL, FALSE, '0000-00-00', ... } elseif (empty($this->formats[$type])) { // return DateTime object (default) $row[$key] = new DibiDateTime(is_numeric($value) ? date('Y-m-d H:i:s', $value) : $value); } elseif ($this->formats[$type] === 'U') { // return timestamp $row[$key] = is_numeric($value) ? (int) $value : strtotime($value); } elseif (is_numeric($value)) { // formatted date $row[$key] = date($this->formats[$type], $value); } else { $value = new DibiDateTime($value); $row[$key] = $value->format($this->formats[$type]); } } elseif ($type === dibi::BINARY) { $row[$key] = $this->getResultDriver()->unescape($value, $type); } } } /** * Define column type. * @param string column * @param string type (use constant Dibi::*) * @return DibiResult provides a fluent interface */ final public function setType($col, $type) { $this->types[$col] = $type; return $this; } /** * Returns column type. * @return string */ final public function getType($col) { return isset($this->types[$col]) ? $this->types[$col] : NULL; } /** * Sets data format. * @param string type (use constant Dibi::*) * @param string format * @return DibiResult provides a fluent interface */ final public function setFormat($type, $format) { $this->formats[$type] = $format; return $this; } /** * Returns data format. * @return string */ final public function getFormat($type) { return isset($this->formats[$type]) ? $this->formats[$type] : NULL; } /********************* meta info ****************d*g**/ /** * Returns a meta information about the current result set. * @return DibiResultInfo */ public function getInfo() { if ($this->meta === NULL) { $this->meta = new DibiResultInfo($this->getResultDriver()); } return $this->meta; } /** * @deprecated */ final public function getColumns() { return $this->getInfo()->getColumns(); } /** @deprecated */ public function getColumnNames($fullNames = FALSE) { trigger_error(__METHOD__ . '() is deprecated; use $res->getInfo()->getColumnNames() instead.', E_USER_WARNING); return $this->getInfo()->getColumnNames($fullNames); } /********************* misc tools ****************d*g**/ /** * Displays complete result set as HTML table for debug purposes. * @return void */ final public function dump() { $i = 0; $this->seek(0); while ($row = $this->fetch()) { if ($i === 0) { echo "\n\n\n\t\n\t\t\n"; foreach ($row as $col => $foo) { echo "\t\t\n"; } echo "\t\n\n\n"; } echo "\t\n\t\t\n"; foreach ($row as $col) { //if (is_object($col)) $col = $col->__toString(); echo "\t\t\n"; } echo "\t\n"; $i++; } if ($i === 0) { echo '

empty result set

'; } else { echo "\n
#row" . htmlSpecialChars($col) . "
", $i, "", htmlSpecialChars($col), "
\n"; } } }