Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suboptimal behaviour when using Result::fetchAssoc in conjunction with Result::setRowFactory #357

Open
dakujem opened this issue Feb 19, 2020 · 12 comments

Comments

@dakujem
Copy link
Contributor

dakujem commented Feb 19, 2020

Version: current 4.1

Bug Description

When using Result::fetchAssoc in conjunction with Result::setRowFactory, fetchAssoc may throw exception Unknown column 'foo' in associative descriptor. even though the property is accessible when accessing $row->foo via magic getters.

Steps To Reproduce

        $result->setRowFactory(function($data){
            return new class($data) {
                use Strict;
                private $data;
                public function __construct($data)
                {
                    $this->data = $data;
                }
                public function __get(string $name)
                {
                    return $this->data[$name] ?? $this->data->{$name} ?? null;
                }
            };
        });
        array_map(function($v){return $v->foo;},$result->fetchAll()); // works just fine
        $result->fetchAssoc('foo'); // will throw

Unfortunatelly, this is quite a common pattern with many ORMs. 😥

The culprit

the problem is in Result class, where using property_exists with magic props will fail, quoted:

The property_exists() function cannot detect properties that are magically accessible using the __get magic method.

Possible Solution

I'm not yet sure what a backward compatible solution should look like.

@dakujem
Copy link
Contributor Author

dakujem commented Feb 28, 2020

This issue most probably plagues the Result::fetchPairs method as well, since property_exists is used there too.

@dakujem
Copy link
Contributor Author

dakujem commented Feb 28, 2020

I was wondering if I could come up with my own solution to the problem, however, as many times before, I hit the wall called "private" or "final".

First, I could post a PR where one could set the result class created by the Connection class instead of hard-wired Dibi\Result. Simple.
Then one could provide his own result class to overcome this issue.

However, since both fetchPairs and fetchAssoc are final, it would either involve lots of code duplication or lots of code rewrite in the Dibi library itself.

I would like to make Dibi support entities that use magic getters, which is quite common use case.
What is your snace on this matter, David? @dg

@dg
Copy link
Owner

dg commented Feb 28, 2020

I think that fetchAssoc is not suitable for entities. Better way is to create new fetchEntities() function. (It does not necessarily have to be a method of the Result class descendant).

@dakujem
Copy link
Contributor Author

dakujem commented Feb 28, 2020

Consider this case. Really simple and useful.

I want to fetch users and then have them automatically sorted into groups by company and account type.
Somewhere in the database layer there is this code to fetch users:

$allUsers = $connection->query('SLEECT * FROM `users` WHERE ... ')
    ->setRowFactory(function($row){ return new UserEntity($row); })
    ->fetchAssoc('company|accountType|id');

Then somewhere else in the application where I only work with entities, there might be code to access users or iterate over them $allUsers[$company][$type][$userId].

To support this, I have to make a 3-levels nested foreach cycle to convert the result to entities, and what is worse, the level of nesting changes depending on how the argument to fetchAssoc looks... 👎

$result = [];
foreach($allUsers as $companyId => $group){
    foreach($group as $type => $users){
        foreach($usersas $userId => $row){
            $result[$companyId][$type][$userId] = new UserEntity($row);
        }
    }
}
return $result;

Why would not this be a supported case for fetchAssoc?

@dakujem
Copy link
Contributor Author

dakujem commented Feb 28, 2020

I forgot to point out that the the suggested fetchEntities function would have to know the depth of nesting of the result, which it does not know.

And I consider parsing the argument to fetchAssoc a hack 🙂

@dg
Copy link
Owner

dg commented Feb 28, 2020

The solution would be to modify fetchAssoc to

  • not use $this->fetch() directly, but fetch row data via $this->getResultDriver()->fetch(true);
  • do all tranformations
  • apply $this->rowFactory & rowClass to the leaf, ie. the last item in structure

dg added a commit that referenced this issue Feb 28, 2020
@dg
Copy link
Owner

dg commented Feb 28, 2020

WIP 6b34262

dg added a commit that referenced this issue Feb 28, 2020
@dakujem
Copy link
Contributor Author

dakujem commented Feb 28, 2020

So bascally you modify the order to do the transformations first, then apply the row factory. Well at a point in time I found it strange that the row factory was invoked before fetchAssoc transformed the result, but it slipped of my mind. If this is not a BCB, then it seems more elegant to me. Since Result is tightly coupled to Connection, it should not be a BCB, at least not the the public interface. Good job.

@dg
Copy link
Owner

dg commented Feb 29, 2020

In fact, it is BC break :-/ See #284

In my opinion, this cannot be solved without BC. And I don't really want to deal with it :-)

@dakujem
Copy link
Contributor Author

dakujem commented Mar 2, 2020

I wonder ... it's quite common to have an entity class put all the "row data" into one attribute and use magic accessors.

So you had the very same idea of soluton here #284, if i understand it correctly, which trned out to be a BCB.


Would making the result class configurable in Connection and removing the final modifiers from those functions in Result class be a BC break? that would allow us to modify the behaviour on the app side.

Furthermore, if there was an interface introduced that would be used along with the property_exists call, then that could solve the issue without a BC break, at the expense of adding a new interface to the library.

Something like the following.

// 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) &&
        (!$row instanceof ExposesProperties || !$row->hasProperty($as))
    ) {
        throw new \InvalidArgumentException("Unknown column '$as' in associative descriptor.");
    }
}
interface ExposesProperties {
    public function hasProperty(string $name): bool;
}

The interface could then be implemented by the entity classes.

@dg
Copy link
Owner

dg commented Mar 3, 2020

Yes, I'm afraid any change can be a BC break. New interface seems like a ways how to solve. I have to think about it more and unfortunately I don't have time for it now.

@milo
Copy link
Collaborator

milo commented Jun 23, 2021

Related probem is with ->setRowClass(null). In that case, fetchAssoc() throws TypeError: property_exists(): Argument #1 ($object_or_class) must be of type object|string, array given

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants