Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

DQL Query: process ArrayCollection values to ease development #590

Merged
merged 1 commit into from 4 months ago

9 participants

Michaël Perrin doctrinebot Asmir Mustafic Benjamin Eberlei Alexander Guilherme Blanco Christophe Coevoet Fabio B. Silva Markus Bachmann
Michaël Perrin

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
Collaborator

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

lib/Doctrine/ORM/AbstractQuery.php
... ...
@@ -284,6 +284,22 @@ public function processParameterValue($value)
284 284
             }
285 285
         }
286 286
 
  287
+        if ($value instanceof ArrayCollection) {
  288
+            $values = array();
  289
+
  290
+            foreach ($value as $valueEntry) {
  291
+                $singleValue = $this->_em->getUnitOfWork()->getSingleIdentifierValue($valueEntry);
3
Christophe Coevoet
stof added a note

What if it does not have a single identifier ?

@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)

Christophe Coevoet
stof added a note

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/AbstractQuery.php
... ...
@@ -284,6 +284,22 @@ public function processParameterValue($value)
284 284
             }
285 285
         }
286 286
 
  287
+        if ($value instanceof ArrayCollection) {
1
Christophe Coevoet
stof added a note

This would not solve your use case as a relation is likely to give you a PersistentCollection, not an ArrayCollection

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Michaël Perrin

@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.

Michaël Perrin

@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.

lib/Doctrine/ORM/AbstractQuery.php
... ...
@@ -284,6 +285,26 @@ public function processParameterValue($value)
284 285
             }
285 286
         }
286 287
 
  288
+        if ($value instanceof Collection) {
3
Fabio B. Silva Owner

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;
}
Benjamin Eberlei Owner
beberlei added a note

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.

@beberlei This is now done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Michaël Perrin

@FabioBatSilva Done (small condition refactoring)

Michaël Perrin

Rebased commits (no more conflicts with current master branch)

lib/Doctrine/ORM/AbstractQuery.php
((12 lines not shown))
  300
+     * Processes a collection parameter value.
  301
+     *
  302
+     * @param Collection $value
  303
+     *
  304
+     * @return array|Collection Array of single identifier values for each entry of the collection
  305
+     *                          or the collection if one of the entries is invalid
  306
+     *
  307
+     * @throws ORMInvalidArgumentException
  308
+     */
  309
+    private function processCollectionParameterValue($value)
  310
