inconsistent mapping in symfony - symfony

I have two entities, User and Notification. In each notification, there is a sender and receiver that are both User entities. But doctrine doesn't like it. The schema validation says:
The mappings ACME\CoreBundle\Entity\Notifications#sender and ACME\CoreBundle\Entity\User#notifications are inconsistent with each other.
Here are the mappings for both entities:
/**
* Notifications
*
* #ORM\Table(name="notifications")
*
*/
class Notifications
{
/**
* #ORM\ManyToOne(targetEntity="WD\UserBundle\Entity\User", inversedBy="notifications")
*/
protected $sender;
/**
* #ORM\ManyToOne(targetEntity="WD\UserBundle\Entity\User", inversedBy="notifications")
*/
protected $receiver;
}
And the User one:
/**
* User
*
* #ORM\Table(name="My_user")
*
*/
class User extends BaseUser
{
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="WD\CoreBundle\Entity\Notifications", mappedBy="receiver")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $notifications;
}
For readability reasons, I did not put the whole entities code, but I believe these should be enough info.
I believe the error comes from the fact I cannot put two 'mappedBy" values in User entity, but I'm not sure. And if it is, then I have no idea how to fix this.
I've found kinda similar cases on this website, but none that was exactly like mine (or I haven't found them).
Any idea how I could fix this?

I think the issue might be that you're having two properties (sender, receiver) and using the same column to map them. If you need to distinguish between sent and received, you'll need to have sender and receiver properties on Notification and then in your user have sentNotifications and receivedNotifications. You can combine them in an un-mapped method in your User if you do need to get everything together in one call such as:
/**
* #var Notification[]|ArrayCollection
*/
public function getAllNotifications()
{
return new ArrayCollection(
array_merge(
$this->sentNotifications->toArray(),
$this->receivedNotifications->toArray()
)
);
}

Related

Looping through entities and updating them causes error on flush

I am new to symfony and doctrine. And I am compeleting a code that someone else has started. I mainly have a form for which I wrote a validation function in my controller. In this form a BusReservation object along with its BusReservationDetails are created and saved to the db. so at the end of the form validation function, after the entities are saved in DB, I call a BusReservation Manager method which is transformBusReservationDetailIntoBusTicket which aim is to take each BusReservationDetail in the BusReservation oject and create a a new entity BusTicket based on it.
so I created this loop (please let me know if there is something wrong in my code so that i can write in a good syntax). I tried to put the 3 persist that you see at the end of the code but I got : Notice: Undefined index: 0000000..
I tried to merge (the last 3 lines in code ) I got the following :
A new entity was found through the relationship 'MyBundle\Entity\CustomInfo#busTicket' that was not configured to cascade persist operations for entity: . 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"}).
I got this same error when i commented all theh 6 lines of merge and flush.
PS: I am not expecting the flush to fully work. There are some properties that are nullable=false so I assume that I must set them as well so that the entities can be saved to DB. But the error i got is by far different than this.
PS : I noticed that there is a onFlush where the customInfo is updated and persisted again and other things happen, but i am trying to debug step by step. I tried to detach this event but still got the same errors. so I want to fix my code and make sure that the code part that i wrote in the manager is correct and if that's the case then I can move to debugging the event Listener. so please I would like to know if the following code is correct and why the flush is not working.
/**
* #param $idBusReservation
* #return bool
* #throws \Doctrine\ORM\NonUniqueResultException
*/
public function transformBusReservationIntoBusTicket($idBusReservation): bool
{ $result = "into the function";
/** #var BusReservation $busReservation */
$busReservation = $this->em->getRepository('MyBundle:BusReservation')->find($idBusReservation);
if ($busReservation !== null) {
/** #var BusReservationDetail $busReservationDetail */
foreach ($busReservation->getBusReservationDetails() as $busReservationDetail) {
$busTicket = new BusTicket($busReservationDetail->getBusModel(), $busReservation->getPassenger());
$busReservationDetail->setBusTicket($busTicket);
$busTicket->setBusReservationDetail($busReservationDetail);
$busTicket->setOwner($busreservation->getPassenger()->getName());
if ($busReservationDetail->getBusModel()->getCode() === 'VIPbus') {
// perform some logic .. later on
} else {
$customInfo = new CustomInfo();
$customInfo->setNumber(1551998);
// $customInfo->setCurrentMode(
// $this->em->getRepository('MyBundle:Mode')
// ->find(['code' => 'Working'])
// );
$customInfo->setBusTicket($busTicket);
// Bus ticket :
$busTicket->addCustomInfo($customInfo);
$busTicket->setComment($busReservation->getComment());
}
/** #var Mode $currentMode */
$currentMode = $this->em->getRepository('MyBundle:Mode')
->findOneBy(['code' => 'Working']);
$busTicket->setCurrentMode($currentMode);
// $this->em->merge($customInfo);
// $this->em->merge($busReservationDetail);
// $this->em->merge($busTicket);
// $this->em->persist($customInfo);
// $this->em->persist($busReservationDetail);
// $this->em->persist($busTicket);
}
$this->em->flush();
// $this->em->clear();
}
return $result;
}
// *************** In BusReservation.php ********************
/**
* #ORM\OneToMany(targetEntity="MyBundle\Entity\BusReservationDetail", mappedBy="busReservation")
*/
private $busReservationDetails;
/**
* Get busReservationDetails
*
*#return Collection
*/
public function getBusReservationDetails()
{
return $this->busReservationDetails;
}
// ---------------------------------------------------------------------
// *************** In BusReservationDetail.php ********************
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusReservation", inversedBy="busReservationDetails")
* #ORM\JoinColumn(name="id_bus_reservation", referencedColumnName="id_bus_reservation", nullable=false)
*/
private $busReservation;
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusModel")
* #ORM\JoinColumn(name="bus_model_code", referencedColumnName="bus_model_code", nullable=false)
*/
private $busModel;
/**
* #ORM\OneToOne(targetEntity="MyBundle\Entity\BusTicket", inversedBy="busReservationDetail", cascade={"merge","remove","persist"})
* #ORM\JoinColumn(name="id_bus_ticket", referencedColumnName="id_bus_ticket")
*/
private $busTicket;
/**
* #return BusModel
*/
public function getBusModel()
{
return $this->busModel;
}
//-------------------------------------------------------------------------
// ************ IN BusTicket.php *****************************
/**
* #ORM\OneToMany(targetEntity="MyBundle\Entity\CustomInfo", mappedBy="busTicket")
*/
private $customInfos;
/**
*
* #param CustomInfo $customInfo
*
* #return BusTicket
*/
public function addCustomInfot(CustomInfo $customInfo)
{
if (!$this->customInfos->contains($customInfo)) {
$this->customInfos[] = $customInfo;
}
return $this;
}
/**
* #ORM\OneToOne(targetEntity="MyBundle\Entity\busReservationDetail", mappedBy="busTicket")
*/
private $busReservationDetail;
// --------------------------------------------------------------------
// CUSTOMINFO ENTITY
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusTicket", inversedBy="customInfos")
* #ORM\JoinColumn(name="id_bus_ticket", referencedColumnName="id_bus_ticket", nullable=false)
*/
private $busTicket;
The answer is in your error message. You either have to add cascade={"persist"} to your entity annotation, or explicitly call persist. I don't believe you need em->merge() in this situation as you're never taking the entities out of context.
Where you have all your persist lines commented out, just try putting this in
$this->em->persist($busTicket);
$this->em->persist($busReservationDetail);
$this->em->persist($customInfo);
and if you're looping through a ton of entities, you could try adding the flush inside the loop at the end instead of a huge flush at the end.

