7

Let's say I have an Doctrine's (version 2) entity as follows:

<?php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * User * * @ORM\Table(name="users") * @ORM\Entity */ class User { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id /** * @var string * * @ORM\Column(name="name", type="string", length=50, nullable=false) */ private $name; /** * @var string * * @ORM\Column(name="group_id", type="string", length=6, nullable=true) */ private $groupId; // Getters and setters... } 

Now, I would like to manage User's relation to Group, but with some conditions, like:

  1. returning NULL (or some sort of skeleton/template of \AppBundle\Entity\Group with fixed values not loaded from database) if Group of users.group_id does not exist, even if it is set to a value (no key restrictions set in the database to prevent this behaviour), so some sort of validation/check required
  2. lazy load Group, when calling $user->getGroup()

I am reading Google on and on and I'm confused of how to achieve that properly (in the line with Doctrine/Symfony way).

I could add a ManyToOne to the entity's class relation like this:

/** * @var \AppBundle\Entity\Group * * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Group") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="group_id", referencedColumnName="id") * }) */ private $group; 

But then how to prevent exceptions caused by non-existing foreign keys? I.e. I would like to retrieve user's group details when it is available in my database, but when it is not I do not want Doctrine to throw an exception an crash my application.

People say that using an Entity Manager from within an Entity is a very bad practice and I agree. But I am confused about using Proxy Objects or Inheritance Mapping for this purposes.

It seems like using Models could be the way, but I couldn't find any strong documentation of how to properly implement them in Doctrine2.

Please help if you can. In Kohana it was so, so simple (but immature this way).


EDIT:

@Massimiliano Fedel suggested trying catching an exception in User::getGroup() method, to eventually make non-existed groups return as NULL.

So I have commited this code:

/** * Get group * * @return \AppBundle\Entity\Group */ public function getGroup() { try { $group = $this->group; } catch (\Doctrine\ORM\EntityNotFoundException $e) { $group = null; } return $group; } 

Unfortunatelly, it seems that exception cannot be catched this way, because framework exits with an Doctrine\ORM\EntityNotFoundException:

Entity of type 'AppBundle\Entity\Group' for IDs id(999) was not found 

EDIT 2:

Below you can find some basing schema of User flow, i.e. why I can not ensure that all Users will have available Groups in my database.

enter image description here

4
  • Can you explain a bit more of the actual use case? Let's suppose we have a user with a group not in your database. What do you expect to happen in between requests? Commented Feb 12, 2016 at 13:51
  • @Cerad what do you mean "between requests"? Commented Feb 12, 2016 at 14:08
  • So you make a user with a group that is not in the database and obviously you don't want to actually persist the group. (Why I don't know). So now the user navigates to some other page. Do you expect the $user object to still have the same group? If so, where will the group be stored? If we knew why you wanted this sort of functionality then perhaps we could offer more suggestions. Commented Feb 12, 2016 at 14:19
  • @Cerad Hmm, the user-group example is just a mockup to make things simple and understanable. However, the real case is about API-API communication, where some records are produced by my API, then send to remote API, which processes them somehow, and then they're returned back to my API but meanwhile a lot of other records, not created by my system, are also returned to me, and so it happens that they're related with groups that I do not have access to.... Commented Feb 12, 2016 at 14:49

4 Answers 4

1

1)Have you tried catching the exception inside the getter method of the "group"? so that you can catch the exception and return "null" in case an exception occured.

2) as from the doctrine 2.1 documentation: "Associations are marked as Lazy by default, which means the whole collection object for an association is populated the first time its accessed." http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html

Sign up to request clarification or add additional context in comments.

2 Comments

1. looks like a good idea (at least good to try), 2. this will do, thanks
I have tried what you have suggested @Massimiliano, but it didn't work - see my first EDIT in the question's body.
1

Well, what about creating a service, that is going to wrap your CRUD operations on the User entity, let's say UserService? This way, you can leave group_id as it is and add group field that is not managed (no annotations, not persisted to DB).

The UserService will have getGroup method, that will take User as an argument, then retrieve his group_id and use EntityManager (or indeed GroupService) to fetch the Group entity, if none was found, you will return null, otherwise you will set the returned Group to the unmanaged field of the entity, so you don't have to fetch it again next time.

2 Comments

This idea looks promising, although I am still mentaly more conviced to using some "Model-alike" wrappers (however "service" could be just another name for that). I'm trying this your idea out right now, Jan. I do not know yet how to easily & properly proxy User (as an entity) methods and properties to the UserService. Maybe you've got some idea for that too?
I would not proxy them. Entities should be mere holders of the data (wrapping it nicely, with methods, that can protest when trying to set something nasty, but that is it). That is why Doctrine 2 moved from Active Record pattern to Data Mapper and I consider that as a big improvement. That is also why using EntityManager in entities is considered as a bad practice - entities are supposed to be stupid objects. Services on the other hand can be clever and "at a hand", for this you need service container. Have a look at this: symfony.com/doc/current/book/service_container.html
1
+50

Considering your specific use-case, your best bet are the Doctrine event subscribers.

Please be careful as those are separate from the regular Symfony event subscribers. The latter are related to the generic Symfony workflow and the former is specific to Doctrine.

