4

the PHP mysqli_result class implements the Traversable interface for some time now. This gives some interesting possibilities like the following piece of code:

<?php

$db = new mysqli( '...' );

$result = $db->query( '...' );

foreach( $result as $row ) {
   //$row is an associative array containing the fetched values
}

Now, I like this pretty much, but in the library I am currently working on I would like to have objects fetched, not associative arrays. I also want to be able to pass a class to it, so I can create a simple model system with it

For that I extend it like this:

<?php

class mysqli_result_extension extends mysqli_result {

    protected $result_class;

    public function set_result_class( $class ) { $this->result_class = $class; }

    public function function_to_overwrite() {

        return $this->fetch_object( $this->result_class );
    }
}

My problem and my question to you:

How do I switch from fetch_assoc to fetch_object now? I would like to overwrite the method the Traversable interface uses in order to return the result, but it doesn't define any (http://php.net/manual/en/class.traversable.php)

So what would I need to overwrite in order to switch from fetch_assoc to fetch_object and to be able to pass my $result_class to it so I am able to do something like this:

<?php

$db = new mysqli_extension( '...' );

$posts = $db->query( 'select * from posts' );
//$result is object of type mysqli_result_extension

$posts->set_result_class( 'post' );

foreach( $posts as $post ) {
   //$post is an instance of class 'post'
}

I don't want something dirty, hacky like overwriting fetch_assoc and just returning the object in it

Thanks in advance for your help and input

Torben
  • 43
  • 3
  • 1
    Is it possible for you to switch to PDO? No idea how to do it in MySQLi, but it's easy to do exactly what you want with PDO. Just set the `PDO::ATTR_DEFAULT_FETCH_MODE` attribute, and it will do the appropriate kind of fetch when you do the foreach loop. – SDC Nov 16 '12 at 14:15
  • This would be a great solution, if I'd also able to specify the class of the result object. I searched through the documentations of PDO but couldn't find a way. Afaik extending the PDOStatement class leads to the same problems as extending mysqli_result, since it also implements Traversable – Torben Nov 16 '12 at 14:55
  • yes, you can. I'll post it as an answer with code. – SDC Nov 16 '12 at 15:00

3 Answers3

4

Since PHP 5.5 (Generator Syntax; Jun 2013) this became a bit easier then previously answered, here a Traversable decorating mysqli_result traversal as fetch_object() results (PHP 8.2 syntax):

readonly class ObjectFetchIterator implements IteratorAggregate
{
    public function __construct(
        private mysqli_result $result, 
        private string $class,
    ) {}
    
    public function getIterator() : Traversable {
        $result = $this->result;
        while ($_ = $result->fetch_object($this->class)) {
            yield $_;
        }
    }
}

Usage Example:

$db = new mysqli('...');

$result = $db->query('...');

class Post extends stdClass {};

foreach(new ObjectFetchIterator($result, 'Post') as $post ) {
   // $post is an object containing the fetched values
}

What follows is the original answer with more information.


The (let's call it) traversal-fetch-mode of the mysqli_result can not be influenced. So as it is by default the associative array (column-name as key and it's value as value) like mysqli_result::fetch_assoc() it stays with that.

So this requires an own Iterator that does the work. So how does a non-foreach iteration of a Mysqli result traditionally look like? Right, the while() loop:

while($row = $result->fetch_assoc())
{
   ...
}

Which reads: while the fetch-method does not return NULL go on. This is a common form to create iteration.

For iterations of this kind I normally use a FetchingIterator, it is a concrete iterator that turns the PHP specific Iterator interface (rewind, valid, current, key, next) into a more simplified one. That simplified one goes: next on rewind, next on next and valid until NULL - which is pretty much the same as the while loop and can be used for similar working APIs like deprecated mysql or even arrays (compare array_shift etc.). It's also similar to other implementations of iterators in different programming languages, here specifically Java (compare Iterator Design Pattern (Source Making Guide on Design Patterns) article).

I did outline it rudimentary back last year in my article Some PHP Iterator Fun (Feb 2012) and a much improved, standard implementation of this can be found as FetchingIterator in Iterator Garden which easily allows to do what you want to do.

Let's see an example application:

class ObjectFetchIterator extends FetchingIterator
{
    public function __construct(mysqli_result $result, $class) {

        parent::__construct(function() use ($result, $class) {
            return $result->fetch_object($class);
        });
    }
}

The mysqli_result::fetch_object() method which has been previously within the while(...) has now been moved into the constructor of a FetchingIterator for mysqli results. This is a pretty straight forward implementation of it then. Alternatively, it's also possible to override the protected fetchNext() method but I only note it because it's not needed here.

The usage example then is pretty straight forward:

$db       = new mysqli('...');    
$result   = $db->query('...');    
$iterator = new ObjectFetchIterator($result, 'myClass');

/* @var $row myClass */
foreach ($iterator as $row)
{
   // $row is a myClass now based on fetched values
}

As this example shows, there is no need to use PDO for that and it sounds like a pretty lazy excuse: Changing the whole database layer only for this little detail? No! The opposite is the case: Using an iterator here in the foreach allows you to de-couple your code from the concrete database access library to the generic type (as it was possible with arrays earlier too, however arrays are by far not that distinct as object types).

Hope this helps even the answer comes a little late. Just came to my attention this wasn't really answered so far.

hakre
  • 193,403
  • 52
  • 435
  • 836
1
Note:

This is an internal engine interface which cannot be implemented in PHP scripts. Either IteratorAggregate or Iterator must be used instead. When implementing an interface which extends Traversable, make sure to list IteratorAggregate or Iterator before its name in the implements clause.

i.e. you'll find the interface definition at http://docs.php.net/class.iterator and/or http://docs.php.net/class.iteratoraggregate

VolkerK
  • 95,432
  • 20
  • 163
  • 226
  • So I would need to define an extended ArrayIterator class that converts the fetched array to my desired object in offsetGet and then implement IteratorAggregate in my mysqli_result_extension which returns the new *Iterator instance of $this in getIterator()? Using Iterator I would have to define all methods for it, I don't really want that I don't want to write like 20 methods and additional classes just to turn fetch_assoc into fetch_object Is there possibly some other way? – Torben Nov 16 '12 at 14:15
0

As I stated in the comments, it's easy to do exactly what you want with PDO. Just set the PDO::ATTR_DEFAULT_FETCH_MODE attribute, and it will do the appropriate kind of fetch when you do the foreach loop.

You can also set the PDO::ATTR_STATEMENT_CLASS attribute to define the class to use.

$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_CLASS);
$pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS,array('classname',$constructorArgs));

See http://php.net/manual/en/pdo.setattribute.php for more info.

With that solution, you'd need to change the ATTR_STATEMENT_CLASS attribute for each query, depending on what class you want to instantiate.

Alternatively, you can set it to specify the classname within the query:

$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE);
$stmt = $db->query("SELECT 'classname', * FROM `table` WHERE `id` = 1;");

Simply add the relevant classname to your query as the first field to be selected.

See one of the answers on this related question In PHP, How do I set default PDO Fetch Class? for more on this.

Community
  • 1
  • 1
SDC
  • 14,192
  • 2
  • 35
  • 48
  • Thanks a lot, this will surely help me getting this done! I will probably use the PDO::FETCH_CLASSTYPE style, it seems to be the most 'dynamic' way – Torben Nov 16 '12 at 15:25