How to properly use postUpdate, postRemove, postPersist in Doctrine?

The documentation says: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/events.html#postupdate-postremove-postpersist
The three post events are called inside EntityManager#flush(). Changes in here are not relevant to the persistence in the database, but you can use these events to alter non-persistable items, like non-mapped fields, logging or even associated classes that are not directly mapped by Doctrine.
So let's imagine I have an Image entity:
<?php
/**
* #ORM\Entity
*/
class Image
{
/**
* #var int|null
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* #var string
* #ORM\Column(type="string")
*/
private string $path;
/**
* #var string
* #ORM\Column(type="string")
*/
private string $status = 'RECEIVED';
}
Once I flush my Image entity I want to upload the corresponding file on a FTP server, so I do it in the postPersist event.
But if the upload on FTP fail I want to change the status of my Image to FTP_ERROR.
<?php
public function postPersist(Image $image, LifecycleEventArgs $event)
{
$em = $event->getEntityManager();
try {
$this->someService->uploadToFtp($image);
} catch (Exception $e) {
$image->setStatus('FTP_ERROR');
$em->persist($image);
$em->flush();
}
}
And it works, but as documentation says it's not a good way to do it.
I have seen this post: Flushing in postPersist possible or not? which says:
#iiirxs:
It is ok to call flush() on a PostPersist lifecycle callback in order to change a mapped property of your entity.
So how to do so? #iiirxs and the documentation say 2 different things.
The best way to download something on such event as postPersist is using https://symfony.com/doc/current/components/messenger.html
so, you can create asynchronous task for file uploading and Symfony Messenger will be able to handle errors and retry it automatically on fail, also you will be able to update it, status and etc in separate process and it will not depend on specific doctrine cases like case in your question.

How can I have two User that share the same table?

