I've made a SPL-based class named Recordset that wraps both MySQLi_STMT and MySQLi_Result objects and allows treating either as a 3-dimensional array.  It requires PHP5.3+.

Here's excerpts of the important parts...

	class Recordset implements Iterator, ArrayAccess, Countable {

		private $MySQLi_set;
		private $MySQLi_set_type;
		private $field_metadata;
		private $num_rows;
		private $bind_row;
		private $pointer;

		function __construct($sql) 
		{
			$bind_params = func_get_args();
			array_shift($bind_params);
			if (count($bind_params) === 0)
				// add code here to build MYSQLI_Result obj and store it in $this->MySQLi_set
			else
				// add code here to build MYSQLI_STMT obj and store it in $this->MySQLi_set
			if (!($this->MySQLi_set instanceof MySQLi_STMT OR $this->MySQLi_set instanceof MySQLi_Result))
				throw new RuntimeException('No MySQLi_STMT or MySQLi_Result object to work with.');
			// cache name of object to avoid repeat instanceof calls
			$this->MySQLi_set_type = get_class($this->MySQLi_set);
			if ($this->MySQLi_set_type === 'MySQLi_Result')
				// Set up special handling for MySQLi_Result objects
				// set $this->field_metadata to array of field info objects
				$this->field_metadata = $this->MySQLi_set->fetch_fields();
			else {
				// else this is a MYSQLI_STMT object
				// add code here to set $this->field_metadata to array of field info objects from MYSQLI_STMT
				// store results to allow object to act as a countable, seekable array
				if (!$this->MySQLi_set->store_result())
					throw new RuntimeException('MySQLi_STMT Result failed to store.  Error: ' . $this->MySQLi_set->error);
				// because bind_result() cannot bind to an object or array, use the 'call_user_func_array()' technique instead
				$this->bind_row = array();
				$bind_row2 = array();
				foreach($this->field_metadata as $field)
					 $bind_row2[$field->name] =& $this->bind_row[$field->name];
				if (!call_user_func_array(array($this->MySQLi_set, 'bind_result'), $this->bind_row))
					throw new RuntimeException($this->MySQLi_set->error);
				if ($this->MySQLi_set->error)
					throw new RuntimeException($this->MySQLi_set->error);
				}
			// cache row count to avoid constant calls to MySQLi_x->num_rows
			$this->num_rows = $this->MySQLi_set->num_rows;
			// if rows were returned, set internal pointer to 0
			if ($this->num_rows > 0) $this->pointer = 0;
		}

		function fetchRow($offset = null) 
		{
			// sets $this->bind_row to an associative array of the next record for MySQLi_STMT wrapped objects
			$this->MySQLi_set->data_seek((($offset !== null) ? (int) $offset : $this->pointer));
			if ($this->MySQLi_set_type === 'MySQLi_Result')
				return $this->MySQLi_set->fetch_assoc();
			// else this is a MySQLi_STMT object
			$fetch_status = $this->MySQLi_set->fetch();
			if ($fetch_status === true) return $this->bind_row;
			else if ($fetch_status === null) return null; // no more rows
			// MySQLi_STMT row fetch failed
			throw new RuntimeException('Error fetching row: . ' . $this->MySQLi_set->error);
		}

			
		function count() {return $this->num_rows;}
			
		function current() {return $this->fetchRow();}

		function key() {return $this->pointer;}
		
		function next() {$this->pointer++;}
		
		function rewind() {$this->pointer = 0;}

		function valid() {return $this->offsetExists($this->pointer);}
			
		function offsetExists($offset) {return ((int) $offset < $this->num_rows AND (int) $offset >= 0);}
			
		function offsetGet($offset)
		{
			if (!$this->offsetExists($offset))
				throw new OutOfBoundsException('Out of range of result set.');
			return $this->fetchRow($offset);
		}


The code is roughly 200 lines long, so instead of making a giant post, here's a pastebin link...  http://pastebin.com/LUe4j9DM

Objects are created by passing in an sql query string.  For handling prepared statements, simply pass in additional arguments as the prepared binding parameters.  In those cases, there is a behind-the-scenes `bind_param` going on generating the MySQLi_STMT object to be wrapped.  That part isn't included here, so for the purposes of this post, pretend it's magic.

For example to automatically handle the query as a prepared statement:

    $recordset = new Recordset("SELECT * FROM tblComments WHERE(parentBlogID = ?)", 2);
    if (count($recordset) > 0) {
        foreach ($recordset as $record)
            echo $record['commentText'] . '<br>';
        }
    else echo "No records returned.";


<b>Advantages...</b>
<ul>
<li>Allows accessing result rows by array key (<code>echo $Recordset[295]['title'];</code>) or by foreach loops (<code>foreach ($Recordsetas $rec) {echo $rec['title'];}</code>).</li>
<li>Allows you to get a row by index number without interfering with the internal result pointer.  In other words, right in the middle of a foreach loop, you can do <code>$x = $Recordset[259]['ID'];</code> without causing the internal result row pointer to jump out of order.</li>
<li>Allows exchanging this class out for regular 3-dimensional arrays of result dumps.</li>
<li>Avoids duplication of result binding code for every statement throughout an application.</li>
<li>Lets you still access functions and properties of the wrapped MySQLi object directly (<code>$Recordset->attr_get(1)</code>).</li>
</ul>

<b>Tests...</b>

Here's some speed tests iterating over 10000 records from a localhost table with 6 columns.  This test was done with prepared statement results either wrapped in Recordset or not.  Timing is measured starting before and ending after each entire loop call..

<b>With Recordset...</b>
<ul>
<li><code>foreach($Recordset as $rec) {$x = $rec;}</code> ... <br>
0.105 s</li>
<li><code>for ($i = 0, $count = count($Recordset); $i &lt; $count; $i++) {$x = $Recordset[$i];}</code> ... <br>
0.056 s</li>
<li><code>for ($i = 0, $count = count($Recordset); $i &lt; $count; $i++) {$x = $Recordset-&gt;fetchRow($i);}</code> ... <br>
0.0351 s</li>
<li><code>while ($x = $Recordset[$i]) {$i++; if (!isset($Recordset[$i])) break;}</code> ... <br>
0.0647 s</li>
</ul>

<b>Without Recordset, but still using the "bind to array" technique and pre-storing results...</b>
<ul>
<li><code>for ($i=0, $count=$MySQLI_STMT-&gt;num_rows; $i &lt; $count; $i++)) {$MySQLI_STMT-&gt;fetch(); $x = $bind_row;}</code> ...<br>
0.0409 s</li>
<li><code>while ($MySQLI_STMT-&gt;fetch()) {$x = $bind_row;}</code> ... <br>
0.009 s</li>
</ul>

I'm pretty bummed about the slow foreach loop over my Recordset object, by the slower seek and fetch speeds within Recordset, and by the fact that nothing can even compare to `while ($MySQLI_STMT->fetch()) {}`. I'd like for this to work better.  Any suggestions or insight how to speed things up?