ManyToOne association for zfcuser doctrine entity with bjyauthorize - php

I'm using the zfcuser doctrine module for my zf2 project with bjyauthorize, which is working quite fine. Now I'd like to get the connected Country entity out of my User entity
in User.php:
/**
* An example of how to implement a role aware user entity.
*
* #ORM\Entity
* #ORM\Table(name="users", indexes={
* #ORM\Index(name="fk_User_Country1_idx", columns={"Country_id"}),
* }, uniqueConstraints={#ORM\UniqueConstraint(name="email_UNIQUE", columns={"email"})})
* #ORM\HasLifecycleCallbacks()
*/
class User implements UserInterface, ProviderInterface
{
...
/**
* #var ersEntity\Country
* #ORM\ManyToOne(targetEntity="Country", inversedBy="users")
* #ORM\JoinColumn(name="Country_id", referencedColumnName="id")
*/
protected $country;
in Country.php
/**
* Entity\Country
*
* #ORM\Entity()
* #ORM\Table(name="Country")
* #ORM\HasLifecycleCallbacks()
*/
class Country implements InputFilterAwareInterface
{
...
/**
* #ORM\OneToMany(targetEntity="User", mappedBy="country")
* #ORM\JoinColumn(name="id", referencedColumnName="Country_id")
*/
protected $users;
A simple testAction in one of my Controllers fails:
$user = $em->getRepository("ersEntity\Entity\User")
->findOneBy(array('id' => 1));
error_log($user->getFirstname().' '.$user->getSurname());
error_log('country: '.$user->getCountry()->getName());
which results in:
[Tue May 19 00:02:44 2015] [error] [client 185.17.207.16] Andi N.
[Tue May 19 00:02:44 2015] [error] [client 185.17.207.16] PHP Fatal error: Call to a member function getName() on a non-object in /home/ers/www/ers/module/Admin/src/Admin/Controller/TestController.php on line 172
I'm wondering why it's not possible to get the Country entity from the User entity. With other entities in this same project this is working fine.
Can somebody tell me what needs to be done to be able to get the Country entity out of this zfcuser-bjyauthorize-doctrine User entity?
For more code info, this whole project is available at https://github.com/inbaz/ers in the develop branch.
EDIT:
With users who have no country set there needs to be an error. It's right that there is the need to check if a country exists. But this user has a country set. I checked that via phpmyadmin. It's not possible to get this country entity via the getCountry() method.
Maybe this is cause the deserializing and serializing of the doctrine entity into the session. I checked the doctrine documentation on how to save entities into the session. But I'd like to keep all subentities in the session, so in my case I have a order entity in the session which holds multiple package entities. Each package entity has one user and multiple item entities. When getting the order entity back from the session I'd like to be able to access all these elements.
I even tried to do a merge on each user in the session like:
foreach($participants as $participant) {
$participant = $em->merge($participant);
}
but that doesn't change anything. Even a merge on the whole order was not successful.
Do you have an idea on how to get the doctrine entities back from the session with the full doctrine features?

It looks all fine to me. At first I thought your getCountry method was missing from your entity definition but I see it is there.
My guess is that user with id 1 does not have a Country set. Go to your table (phpmyadmin or whatever) and check the value of the Country_id column.
If you want each user to have a country you can insure your data integrity by adding nullable=false to your join column definition according to the Doctrine2 #JoinColumn specs.
Otherwise (if $country is allowed to be null) you should maybe first check if country is an object before pulling getName():
$countryName = null;
$country = $user->getCountry();
if(is_object($country)){
$countryName = $country->getName();
}
/** #var null|string $countryName */
$countryName //... use your country name which is null or string
One more thing, I would strongly suggest to use only lowercase strings for table and column names. Using uppercase could get you into trouble.
EDIT
I took a closer look and see now that your mappings are wrong. Did you use doctrine to validate your schema? There is a #ORM\JoinColumn on the inverse side of the relationship (inside Country.php). The inverse side does not need this #ORM\JoinColumn declariation. You should remove this line. The owning side is User so the join column definition should be only declared there.
It should be like this:
Country.php:
/**
* #var Collection
* #ORM\OneToMany(targetEntity="User", mappedBy="country")
*/
protected $users;
User.php:
/**
* #var Country
* #ORM\ManyToOne(targetEntity="Country", inversedBy="users")
* #ORM\JoinColumn(name="Country_id", referencedColumnName="id")
*/
protected $country;

Doctrine 2: Can entities be saved into sessions?
This is the answer to my question.
if a lazy-loaded entity is not loaded at serialization time, it won't be loadable after de-serialization. So you have to make sure the entity is fully loaded before serializing it.
There is still the question open if it's not possible to create a new doctrine entity and get an array copy of the de-serialized one and populate this data into the new entity.

Related

Doctrine in Symfony: use a single “Author” associative entity related to different entities

I'm developing a custom content management system with Symfony 5 and Doctrine.
I'm trying to implement a relation between the entities Document and Video (actually there are many more, but for simplicity sake let's say are just two) and the User entity.
The relation represent the User who wrote the document or recorded the video. So the relation here is called Author. Each document or video can have one or more author. Each User can have none or more document or video.
I would like to use just a single associative Author associative entity, like this:
entity_id|author_id|entity
Where:
entity_id: is the id of the document or video
author_id: is the user_id who authored the entity
entity: is a constant like document or video to know to which entity the relation refer to
The problem is that I cannot understand how to build this in Doctrine. Was this a classic SingleEntity<-->Author<-->Users relationship I would have build it as a ManyToMany item, but here it's different.
Author would probably contain two ManyToOne relations (one with the User entity and one with either the Document or the Video entity) plus the entity type field, but I really don't know how to code the "DocumentorVideo`" part. I mean:
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=??????????, inversedBy="authors")
* #ORM\JoinColumn(nullable=false)
*/
private $entity; // Document or Video
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="articles")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\Column(type="smallint")
*/
private $entityType;
How should I manage the first field?
Don't know if would be better to store it under two differents attributes. If not and mandatory, I think those "objects" should have a common interface or something, so take a look to doctrine inheritance that should fulfill your needs
My suggestion is to store the entity namespace Ex. Acme\Entity\Document in a property and the id in another and to use the entity manager to get the entity.
Edit: Though you won't have the relation, I prefer that way over others because it is reusable and the performance is rather the same. Also if I need to pass it to a JSON response, I just create a normalizer and I am good to go.
/**
* #ORM\Column(type="string")
*/
private $entityNamespace;
/**
* #ORM\Column(type="integer")
*/
private $entityId;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getEntity()
{
return $this->em->getRepository($this->entityNamespace)->find($this->entityId);
}

