0

I'm writing a feature which calls for the records of my joining table to carry extra metadata (Joining-Table with Metadata). I've attempted to implement this in accordance with this section of the Doctrine documentation.

See below for example Entity definitions.

The challenge now is that getGroups and setGroups do not yield/set Group entities (& the same is true from the Group instance perspective), but they yield GroupUser entities.

This adds a substantial delay to process of managing this relationships, which so far have been extremely smooth - for example, I cannot simply add, remove, or check for the existence of a Group to the collection which getGroups yields.

Can anyone identity any errors in my implementation, or else recommend a more fluid way of implementing this concept?

Thanks in advance for any input.

EDIT:

My main concern is this: using this implementation, retrieving a collection of Users from a Group entity requires this Entity method's mediation:

public function getUsers() {
    return $this->users->map(function($groupUser){
        return $groupUser->getUser();
    });
}

I'm concerned that this could imply a major performance hit down the road. Am I incorrect?

Furthermore, how does one re-implement the setUsers method?

Group entity:

<?php

/**
 * @Entity
 * @Table(name="group")
 */

class Group {
    /**
     * @Column(type="integer", nullable=false)
     * @Id
     */
    protected $id = null;

    /**
     * @OneToMany(targetEntity="GroupUser", mappedBy="group")
     * @var \Doctrine\Common\Collections\Collection
     */
    protected $users;
}

User entity:

<?php

/**
 * @Entity
 * @Table(name="user")
 */
class User {
    /**
     * @Column(type="integer", nullable=false)
     * @Id
     */
    protected $id = null;

    /**
     * @OneToMany(targetEntity="GroupUser", mappedBy="user")
     * @var \Doctrine\Common\Collections\Collection
     */
    protected $groups;
}

Joining entity:

<?php
/**
 * @Entity
 * @Table(name="group_user") 
 */
class GroupUser {

    /**
     * @Id
     * @ManyToOne(targetEntity="User", inversedBy="groups")
     * @JoinColumn(name="userId", referencedColumnName="id")
     */
    protected $user;

    /**
     * @Id
     * @ManyToOne(targetEntity="Group", inversedBy="users")
     * @JoinColumn(name="groupId", referencedColumnName="id")
     */
    protected $group;

    /**
     * @Column(type="integer")
     */
    protected $relationship;
}

Related -

Community
  • 1
  • 1
Daniel B.
  • 1,650
  • 1
  • 19
  • 40

1 Answers1

0

I've found just two examples (see question) of entity definitions for this specific type of relationship, however no example code for how they're used. As such it was fairly unclear how fluid (or otherwise) the resulting setters & getters could be expected to be. Hopefully this code will help clear up the approach for anyone else making a similar attempt.

The ideal solution under the circumstances (thanks #doctrine @ freenode) was to implement a custom repository - a more flexible & efficient place for creating & managing the association.

Example Custom Repository for Join-Table with Metadata Class - Solution accompanies code in original question

<?php

use Doctrine\ORM\EntityRepository;

class GroupUserRepository extends EntityRepository {
    /**
     * @param \User $user
     * @param \Group $group
     * @param integer $type One of the integer class constants defined by GroupUser
     * @param string $role Optional string defining user's role in the group.
     * @return \GroupUser
     */
    public function addUserToGroup(User $user, Group $group, $relationship, $role = '') {
        $groupUser = $this->findOneBy(array('user' => $user->getId(), 'group' => $group->getId()));
        if(!$groupUser) {
            $groupUser = new GroupUser();
            $groupUser->setGroup($group);
            $groupUser->setUser($user);
            $groupUser->setRole($role);
            $groupUser->setRelationship($relationship);
            $this->_em->persist($groupUser);
        }
        return $groupUser;
    }

    /**
     * @param \User $user
     * @param \Group $group
     * @return null
     */
    public function removeUserFromGroup(User $user, Group $group) {
        $groupUser = $this->findOneBy(array('user' => $user->getId(), 'group' => $group->getId()));
        if($groupUser)
            $this->_em->remove($groupUser);
    }
}

Then, from the join-table class, modify the Entity meta-data accordingly to specify the custom repository.

<?php
/**
 * @Entity(repositoryClass="\Path\To\GroupUserRepository")
 */
class GroupUser {
    // ...   
}

This causes the custom repository to yield in place of the default one, making a proxy method from the Entity class simple.

<?php
/**
 * @Entity
 */
class Group {
    /**
     * @param \User $user
     * @param integer $relationship One of the integer class constants defined by GroupUser
     * @param string $role Optional string defining user's role in the group.
     * @return \GroupUser
     */
    public function addUser(User $user, $relationship, $role = '') {
        return $this->_em->getRepository('GroupUser')
                ->addUserToGroup($user, $this, $relationship, $role);
    }
}

And things are about as manageable as they were before.

Daniel B.
  • 1,650
  • 1
  • 19
  • 40
  • 1
    This solution fails in many ways. First, it breaks if you use removeUser just after addUser, without a flush between both. It shouldn't fail here, because entities are DB independent. Next, you cannot adapt this approach to bidirectional relationships, because the inverse side could not retrieve the same joining entity as the owning side (it would create a new entity with the same parameters). – Ninj Mar 07 '13 at 10:44
  • Ninj, submit a better solution and I'd be more than happy to accept it – Daniel B. Mar 07 '13 at 16:47
  • 1
    Jean, I didn't mean to offend you, my message was a little rude, sorry. I just wanted to share with everyone the problems i encountered when applying your solution. For the moment, the best solution i found was to manage the association from the calling code. It makes a little overhead but at least i access freely to the exrta fields and add/remove the association entities properly. Considering the linking entity as a full-featured entity, and not as a relation, was the key. You cannot make it behave just like a relation, it's not. – Ninj Mar 08 '13 at 08:41