在学说中加载相关实体的子集

I am not sure if what I want to do is possible or not, but I can't find any documentation on it. Hopefully I am just looking in the wrong places.

What I would like to do is load an object that will always load a subset of a related object. some background - I want to keep a history of the contacts that a user deletes, so when a contact is deleted, I set a isDeleted column in the db to true. I will rarely want to load the deleted contacts, so I will worry about that later. But when I load a user, and then load the contacts (in my case by serializing the whole user object), all of the contacts are loaded including the deleted ones. So I would like to only load the contacts that have not been deleted yet. This is what I have right now, and I don't know why it is not working.

use Doctrine\Common\Collections\Criteria,
    Doctrine\Common\Collections\ArrayCollection;

class User{
/**
 * @ORM\OneToMany(targetEntity="Contacts", mappedBy="user", cascade={"persist"}, fetch="EXTRA_LAZY")
 * @ORM\JoinColumn(name="contact_id", referencedColumnName="contact_id")
 */
private $contacts;

public function __construct(){
    $this->contacts = new ArrayCollection();
}

/**
 * Get contacts
 *
 * @return \Doctrine\Common\Collections\ArrayCollection
 */
public function getContacts()
{
    // This is where I am filtering the contacts
    $criteria = Criteria::create()
    ->where(Criteria::expr()->eq("isDeleted", false));
    return $this->contacts->matching($criteria);
}

And then I am loading and serializing a user using the JMS Serializer and FOSRest in my Controller.

public function getUserAction($slug){
    $data = $this->getDoctrine()->getRepository('MyCoreBundle:User')->find($slug);
    $view = $this->view($data, 200);

    return $this->handleView($view);
}

And the result is the following JSON:

{
userId: 7,
creationDate: "2014-07-28T22:05:43+0000",
isActive: true,
username: "myUser",
contacts: [
    {
        contactId: 23,
        firstName: "Jim",
        lastName: "Bin",
        email: "zzz@zzz.com",
        phone: "1231231231",
        isDeleted: true
    }
]
}

I am not sure why this is still loading the deleted user. If I do the filtering in my controller instead of the Entity class, then the filtering works. But I don't want to have to do that everywhere in my application that a contact may be loaded. If I could get something working in the Entity class, that would be ideal. Has anyone done something similar that could work?

I'm using Symfony 2.3

I'd highly recommend the Soft-Deleteable extension. You'd have to use a deletedAt field which is null when the object is not deleted, and a timestamp if deleted.

It goes like this:

/**
 * @ORM\Entity
 * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
 */
class User
{
    /**
     * @ORM\Column(name="deletedAt", type="datetime", nullable=true)
     */
    private $deletedAt;

    public function getDeletedAt()
    {
        return $this->deletedAt;
    }

    public function setDeletedAt($deletedAt)
    {
        $this->deletedAt = $deletedAt;
    }

}

Have a look at this extension here: Soft-Deleteable Extension

I think your problem is that getContacts doesn't actually get called when you fetch a User from its repository, that's done using a standard Doctrine query, and since User and Contacts are related regardless of whether a Contact has been deleted, they all come across. One solution would be to create your own UserRepository with a findUserWithActiveContacts() method, which executes a more appropriate query, and use that instead.

For example:

UserRepository.php

namespace Demo\MyBundle\Entity;

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
    public function findWithActiveContacts($slug)
    {
        $result= $this->createQueryBuilder('u')
            ->join('u.contacts', 'c')
            ->select('u,c')
            ->where('c.isDeleted = false')
            ->andWhere('u.slug = :slug')
            ->setParameter('slug', $slug)
            ->getQuery()
            ->getResult();

        return $result;
    }
}

And hook up the entity to the new repository:

User.php

class User{
/**
 * @ORM\OneToMany(targetEntity="Contacts", mappedBy="user", cascade={"persist"}, fetch="EXTRA_LAZY")
 * @ORM\JoinColumn(name="contact_id", referencedColumnName="contact_id")
 * @ORM\Entity(repositoryClass="Demo\MyBundle\Entity\UserRepository")
 */

And then in the controller:

public function getUserAction($slug){
    $data = $this->getDoctrine()->getRepository('MyCoreBundle:User')->findWithActiveContacts($slug);
    $view = $this->view($data, 200);

    return $this->handleView($view);
}

So as long as you use findWithActiveContacts instead of find you should always get just the non-deleted contacts, without having to change anything else.

I think you could probably override find itself in your customer Repository if you wanted, but for you that would just change the name of the method you're calling, and I'd be worrying that other code (e.g. stuff which fetches entities for forms) would start using it, which might be what you want but also might have unpredictable consequences.

The official Doctrine way of tweaking all queries in this sort of fashion is to use Filters, which actually slips in an extra where clause to everything which goes through Doctrine. This is how the SoftDeleteable extension does it.