Doctrine launches INSERT always instead of UPDATE for existing entities

Let's say I have the following entities:
App\Entity\MainEntity:
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType1FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType1;
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType2FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType2;
Basically, one-directional 1:1 relationship from main entity to the same dependent entity using two different columns in the main entity table.
It doesn't matter, whether I use fetch="EAGER" or normal lazy loading through Doctrine proxy classes, when I do something like this:
$mainEntity = $this->mainEntityRepository->find(74);
$mainEntity->setDependentEntityType1($this->dependentEntityRepository->find(35));
$this->mainEntityRepository->saveTest($mainEntity);
where ::saveTest() is:
public function saveTest(MainEntity $mainEntity) {
$this->_em->persist($mainEntity->getDependentEntityType1());
$this->_em->merge($mainEntity);
$this->_em->flush();
}
it always tries to INSERT a new dependent entity to the table, even though I never made any changes (and even if I made them, it should have been UPDATE! for it)
The question is: why does Doctrine decide this dependent entity is a new one if I did $this->dependentEntityRepository->find(35) , so loaded an existing one?
I tried fetch="EAGER" thinking that spl_object_hash might return different hashes for a Proxy class instance and the actual DependantEntity one, but it doesn't matter, the DependantEntity is for some reason always considered as "new".
UPDATE: here is the code of ::setDependentEntityType1()
public function setDependentEntityType1(DependentEntity $dependentEntity) : void {
$this->dependentEntity = $dependentEntity;
}

How to restrict associations to a subset of another association in Doctrine?

