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

DQL Query: process ArrayCollection values to ease development #590

Conversation

michaelperrin
Copy link
Contributor

I added some code to ease "where in" parameter binding.

As you know, when a where condition is added, the object itself can be passed as a parameter and it's id is automatically retrieved:

$queryBuilder = $this
    ->where('model.category = :category')
    ->setParameter('category', $category)
;

Where $category is an object.

But it doesn't work for collections:

$queryBuilder = $this
    ->where('model.category IN (:categories)')
    ->setParameter('categories', $categories)
;

Where categories is an ArrayCollection object (retrieved from a many to one relation for instance).

This doesn't work in the current version of Doctrine, and my PR solves that.

Note that I didn't add any unit test for this feature. Can you explain me where I should add the test?

This enhancement is now unit-tested in this same PR.

So far, the only solution was to do the following, which is pretty borring:

$categoryIds = array();

foreach ($categories as $category) {
    $categoryIds[] = $category->getId();
}

$queryBuilder = $this
    ->where('model.category IN (:category_ids)')
    ->setParameter('category_ids', $categoryIds)
;

@doctrinebot
Copy link

Hello,

thank you for positing this Pull Request. I have automatically opened an issue on our Jira Bug Tracker for you with the details of this Pull-Request. See the Link:

http://doctrine-project.org/jira/browse/DDC-2319

$values = array();

foreach ($value as $valueEntry) {
$singleValue = $this->_em->getUnitOfWork()->getSingleIdentifierValue($valueEntry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if it does not have a single identifier ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof It's tested on the line below and an Exception is thrown, this is the same behavior for the object case (see line 283 of the same file)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that entities using a composite identifier would throw an exception here because getSingleIdentifierValue is not supported for them (it cannot be a single one)

@michaelperrin
Copy link
Contributor Author

@stof I updated the code according to the comments you made. If you think the feature is worth being merged, I'll write tests associated to it.

@michaelperrin
Copy link
Contributor Author

@stof Sorry for the misunderstanding. You're right, an exception (ORMInvalidArgumentException) is thrown if one of the collection entities has a composite identifier. That's actually the same when a single entity is bound, see line 281 of the AbstractQuery class:

$value = $this->_em->getUnitOfWork()->getSingleIdentifierValue($value);

When binding object parameters, developers will know (and already know with the current code) that only object having a single identifier can be used thanks to this raised exception.

I added by the way more control on the collection entities and added tests as well for this enhancement.

@@ -284,6 +285,26 @@ public function processParameterValue($value)
}
}

if ($value instanceof Collection) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this might be easier to read :

if ($value instanceof Collection) {
    $values = array();

    foreach ($value as $valueEntry) {

        if ( ! is_object($valueEntry) || ! $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($valueEntry))) {
            return $value;
        }

        $singleValue = $this->_em->getUnitOfWork()->getSingleIdentifierValue($valueEntry);

        if ($singleValue === null) {
            throw ORMInvalidArgumentException::invalidIdentifierBindingEntity();
        }

        $values[] = $singleValue;
    }

    return $values;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please move the block inside if() into a method processCollectionParameterValue()? This would avoid the method to become that long.

After that its good to merge from my POV.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beberlei This is now done!

@michaelperrin
Copy link
Contributor Author

@FabioBatSilva Done (small condition refactoring)

@michaelperrin
Copy link
Contributor Author

Rebased commits (no more conflicts with current master branch)


foreach ($value as $valueEntry) {
if ( ! is_object($valueEntry) || ! $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($valueEntry))) {
return $value;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure? I think it must be:

        foreach ($value as $valueEntry) {
            if ( ! is_object($valueEntry) || ! $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($valueEntry))) {
                $values[] = $value;
                continue;

@michaelperrin
Copy link
Contributor Author

@Baachi Hum, you're right, that's better adding the value itself in case the value in the collection parameter is not an object. That's fixed now.


foreach ($value as $valueEntry) {
if ( ! is_object($valueEntry) || ! $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($valueEntry))) {
$values[] = $value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong,
You should add a continue; here..
Otherwise you are adding the value and then trying to extract the identifier which will probably fail ..

Please add a test for this case as well..

@goetas
Copy link
Member

goetas commented Apr 26, 2013

Is compatible with #522 ?

@@ -288,10 +289,45 @@ public function processParameterValue($value)
return $value->name;
}

if ($value instanceof Collection) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not always passing around objects in collections, and collections musn't have only objects. I am not sure if this is a helpful addition for users and as you show in the test, the API actually requires quite some code to instantiate and add elements to the collection, whereas i could just do:

$query->setParameter('foo', array_map(function ($object) {
    return $object->getId();
}, array($a, $b));

This is much simpler and generic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beberlei The case where objects and values can be mixed in the same collection is now handled and unit-tested. And while it requires some code if the collection is built manually, it's not the case when the collection has been generated by Doctrine (like when retrieving a one to many value on an object) and can later directly be used as a query parameter with the solution I propose. What do you think?

@michaelperrin
Copy link
Contributor Author

@FabioBatSilva Aw, my mistake, sorry. This is now fixed, and unit tested.

@beberlei
Copy link
Member

beberlei commented May 4, 2013

Just checked the code again, please remove the processcollectionParamterValue() and just add a check for instanceof Collection in the line if (is_array($value)) right at the top. This does exactly the same and only needs one line change.

@michaelperrin
Copy link
Contributor Author

@beberlei This is indeed a more clever way to implement it indeed and this is now done with the last commit.

I first convert the collection to an array as the processParameterValue method would return a Collection instead, which is not handled correctly.
An other way would be to do the instanceof Collection test and the conversion to an array just before returning the value in the if (is_array($value)) condition. There might be other solutions, but this is my first PR on Doctrine, so I prefer to keep things simple.

Anyway, this solution works fine!

Thanks for your help!

@michaelperrin
Copy link
Contributor Author

Hello,

Do you have any news on the merge of this PR?
I can make further changes if needed.

@michaelperrin
Copy link
Contributor Author

@beberlei I rebased my commits and resolved conflicts so that it can be merged to master.

@asm89
Copy link
Member

asm89 commented Dec 17, 2013

@michaelperrin Can you squash your PR into 1 commit?

@michaelperrin
Copy link
Contributor Author

@asm89 Done!

@guilhermeblanco
Copy link
Member

Waiting for Travis to respond. It failed temporarily and manually triggered a new build.

guilhermeblanco added a commit that referenced this pull request Dec 17, 2013
…-value

DQL Query: process ArrayCollection values to ease development
@guilhermeblanco guilhermeblanco merged commit 423ea00 into doctrine:master Dec 17, 2013
@michaelperrin
Copy link
Contributor Author

Thanks @guilhermeblanco !

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

Successfully merging this pull request may close these issues.

None yet

9 participants