+    {
  311
+        $values = array();
  312
+
  313
+        foreach ($value as $valueEntry) {
  314
+            if ( ! is_object($valueEntry) || ! $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($valueEntry))) {
  315
+                return $value;
1
Markus Bachmann
Baachi added a note

Sure? I think it must be:

        foreach ($value as $valueEntry) {
            if ( ! is_object($valueEntry) || ! $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($valueEntry))) {
                $values[] = $value;
                continue;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Michaël Perrin

@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.

lib/Doctrine/ORM/AbstractQuery.php
((12 lines not shown))
  300
+     * Processes a collection parameter value.
  301
+     *
  302
+     * @param Collection $value
  303
+     *
  304
+     * @return array|Collection Array of single identifier values for each entry of the collection
  305
+     *                          or the collection if one of the entries is invalid
  306
+     *
  307
+     * @throws ORMInvalidArgumentException
  308
+     */
  309
+    private function processCollectionParameterValue($value)
  310
+    {
  311
+        $values = array();
  312
+
  313
+        foreach ($value as $valueEntry) {
  314
+            if ( ! is_object($valueEntry) || ! $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($valueEntry))) {
  315
+                $values[] = $value;
1
Fabio B. Silva Owner

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..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Asmir Mustafic

Is compatible with #522 ?

lib/Doctrine/ORM/AbstractQuery.php
... ...
@@ -288,10 +289,45 @@ public function processParameterValue($value)
288 289
             return $value->name;
289 290
         }
290 291
 
  292
+        if ($value instanceof Collection) {
2
Benjamin Eberlei Owner
beberlei added a note

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.

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Michaël Perrin

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

Benjamin Eberlei
Owner

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.

Michaël Perrin

@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!

Michaël Perrin

Hello,

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

Michaël Perrin

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

Alexander
Collaborator

@michaelperrin Can you squash your PR into 1 commit?

Guilherme Blanco

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

Guilherme Blanco guilhermeblanco merged commit 423ea00 into from
Guilherme Blanco guilhermeblanco closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 2 authors.

Dec 17, 2013
Michaël Perrin Simpler way to handle Collection parameters in DQL queries (refs #DDC…
…-2319)
1032a16
This page is out of date. Refresh to see the latest.
7  lib/Doctrine/ORM/AbstractQuery.php
@@ -20,6 +20,7 @@
20 20
 namespace Doctrine\ORM;
21 21
 
22 22
 use Doctrine\Common\Util\ClassUtils;
  23
+use Doctrine\Common\Collections\Collection;
23 24
 use Doctrine\Common\Collections\ArrayCollection;
24 25
 
25 26
 use Doctrine\ORM\Query\Parameter;
@@ -405,6 +406,10 @@ public function processParameterValue($value)
405 406
             return $value;
406 407
         }
407 408
 
  409
+        if ($value instanceof Collection) {
  410
+            $value = $value->toArray();
  411
+        }
  412
+
408 413
         if (is_array($value)) {
409 414
             foreach ($value as $key => $paramValue) {
410 415
                 $paramValue  = $this->processParameterValue($paramValue);
@@ -1089,7 +1094,7 @@ public function __clone()
1089 1094
 
1090 1095
     /**
1091 1096
      * Generates a string of currently query to use for the cache second level cache.
1092  
-     * 
  1097
+     *
1093 1098
      * @return string
1094 1099
      */
1095 1100
     protected function getHash()
49  tests/Doctrine/Tests/ORM/Functional/QueryTest.php
@@ -676,6 +676,55 @@ public function testSetParameterBindingSingleIdentifierObject()
676 676
         $q->getResult();
677 677
     }
678 678
 
  679
+    /**
  680
+     * @group DDC-2319
  681
+     */
  682
+    public function testSetCollectionParameterBindingSingleIdentifierObject()
  683
+    {
  684
+        $u1 = new CmsUser;
  685
+        $u1->name = 'Name1';
  686
+        $u1->username = 'username1';
  687
+        $u1->status = 'developer';
  688
+        $this->_em->persist($u1);
  689
+
  690
+        $u2 = new CmsUser;
  691
+        $u2->name = 'Name2';
  692
+        $u2->username = 'username2';
  693
+        $u2->status = 'tester';
  694
+        $this->_em->persist($u2);
  695
+
  696
+        $u3 = new CmsUser;
  697
+        $u3->name = 'Name3';
  698
+        $u3->username = 'username3';
  699
+        $u3->status = 'tester';
  700
+        $this->_em->persist($u3);
  701
+
  702
+        $this->_em->flush();
  703
+        $this->_em->clear();
  704
+
  705
+        $userCollection = new ArrayCollection();
  706
+
  707
+        $userCollection->add($u1);
  708
+        $userCollection->add($u2);
  709
+        $userCollection->add($u3->getId());
  710
+
  711
+        $q = $this->_em->createQuery("SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u IN (:users) ORDER BY u.id");
  712
+        $q->setParameter('users', $userCollection);
  713
+        $users = $q->execute();
  714
+
  715
+        $this->assertEquals(3, count($users));
  716
+        $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[0]);
  717
+        $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[1]);
  718
+        $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[2]);
  719
+
  720
+        $resultUser1 = $users[0];
  721
+        $resultUser2 = $users[1];
  722
+        $resultUser3 = $users[2];
  723
+
  724
+        $this->assertEquals($u1->username, $resultUser1->username);
  725
+        $this->assertEquals($u2->username, $resultUser2->username);
  726
+        $this->assertEquals($u3->username, $resultUser3->username);
  727
+    }
679 728
 
680 729
     /**
681 730
      * @group DDC-1822
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.