I got a bit stuck with multiple mappings of the same object in Doctrine. The app is build on Symfony btw, hence the slightly different annotations.
Basically I have the following objects:
Organisation: an umbrella holding attributes about an organisation
Department: a department within the organisation
User: a generic user object
Those objects are related as follows:
An organisation always has one and only one owner, which is a User
An organisation has many members, which are all User's
A department consists of many User's, but only members of the Organisation the Department is a part of are allowed
I'm a bit stuck at the third requirement... First of all, this is how my objects more or less look like atm:
/**
* #ORM\Entity
* #ORM\Table(name="organisations")
*/
class Organisation
{
// ...
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="organisation")
*/
private $owner;
/**
* ORM\OneToMany(targetEntity="User", mappedBy="organisation")
*/
private $members
}
/**
* #ORM\Entity
* #ORM\Table(name="departments")
*/
class Department
{
// ...
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="departments")
*/
private $members
/**
* #ORM\ManyToOne(targetEntity="Organisation", inversedBy="departments")
*/
private $organisation;
}
/**
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User
{
// ...
/**
* The organisation this user "owns"
*
* #ORM\OneToOne(targetEntity="Organisation", mappedBy="owner", nullable=true)
*/
private $owning_organisation;
/**
* #ORM\ManyToOne(targetEntity="Organisation", inversedBy="members")
*/
private $organisations;
/**
* #ORM\ManyToMany(targetEntity="Department", inversedBy="members")
* #ORM\JoinTable(name="users_departments")
*/
private $departments;
}
Now this basically works, if and only of in the controllers I do all the checking (something like (if( $user->isPartOfOrganisation($department-getOrganisation()) { $department->addMember($user); }).
But is there a way to restrict possible object associations on design level? So basically what I want is that if a user is added to a department, it is solely possible if the user is already part of the organisation the department is also a part of. Or should I do the check in the addMember() method of the Department object? I can imagine (but cannot find it) that there is some kind of a subset-restriction (Department::members is subset of Organisation::members).
To implements this check low-level as possible (nearest to the db) I think the only solution is a Doctrine Event Listener that in the pre-persist event check for your custom constraint. Read more about Doctrine Event System .
BTW I think you can manage this situation in a more simply manner: I suggest you to incapsulate the business logic into a service (so you can reuse it more simply) and use it in a custom validator that you will use in the form where you manage this situation.
Let me know if you need more tips to develop one of this solutions or if you found something more useful.
Hope this help

Symfony Association Mapping OneToOne and OneToMany to Same Entity

I have a View entity that represents the primary page record, and then I have an associated entity called ViewVersion which stores multiple versions of the entity as it's changed over time. The View entity sets the current "Published" ViewVersion in the VersionId field. This makes for a simple OneToOne association. But in some contexts I will also want to get all the versions associated with this View entity, e.g. if I want to allow the user to review older versions and revert back. So I will need another mapping which is a OneToMany. The first viewVersion will map to the active "published" version, and the second viewVersions will show all the versions.
Entity Definitions
/**
* #ORM\Entity
* #ORM\Table(name="view")
* #ORM\Entity(repositoryClass="Gutensite\CmsBundle\Entity\View\ViewRepository")
*/
class View extends Entity\Base {
/**
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", inversedBy="view", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\JoinColumn(name="versionId", referencedColumnName="id")
*/
protected $viewVersion;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $versionId = NULL;
/**
* #ORM\OneToMany(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", mappedBy="viewAll", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $viewVersions;
}
/**
* #ORM\Entity
* #ORM\Table(name="view_version")
* #ORM\Entity(repositoryClass="Gutensite\CmsBundle\Entity\View\ViewVersionRepository")
*/
class ViewVersion extends Entity\Base {
/**
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", mappedBy="viewVersion", cascade={"persist"})
*/
protected $view;
/**
* #ORM\ManyToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", inversedBy="viewVersions")
* #ORM\JoinColumn(name="viewId", referencedColumnName="id")
*/
protected $viewAll;
/**
* The primary view entity that this version belongs to.
* #ORM\Column(type="integer", nullable=true)
*/
protected $viewId;
}
This "works" but is it recommended to have two associations with the same entity like this? Or is this a really bad idea?
The ViewVersion entity will reference a single View entity in both cases, but the mapped associations need two separate variables, e.g. View and ViewAll. I'm not exactly sure how the internals work for the association, and how the reference variable with the mapping is used.
Alternatively, I could get rid of the OneToOne association, and just set a ViewRepository function to get the current published version based on the versionId (just like the old mapped entity used to do with the getVersion()). That would work, but is it more internal overhead, because it would make two queries... or will Doctrine be smart enough to optimize this, just like it did with the getVersion().
NOTE:
These other answers are not complete.
References:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html
http://doctrine-orm.readthedocs.org/en/2.0.x/reference/association-mapping.html#one-to-many-bidirectional
Typically, I have found the best approach is to solve this in a different way.
One common pattern I have seen before is you use a single table to hold all records, and have an 'active' flag.
If your query to select the active one works like so:
SELECT * FROM table WHERE active = true ORDER BY updated_at DESC LIMIT 1;
Then enabling a new one becomes as simple as:
UPDATE table SET active = 1, updated_at = '<timestamp>' WHERE id = <new id>;
UPDATE table SET active = 0, updated_at = '<timestamp>' WHERE id = <old id>;
Your new page will be active as soon as the first query hits, and your second query will avoid any sort of weirdness as that row will already be no longer active.
If you have other models that depend on a consistent ID to reference, then another route which also maintains some sanity would be to have one table for the active entries (in whole, not in part) and then a second table with additional metadata to track versions.
The latter approach could be nicely handled via Doctrine's inheritance system (http://docs.doctrine-project.org/en/2.0.x/reference/inheritance-mapping.html) which would let you define the base View class, and then for the "ViewRevision" model, extend View and add a "Revised on" type timestamp.
Per the advice from #jmather I've decided this model is "okay", because I need a single View entity that other entities can access (e.g. Routing urls that point to a single View, i.e. "page").
I've changed the OneToOne relationship for View to be unidirectional only, because the ViewVersion already has an association back to the View via the other OneToMany (so it doesn't need two paths back).
This allows me to keep a simple method for $view->getPublished() handy and seems more logical.
/**
* #ORM\Entity
* #ORM\Table(name="view")
*/
class View extends Entity\Base {
/**
* This is a OneToOne Unidirectional association, just so that we can get the
* current published version easily, based on the publishedId.
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\TestVersion")
* #ORM\JoinColumn(name="publishedId", referencedColumnName="id")
*/
protected $published;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $publishedId = NULL;
/**
* This is the regular OneToMany Bi-Directional Association, for all the versions.
* #ORM\OneToMany(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", mappedBy="view", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $versions;
}
/**
* #ORM\Entity
* #ORM\Table(name="view_version")
*/
class ViewVersion extends Entity\Base {
/**
* #ORM\ManyToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", inversedBy="versions")
* #ORM\JoinColumn(name="viewId", referencedColumnName="id")
*/
protected $view;
/**
* The primary view entity that this version belongs to.
* #ORM\Column(type="integer", nullable=true)
*/
protected $viewId;
}
However, I've discovered that as long as the $view->publishedId is set the view can't be deleted from the database because of foreign key constraints (even though it's uni-directional). So I have to break that foreign key link before removing. I think that's fine. I posted details about that here: Overlapping Entity Association causing Database Foreign Key Constraint Errors when Removing Entity

Doctrine 2, error inserting in table with foreign key

I'm very new using Doctrine, is the first project I work with it and I'm having an error while I try to insert a new user.
The thing is I've got a class User with a foreign key Country and when I try to insert a user Doctrine also try to insert the country, the country already exists so PDO launch an integrity constraint violation and Doctrine a Doctrine\DBAL\DBALException.
I know the annotation cascade={"persist"} makes the country entity to be written in the db, without it, doctrine launch another error:
A new entity was found through the relationship 'User#country' that was not configured to cascade persist operations for entity: Country#0000000078b1861f00007f935266d9fe. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Country#__toString()' to get a clue.
I've tried with all cascade options and only with persist and all the error above doesn't come up...
Is there something like cascade={"no-persist"} or something that tells doctrine the value of this attribute must be already inserted in table country???
Some code:
/**
* User
*
* #Table(name="user")
* #Entity
*/
class User {
...
/**
* #var Country
*
* #OneToOne(targetEntity="Country", cascade={"persist"})
* #JoinColumn(name="country", referencedColumnName="id")
*/
private $country
...
}
/**
* Country
*
* #Table(name="country")
* #Entity
*/
class Country {
...
/**
* #var integer
*
* #Column(name="id", type="integer")
* #Id
*/
private $id;
}
Any clue will be highly appreciated.
Thanks.
Put the cascade=persist back in.
You need to check the database to see if the country exists. Your insert with an existing country fails because the country object needs to be managed by the entity manager.
$country = $countryRepository->find($countryId);
if (!$country)
{
$country = new Country();
$entityManager->persist($country);
}
$user->setCountry($country);

Categories