DDC-3234: Empty properties when filtering collections #4022

Closed
doctrinebot opened this Issue Jul 30, 2014 · 8 comments

2 participants

@doctrinebot

Jira issue originally created by user diogo.domanski:

I'm facing some troubles when filtering an entity association (ArrayCollection) by using Criteria.

The scenario is the following: I have 3 entities:

<?php

namespace Entity;

use Doctrine\ORM\Mapping as ORM;
use \Doctrine\Common\Collections\Criteria;
use Zend\Stdlib\Hydrator;

/****
 * User
 *
 * @ORM\Table(name="user")
 * @ORM\Entity(repositoryClass="Entity\UserRepository")
 * @ORM\HasLifecycleCallbacks
 * @author domanski
 */
class User implements \Serializable {
    /****
     *
     * @ORM\Id
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="AUTO")
     * @var integer
     */
    private $id;

    /****
     * @ORM\Column(name="delete_date", type="datetime")
     * @var \DateTime
     */
    private $deleteDate;

    public function **construct(array $options = array()) {
        if (!empty($options))
            $this->hydrate($options);
    }

    public function hydrate(array $options = array(), \Doctrine\ORM\EntityManager $em = null) {
        $hydrator = new Hydrator\ClassMethods();
        $hydrator->hydrate($options, $this);
    }

    /****
     * 
     * @return int
     */
    public function getId() {
        return $this->id;
    }

    /****
     * 
     * @param int $id
     * @return \Entity\User
     */
    public function setId($id) {
        $this->id = $id;
        return $this;
    }

    /****
     * 
     * @return \DateTime
     */
    public function getDeleteDate() {
        return $this->deleteDate;
    }

    /****
     * @param \DateTime|null $deleteDate
     * @return \Entity\User
     */
    public function setDeleteDate($deleteDate = null) {
        $this->deleteDate = $deleteDate;
        return $this;
    }

    /****
     * 
     * @return array
     */
    public function toArray() {
        $result = array(
            'id' => $this->getId(),
            'delete_date' => $this->getDeleteDate()
        );

        return $result;
    }

}
<?php

namespace Entity;

use Doctrine\ORM\Mapping as ORM;
use \Doctrine\Common\Collections\Criteria;

use Zend\Stdlib\Hydrator;

/****
 * Description of FieldWorker
 *
 * @ORM\Entity(repositoryClass="Entity\FieldWorkerRepository")
 * @ORM\Table(name="field_worker")
 * @ORM\HasLifecycleCallbacks
 * @author domanski
 */
class FieldWorker {

    /****
     * This attribute must exist so the inverse join with any other entity can work
     * 
     * @ORM\Id
     * @ORM\Column(type="integer", name="user_id")
     * @var string
     */
    protected $id;

    /****
     * 
     * @ORM\OneToOne(targetEntity="Entity\User")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     * @var \Entity\User
     */
    protected $user;

    public function **construct($options = array()) {       
        if(!empty($options))
            $this->hydrate($options);
    }

    public function hydrate(array $options = array(), \Doctrine\ORM\EntityManager $em = null) {
        if(!empty($em)) {
            // user
            if(isset($options['user']))
                $options['user'] = $em->getReference('Entity\User', $options['user']);
            else if(isset($options['user_id']))
                $options['user'] = $em->getReference('Entity\User', $options['user_id']);

        }

        $hydrator = new Hydrator\ClassMethods();
        $hydrator->hydrate($options, $this);
    }

    /****
     * 
     * @return \Entity\User
     */
    public function getUser() {
        return $this->user;
    }

    /****
     * 
     * @param \Entity\User $user
     * @return \Entity\FieldWorker
     */
    public function setUser(\Entity\User $user) {
        $this->user = $user;
        $this->id = $user->getId();
        return $this;
    }

    /****
     * 
     * @return array
     */
    public function toArray() {
        return $this->getUser()->toArray();
    }
}
<?php

namespace Obra\Entity;