I have an entity User with lots of feature built for it.
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
}
This entity is good as is for most of my User.
However, a few of them will have a special ROLE, ROLE_TEACHER.
With this role, I need to store a lot of new variables specially for them.
If I create a new entity Teacher, doctrine creates a new table with every User's data + the Teacher's data.
/**
* #ORM\Entity(repositoryClass="App\Repository\TeacherRepository")
* #Table(name="teachers")
*/
class Teacher extends User
{
/**
* #ORM\Column(type="string", length=64, nullable=true)
*/
protected $test;
public function __construct()
{
parent::__construct();
}
}
What I want, is for Teacher & User to share the users table and have the teachers table only store the extra data. How could I achieve that ?
This is more of system design problem than implementation problem. as #Gary suggested you can make use of Inheritance Mapping which can have Performance issues, I'd rather suggest re think your schema and make use of database normalization techniques to break up your data into more manageable entities.
You can have User entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
/**
* One user has many attibute data. This is the inverse side.
* #OneToMany(targetEntity="UserData", mappedBy="data")
*/
private $data;
}
With other UserData Entity with OneToMany relationship :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserDataRepository")
* #Table(name="user_data")
*/
class UserData
{
/* variables + getter & setter */
#ORM\Id()
private $id;
/**
* Many features have one product. This is the owning side.
* #ManyToOne(targetEntity="User", inversedBy="data")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/**
* #ORM\Column(type="string")
*/
private $attribute;
/*
* #ORM\Column(name="value", type="object")
*/
private $value;
}
Now you can have list of user attributes without requiring specific structure to each role. It's scalable and arbitrary.
You can also define same Relation with TeacherData, StudentData or UserProfile Entities with foreign keys and branch your application logic according to the roles. Key is to break data into their separate domains and keep common data in one table. Load related data by querying related entity, this increases readability and makes it easy to break complex structure into manageable codebase.

Not valid Doctrine2 mapping: The entity-class 'Downloads' mapping is invalid

I am trying to define a OneToMany bidirectional (to avoid a ManyToMany and extra table) and I am doing (I think) as docs says here but certainly I am missing something since I am getting this error after run the doctrine:schema:validate command:
The association PlatformBundle\Entity\Downloads#identifier refers to the owning side field PlatformBundle\Entity\Identifier#downloads which does not exist.
This is how the entities looks like (just the relevant fields):
class Identifier
{
/*
* #var Downloads
* #ORM\ManyToOne(targetEntity="Downloads", inversedBy="identifier")
* #ORM\JoinColumn(name="downloads_id", referencedColumnName="id")
*/
protected $downloads;
}
class Downloads
{
/**
* #var Collection
* #ORM\OneToMany(targetEntity="Identifier", mappedBy="downloads")
*/
protected $identifier;
public function __construct() {
$this->identifier = new ArrayCollection();
}
}
This is a association where a download is assigned to many identifier. What I am doing wrong or missing here?
You are mising an * in Identifier Class :
/**<- this one could cost you many hours :P
* #var Downloads
* #ORM\ManyToOne(targetEntity="Downloads", inversedBy="identifier")
* #ORM\JoinColumn(name="downloads_id", referencedColumnName="id")
*/
protected $downloads;

An optional OneToOne relationship between entities?

I have a User Entity. This is considered the primary entity in this case and the mere fact it is being used means it is present.
The User entity, has a Store entity. But not all Users will necessarily have a Store entity.
It is worth noting that this is an existing database we are working with, and the id for the User table is the same as the id for the Store table. Name (id) and Value. It's just that in some cases, Store does not have a record for a given User id.
User:
class User extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Store")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $store;
...
}
Store:
class Store extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
...
}
This causes problems in the controllers. If a User entity does not have a Store record, it fails with a "Entity not found" exception. This can be dealt with using a try catch easy enough (I haven't been able to find a way to check if an Entity object exists or is just a proxy). If the User does have a store record, all is fine here.
But the big issue I have is especially the Fixtures:
protected function createUser($id)
{
$user = new User();
$user->setId($id);
$user->setEmail($id.'#example.com');
$user->setUserName($id.'_name');
$user->setArea($this->manager->find('Area', 156)); // Global
$this->manager->persist($user);
return $user;
}
When I run Fixtures, this fails. Giving me the error "Integrity constraint violation: 1048 Column 'id' cannot be null". This message disappears if I remove the store entity from User. So in a nutshell, I cannot add a user if it doesn't have a store.
Anyone know what's happening? I've done some looking around and I can't find anything, including doctrine docs, on having optional relationships between Entities. Which I thought would have been a common situation.
Found the solution to this on this doc page:
http://docs.doctrine-project.org/en/latest/tutorials/composite-primary-keys.html#use-case-3-join-table-with-metadata
In my case, rather than the User entity being associated with the Store entity using the id field, the store property in the User entity would be associated to the Store entity by user (an entity object). In return, the Store object will hold a User entity, which is annotated as the entity's id.
I'm sure that's as confusing as hell, so just look at the sample above. Below are my adjusted Entity classes:
User
class User extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Store", mappedBy="user")
*/
protected $store;
...
}
Store
class Store extends Entity
{
/**
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $user;
...
}
Now, if there is no Store record present for a given User, the store property in the User entity will be null. Fixtures runs as expected too.
In addition to the answer above, I also needed to add an inversedBy attribute. Otherwise, an invalid Entity mapping error will be thrown.
Using the entities above, the Store object would need to look like this:
class Store extends Entity
{
/**
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User", inversedBy="store")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $user;
...
}

Resources