Specifically, the postLoad event is what you need. The idea here is to:

  1. Add a group property to User with corresponding typehinted getter and setter
  2. Create a custom Doctrine subscriber, called MyBundle\Entity\EventSubscriber\UserSubscriber
  3. Inject it with the Doctrine entity manager service
  4. Create the postLoad method like this:

    public function postLoad(LifecycleEventArgs $args) { $user = $args->getEntity(); if ($user instanceof User && $user->getGroupId()) { $r = $this->em->getRepository('MyBundle:Group'); $group = $r->find($user->getGroupId()); if ($group instanceof Group) { $user->setGroup($group); } // If not an instance of Group, no group can be found: do nothing and leave it NULL } } 

The postLoad method is called whenever a Doctrine entity is loaded from the manager, meaning all entities found through Doctrine will be populated with their group when possible.

If you want lazy loading... first, are you sure you want this? User groups are generally used in most pages in Symfony applications - in authorization checks, for instance. If groups are loaded at every page it really isn't worth it.

If you absolutely need this, you could use the Doctrine proxy factory service and populate your User with a proxy instead of an actual entity (not sure how this works exactly):

$group = $this->em->getProxyFactory()->getProxy('MyBundle:Group', $user->getGroupId()); $user->setGroup($group); 

In that case you would have to keep the following because some loading attempts will fail if the id does not exist:

/** * Get group * * @return \AppBundle\Entity\Group */ public function getGroup() { static $loaded = false; if (!$loaded) { try { $this->group; } catch (\Doctrine\ORM\EntityNotFoundException $e) { $this->group = null; } $loaded = true; } return $this->group; } 

I tweaked the original code of Jan Mares as at every call of getGroup() Doctrine would have attempted to load the Group.

1 Comment

Thank you Zephyr for your involvement. I will not be able to check your solution in next 12 hours when the bounty time is over, but it looks convincing. I was not aware of Doctrine event subscribers, which seems like a very powerfull tool to help with extra-ordinary situations. This is the kind of help I was looking for. My thanks go also to Jan Mares and Massimiliano Fedel. I will mark this answer as the correct one as soon I verify it (1-3 days).
0

OK, first, your User-Group relationship is not correctly defined. You absolutely need to add the ManyToOne relationship:

/** * @var \AppBundle\Entity\Group * * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Group") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="group_id", referencedColumnName="id") * }) */ private $group; 

Some word about this. In the JoinColumns clause, by default, the relationship is nullable, meaning you can have NULL instead of the foreign key in the resulting group_id column (the join column that Doctrine will create for you).

If you want to have a non-nullable relationship, the definition has to go like this instead:

/** * @var \AppBundle\Entity\Group * * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Group") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="group_id", referencedColumnName="id", nullable=false) * }) */ private $group; 

So, by default, you can have a NULL group for a User. Once this is properly defined, regarding your questions:

1.

If your application is properly created, you should never end up with a User related to a Group that has not yet been committed to the database. Every Group you tie to a User has to be taken from Doctrine somehow before persisting the User itself. Be it from within the application or from a form. If you need to validate that the Group exists before persisting the User, you have to tackle the problem upstream instead, by being 100% sure that any Group you use is taken from the database.

In this specific context, the setGroup setter should already ensure that what is provided is indeed a Group entity (if not you should add a typehint), and Doctrine already ensures that this entity already exists in the database. I agree that the Doctrine errors can get ugly, but a delivered product should not include any of those anyway.

Understand that Symfony validation is generally used with forms, with the intent of validating potential violations introduced by the end user, not by the developer (generally).

2.

With a properly defined relationship, as shown above, Doctrine will take care of this for you automatically. By default, all Doctrine relationships are loaded lazily.

Also:

/** * Get group * * @return \AppBundle\Entity\Group */ public function getGroup() { try { $group = $this->group; } catch (\Doctrine\ORM\EntityNotFoundException $e) { $group = null; } return $group; } 

You don't need to do this. Remove it.

8 Comments

Zephyr, thank you for your answer. This, though, could not be applied to my situation as I do try to build Symfony+Doctrine application based on database schema already defined some time ago. I can not (and will never can) ensure valid existing relation between User and Group (see my comment here). Users can have group_id relating to non-existing group and I can not change this.This is why I need some another, possibly more abstract approach.
Ok then, is your application in charge of adding User-Group content, or is it only reading it? Is this some kind of refactoring or are you using external data? You say above that you what you are actually trying to set up API-API communication, but Doctrine can be quite different from communicating with a (say) Rest API for instance.
My application is in charge of adding User content, and Groups are (say) read-only: there is fixed amount of groups that I can "bind" some Users to. Now, some more in-depth description: my app connects to external API, where it loads Users from, then it stores them in local DB. Other module of my app generates a report of all locally stored Users and presents it via, let's say, table on a HTML page. These users which can be connected with local groups are presented with some extra group-details. For others, group details are substituted with some dummy data.
To (hopefully) put some more light on the User-Group relation in my situation I have commited a schema.
It seems you have some control over the Group table. Would it not be possible to clean it? If not, what would you do with a User related to a non-existing Group? Would you allow the app to edit it anyway?
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.