use Doctrine\ORM\Mapping as ORM;
use Zend\Stdlib\Hydrator;
use Doctrine\Common\Collections\Criteria;

/****
 * Description of Stage
 *
 * @ORM\Table(name="stage")
 * @ORM\Entity(repositoryClass="Entity\StageRepository")
 * @ORM\HasLifecycleCallbacks
 * @author domanski
 */
class Stage {

    /****
     *
     * @ORM\Id
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="AUTO")
     * @var integer
     */
    private $id;

    /****
     * @ORM\ManyToMany(targetEntity="Entity\FieldWorker")
     * @ORM\JoinTable(name="stage*field*worker",
     *      joinColumns={@ORM\JoinColumn(name="stage_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="field*worker_id", referencedColumnName="user*id")}
     *  )
     * @var \Doctrine\Common\Collections\ArrayCollection
     */
    private $fieldWorkers;

    public function **construct(array $options = array()) {
        $this->fieldWorkers = new \Doctrine\Common\Collections\ArrayCollection();

        if (!empty($options))
            $this->hydrate($options);
    }

    public function hydrate(array $options = array(), \Doctrine\ORM\EntityManager $em = null) {
        $hydrator = new Hydrator\ClassMethods();
        $hydrator->hydrate($options, $this);
    }

    /****
     * 
     * @return int
     */
    public function getId() {
        return $this->id;
    }

    /****
     * 
     * @param int $id
     * @return \Entity\Stage
     */
    public function setId($id) {
        $this->id = $id;
        return $this;
    }

    /****
     * 
     * @return \Doctrine\Common\Collections\ArrayCollection
     */
    public function getFieldWorkers() {
        $criteria = Criteria::create()
                ->where(Criteria::expr()->isNull("user.deleteDate"))
                ->orderBy(array("user.name" => Criteria::ASC));

        return $this->fieldWorkers->matching($criteria);
    }

    /****
     * 
     * @return \Entity\Stage
     */
    public function clearFieldWorkers() {
        $this->fieldWorkers->clear();
        return $this;
    }

    /****
     * 
     * @param \Entity\FieldWorker $fieldWorker
     * @return \Entity\Stage
     */
    public function addFiscal(\Entity\FieldWorker $fieldWorker) {
        $this->fieldWorkers->add($fieldWorker);
        return $this;
    }

    /****
     * 
     * @return array
     */
    public function toArray() {
        $result = array(
            "id" => $this->getId(),
            "field_workers" => array()
        );

        foreach ($this->getFieldWorkers() as $fieldWorker) {
            $result['field_workers'][] = $fieldWorker->toArray();
        }

        return $result;
    }
}

The problem is that whenever the Stage::getFieldWorkers() method is invoked, the list of associated field workers is returned correctly, however if I try to retrieve the respective user of any field worker, it is NULL.

For example:

$stageEntity = $em->getRerefence('Entity\Stage', 1);
$fieldWorkers = $stageEntity->getFieldWorkers();

foreach($fieldWorkers as $fieldWorker) {
   $userId = $fieldWorker->getUser()->getId(); // This throws an error message saying that the result of getUser() is NULL
}

Another example would be if I try to call $stageEntity->toArray() (because it does the same thing as show above).

The database tables are as following:

CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `delete_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `field_worker` (
  `user_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`user_id`),
  CONSTRAINT `fk*field_worker_user1` FOREIGN KEY (`user*id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `stage` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `stage*field*worker` (
  `stage_id` int(10) unsigned NOT NULL,
  `field*worker*id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`stage*id`,`field_worker*id`),
  KEY `fk*stage_field_worker_stage_idx` (`stage*id`),
  KEY `fk*stage_field_worker_field_worker_idx` (`field_worker*id`),
  CONSTRAINT `fk*stage_field_worker_field_worker` FOREIGN KEY (`field_worker_id`) REFERENCES `field_worker` (`user*id`) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT `fk*stage_field_worker_stage` FOREIGN KEY (`stage*id`) REFERENCES `stage` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` SET `id` = 1;
INSERT INTO `field*worker` SET `user*id` = 1;
INSERT INTO `stage` SET `id` = 1;
INSERT INTO `stage*field_worker` SET `stage_id` = 1, `field_worker*id` = 1;

After hours trying to understand what was wrong, I decided to make a test and modify the Stage::getFieldWorkers() method by removing the filtering Criteria:

public function getFieldWorkers() {
    return $this->fieldWorkers;
}

By simply doing that, the getUser() method started to return an entity of type User (as expected).

I am not a Doctrine ORM expert, but there seems to be something wrong with ArrayCollection filtering (matching() method)

@doctrinebot

Comment created by diogo.domanski:

The same problem occurs with QueryBuilder. For example:

<?php

namespace Entity;

use Doctrine\ORM\EntityRepository;

/****
 * Description of StageRepository
 *
 * @author domanski
 */
class StageRepository extends EntityRepository 
{

    public function findByFieldWorkerId($fieldWorkerId) {
        $qb = $this->getEntityManager()->createQueryBuilder()
            ->select('s')
            ->from('Entity\Stage', 's')
            ->innerJoin('s.fieldWorkers', 'f')
            ->innerJoin('f.user', 'u', \Doctrine\ORM\Query\Expr\Join::WITH, "u.deleteDate IS NULL AND u.id = :field*worker*id")
                ->setParameter('field*worker*id', $fieldWorkerId);

        return $qb->getQuery()->getResult();
     }
}

If I try to iterate over the result of StageRepository::findByFieldWorkerId() and call toArray() of any element, I get the same error. I've even tried to remove the inner join with User entity from query:

    public function findByFieldWorkerId($fieldWorkerId) {
        $qb = $this->getEntityManager()->createQueryBuilder()
            ->select('s')
            ->from('Entity\Stage', 's')
            ->innerJoin('s.fieldWorkers', 'f');

        return $qb->getQuery()->getResult();
     }
@doctrinebot

Comment created by @ocramius:

I suspect that something is wrong in your mappings then...

@doctrinebot

Comment created by diogo.domanski:

Hi Marco,

The mappings are shown above. The only thing I did was to omit some entities/tables properties/columns that don't have to see with the associations - except the field worker (table and Entity) and stagefieldworker table, that are fully presented.

Maybe is important to notice that the fieldworker table has only one column (userid) that is primary key and foreign key (referencing user.id).

Thanks for you support

@doctrinebot

Comment created by @ocramius:

[~diogo.domanski] we can't debug this as it is. We'd need a functional test case to be added to https://github.com/doctrine/doctrine2/tree/0650bb954f5e8d05776f99abd04c81948413299f/tests/Doctrine/Tests/ORM/Functional/Ticket first, in order to see the failure

@doctrinebot

Comment created by diogo.domanski:

[~ocramius] is there any documentation (tutorial, instructions, guide, etc) that I can use to learn how to write the functional test cases that I need?

@doctrinebot

Comment created by @ocramius:

[~diogo.domanski] you need to look at the existing ones.

For running the test suite:

git clone git@github.com:doctrine/doctrine2.git
cd doctrine2 
curl -s https://getcomposer.org/installer | php --
./composer.phar install
./vendor/bin/phpunit
@doctrinebot

Comment created by diogo.domanski:

I solved the problem by removing the property $id from FieldWorker entity and add annotation @ORM\Id to $user property (in this same entity).

I didn't understand why the previous definition of FieldWorker entity was not working. I have another similar relationship scenario, between 3 different entities, and the error does not occur.

Thanks to all for the support

@doctrinebot

Issue was closed with resolution "Cannot Reproduce"

@Ocramius Ocramius was assigned by doctrinebot Dec 6, 2015
@doctrinebot doctrinebot closed this Dec 6, 2015
@doctrinebot doctrinebot added the Bug label Dec 7, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment