doctrine 2 cascade persist saving too much - php

so this is my prePersist on EventListener
public function prePersist(LifecycleEventArgs $args)
{
//the first entity will have the PMP, so we catch it and continue to skip this if after this
if ($this->pmp == null) {
$this->pmp = $args->getEntity()->getPmp();
}
$taxonomicClass = $args->getEntity();
if($taxonomicClass instanceof TaxonomicClass){
if(is_null($taxonomicClass->getId())){
//here it says that i have created a new entity, need to persist it via cascade={"persist"}
$taxonomicClass->setPmp($this->pmp);
}
}
}
that's fine, i had added the annotation on it:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Pmp", cascade={"persist"})
* #ORM\JoinColumn(name="pmp_id", referencedColumnName="id", nullable=false)
**/
private $pmp;
and it saves everything from my hierarchy, even a new PMP, an object that already exist in the database!
what i want is that everything that im saving from my hierarchy needs to be related to the PMP that i passed, but when i set $taxonomicClass->setPmp($this->pmp); doctrine thinks that i created a new instance of PMP, since im not, i just want to this guy have an associaton with the PMP.
i tried put merge on the cascade option, but it only works with persist, how to make doctrine dont create a new instance, and instead use the one that i passed?

noticed my problem, i was assigning an attribute from memory, i should retrive him from database to doctrine understand.
public function prePersist(LifecycleEventArgs $args)
{
if ($this->pmp == null) {
$this->pmp = $args->getEntity()->getPmp();
}
$taxonomicClass = $args->getEntity();
if($taxonomicClass instanceof TaxonomicClass){
if(is_null($taxonomicClass->getId())){
//this solved the problem
$pmp = $args->getEntityManager()->getRepository("AppBundle:Pmp")->find($this->pmp->getId());
$taxonomicClass->setPmp($pmp);
}
}
}
i will keep in mind now that when a new entity is created, but it doesn't need to be saved, you must retrieve it from db, cascade={"persist"} wasn't even necessary

Related

flush not work after reset manager in doctrine

i have a error in my data and i get error out of range on a integer column and i try to prevent closed entity manager for proceeding work and for this purpose i reset manager in exception
public function renewDeliveryTime($delayReport) : void
{
try {
$this->delayReportRepository->updateRenewedDeliveryTimeAt($delayReport, 50000000);
}catch (\Exception $exception){
// out of range error
$this->managerRegistry->resetManager();
}
}
public function updateRenewedDeliveryTimeAt($delayReport,$delayDuration)
{
/**
* #var DelayReport $delayReport
*/
$delayReport->setDelayDuration($delayDuration);
$delayReport->setStatus(DelayReport::STATUS['DONE']);
$this->getEntityManager()->flush();
}
the problem is after i have another object and almost same operation in database but seems $this->getEntityManager()->flush() not work any more and nothing happens in database . it is related to $this->managerRegistry->resetManager()
public function enqueue($delayReport) : void
{
$this->pushInQueueReport($delayReport);
$this->delayReportRepository->updateStatus($delayReport, DelayReport::STATUS['IN_QUEUE']);
}
public function updateStatus($delayReport, $status)
{
/**
* #var DelayReport $delayReport
*/
$delayReport->setStatus($status);
$this->getEntityManager()->flush();
}
what is the problem and solution for this?
The problem with resetManager() is, that not all services wich has a reference to the entitymanager directly will be magically updated to have the new instance.
In updateStatus() Method you can easy check if your entity is Managed by the entity manager.
$uow = $this->getEntityManager()->getUnitOfWork();
if($uow->getEntityState($delayReport) !== UnitOfWork::STATE_MANAGED) {
// not managed
}
Dont know if a reassign can help here like $this->getEntityManager()->merge($delayReport).
BUT its really better to avoid a closed manager and validate your data before.
EDIT:
Not testet, if you will get the resetted EntityManager over the Registry. But its worth a try.
$entityManager = $managerRegistry->getManagerForClass(get_class($delayReport));

Save new Entity with relations on existed Entity (Symfony/Doctrine2)

I want to create new Entity SlideTranslation, and assign existed slide.
But every time entity had created without id of Slide. I can create SlideTranslation and than assign Slide to it, but it seems bad solution for me.
$slide = $em->getRepository('Model:Slide')->find($id);
if(isset($slide)) {
try {
$slideTranslation = new SlideTranslation();
$slideTranslation->setTranstable($slide);
$slideTranslation->setLocale('uk');
$slideTranslation->setAltText('Alt text');
$em->persist($slideTranslation);
$em->flush();
} catch (Exception $e) {
dump($e->getMessage());
}
}
Relations.
/**
* #ORM\ManyToOne(targetEntity="Model\Entity\Slide", inversedBy="tranlations")
* #ORM\JoinColumn(name="translatable_id", referencedColumnName="id")
*/
private $transtable;
I have tried method with getReference, but no result. Maybe I am breaking some patterns or principles and It's not possible in Doctrine2.
You will have to probably do it in the other way around
$slide = $em->getRepository('Model:Slide')->find($id);
$slideTranslation = new SlideTranslation();
$slideTranslation->setLocale('uk');
$slideTranslation->setAltText('Alt text');
$slide->addTranslation($slideTranslation);
$em->flush();
Then add cascade to the Slide entity, and you don't even need to persist the entity Translation
/**
* #ORM\OneToMany(targetEntity="Model\Entity\SlideTranslation", mappedBy="transtable", cascade={"persist", "remove"})
*/
private $translations;

Moving entities across collections in Doctrine

This question is a bit related, to my other question here
ArrayCollection ( Doctrine ) contains function returns incorrect results , so if anyone wants more information about my problem, it is there, although this is not strictly the same problem.
Now, to the point, I have a FileSystemFolder that models a folder, and a FileProxy that models a File, FileSystemFolder contains an ArrayCollection of FileProxy, I also have a FileManager, that performs a move operation, something that you would expect to have on any FileSystem. This move operation takes two FileSystemFolder and a FileProxy, removes this FileProxy from one FileSystemFolder and adds it into another FileSystemFolder.
Here is this function, in all its glory:
public function moveFileProxy(FileSystemFolder $from, FileSystemFolder $to, FileProxy $proxy, $force = false)
{
if (!$this->checkFolder($from))
{
return array('type' => 'error', 'message' => 'Cannot move from this folder.');
}
if (!$force)
{
if (!$this->checkFolder($to))
{
return array('type' => 'error', 'message' => 'Cannot move to this folder.');
}
}
/*$return = "";
foreach($from->getFiles() as $file)
{
$return .= $file->getFilename() . " --- ";
}
if(!$from->getFiles()->contains($proxy))
{
return array('type' => 'error', 'message' => 'Folder '.$from->getName().' does not contain '.$proxy->getFilename(). ' All files from this folder '. $return);
}
if($to->getFiles()->contains($proxy))
{
return array('type' => 'error', 'message' => 'Folder '.$to->getName().' already contains '.$proxy->getFilename());
}*/
$from->removeFile($proxy);
$to->addFile($proxy);
return array('type' => 'pass', 'message' => 'Operation Successful.');
}
The commented out piece of code belongs to the other question. Checks do not matter.
For the sake of this question, I'll copy one piece of information from that other question, because it is important, here it is:
/**
* #ORM\ManyToMany(targetEntity="FileProxy", fetch="EXTRA_LAZY")
* #ORM\JoinTable(name="file_system_folders_files",
* joinColumns={#ORM\JoinColumn(name="file_system_folder_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="proxy_id", referencedColumnName="id", unique=true)})
*/
protected $files;
and this:
/**
* Add proxy
*
* #param FileProxy $proxy
* #return FileSystemFolder
*/
public function addFile(FileProxy $proxy)
{
if(!$this->getFiles()->contains($proxy))
{
$this->getFiles()->add($proxy);
}
return $this;
}
/**
* Remove proxy
*
* #param FileProxy $proxy
* #return FileSystemFolder
*/
public function removeFile(FileProxy $proxy)
{
if($this->getFiles()->contains($proxy))
{
$this->getFiles()->removeElement($proxy);
}
return $this;
}
Now, these functions are exteremely straightforward, they really do not do anything fancy, exactly what their names tell, and the funny thing is,
the moveFileProxy function works perfectly fine ( I persist both folders out of this function, which I think is a good practice ).
The question is: Should this function actually work ? As you can see, the $files variable, which is an ArrayCollection does not cascade persist, it doesn't cascade anything, yet this function still works.
Here is a piece of code showing the whole process:
$result = $fm->moveFileProxy($origin, $destination, $upload);
if($result['type'] === 'error')
{
return JsonResponse::create($result);
}
$em = $this->getDoctrine()->getManager();
$em->persist($origin);
$em->persist($destination);
$em->flush();
But it works just most of the time, it does not throw errors most of the time, but sometimes, on some weird occassions, it throws "Duplicate entry exception" which is also weird, because this is after all a move operation.
If you know anything about this problem, please help me, #symfony and #doctrine IRCs are really awful at helping.
Thanks.
It would seem, that I discovered what might be the answer to this question ( completely by accident ), bare in mind, that I am not entirely sure, and I do not think it is documented in Doctrine Documentation. It might be buried somewhere in the code, and I might look that up later to prove this theory.
My theory is:
Unidirectional Many-To-Many associations create an additional table, so that the association is composed of 3 tables, with no actual Many-To-Many association, the 2 tables are obviously the ones you used to form the association, but the third one is important.
The third table is created by Doctrine on the spot, and even though we specify the properties for the columns with #JoinColumn, I think Doctrine actually modyfies them further to cascade persist, because it would make no real sense otherwise.
I am also pretty sure, that it actually cascades all, but like I said, it's just a theory.

DDD - Entity creation business rule validation

I have a business rule such as this :
If a JobSeeker wants to apply to a Vacancy, make sure that the Resume used in application is completed and that JobSeeker hadn't
applied to that Vacancy already. If the condition is satisfied, then
the application will be written in a JobApplicatiion.
This is what I came up with :
JobSeeker.php
class JobSeeker {
private $applications;
private $resume;
/** Other irrelevant props **/
public function apply(Vacancy $vacancy, Resume $resume) {
// Business rule #1
if(!$resume->isCompleted()) throw new \Exception('Resume '.$resume->getTitle().' is incomplete.');
// Business rule #2
$alreadyApplied = array_filter($this->applications->toArray(), function(JobApplication $application) use($vacancy) {
return $application->getVacancy() === $vacancy;
});
if($alreadyApplied) throw new \Exception('Vacancy '.$vacancy->getTitle().' is already applied');
// If both rules passed, then create a JobApplication
$application = new JobApplication($this, $vacancy, $resume);
$this->applications->add($application);
return $application;
}
}
JobApplication.php
class JobApplication {
private $applicant;
private $vacancy;
private $resume;
public function __construct(JobSeeker $applicant, Vacancy $vacancy, Resume $resume) {
$this->applicant = $applicant;
$this->vacancy = $vacancy;
$this->resume = $resume;
}
}
If I was to expect that everyone would just use
$jobApplication = $jobSeeker->apply($vacancy, $jobSeeker->getResume());
Then there's no problem.
The problem arise when someone do this
$jobApplication = new JobApplication($jobSeeker, $vacancy, $resume);
The second example will bypass the business rule validation.
It did occurred to me to separate the rule checking to a different method :
JobSeeker.php
class JobSeeker {
public function canApply() {
// Here goes those 2 business rules mentioned
}
public function apply(Vacancy $vacancy, Resume $resume) {
if($this->canApply($vacancy, $resume)) {
return new JobApplication($this, $vacancy, $resume);
}
}
}
JobApplication.php
class JobApplication {
public function __construct(JobSeeker $jobSeeker, Vacancy $vacancy, Resume $resume) {
if($jobSeeker->canApply($vacancy, $resume)) {
// Same as before
}
}
}
While the second approach guarantees the business rule constraint, it's very redundant and still does not provides the expected result.
$jobApplication = new JobApplication($jobSeeker, $vacancy, $resume);
I need an insight in this.
Thanks !
Depending how you do it you have 2 aggregate roots as I see it
JobSeeker
Vacancy
Resume is an like a profile for an user
Well DDD likes to uses services, for almost everything.
So we have the JobSeekerApplicaitonService this services will be used for the external world.
On the JobSeekerApplicaitonService I would add the method apply
public function apply(JobSeeker $jobSeeker, Vacancy $vacancy);
First we check if the bussiness rules are met.
ie.
$jobSeeker->getResume()->isCompleted();
This check throws an error if it is not completed.
Next we make another function at the JobSeekerApplicaitonService which checks if an JobSeeker already has applied, can also be used for the view to let the user already see he has applied for example.
public function hasApplied(JobSeeker $jobSeeker, Vacancy $vacancy);
But this method can now be used in our apply function
$this->hasApplied($jobSeeker, $vacancy);
Again throw an exception when already applied.
You can now savely reutrn the new JobApplication. Although I would say the JobSeekerApplicaitonService repository and create it there, so it is saved in the db because that is what an application service is, a delegator.
Code
class JobSeekerApplicaitonService {
public function apply(JobSeeker $jobSeeker, Vacancy $vacancy) {
if ($jobSeeker->getResume()->isCompleted()) {
// throw exception
} elseif ($this->hasApplied($jobSeeker, $vacancy)) {
// throw exception
}
// save logic or something else you want
}
public function hasApplied(JobSeeker $jobSeeker, Vacancy $vacancy) {
// your check, I would now use the JobApplicationRepository
return false;
}
}
Your first examples of JobSeeker & JobApplication are correct. The JobSeeker.apply method is acting as the factory for JobApplications:
job_application = job_seeker.apply(vacancy, resume)
Looks good.
The following statement, however, doesn't make much sense:
$jobApplication = new JobApplication($jobSeeker, $vacancy, $resume);
Considering the real world, have you ever seen a JobApplication randomly burst into existence out of thin air and land on your desk? I haven't :) In most cases, one entity is created from another:
employee = company.hire(person)
invoice = employee.create_invoice(customer, invoice_terms)
etc...
If you see an entity being 'new-ed' in a command handler, it should raise an eyebrow.

Doctrine won't add persist entity relationship

I have two entities, View and Location
Each View can have a Location.
In my view I thus have:
class View
{
//..... Other Stuff.....
/**
* #ManyToOne(targetEntity="Location", inversedBy="views")
**/
private $location;
//...setters and getters....
public function setLocation($location){
$this->location = $location;
}
}
and then for my Location
class Location
{
//.....other stuff.....
/**
* #OneToMany(targetEntity="View", mappedBy="location")
**/
private $views;
public function __construct() {
$this->created = $this->updated = new \DateTime("now");
$this->views = new \Doctrine\Common\Collections\ArrayCollection();
}
// .... Getters and Setters ....
}
But when I try and do this:
<?php
$this->pageview = $this->em->getRepository('Entities\View')->find(1);
$this->location = $this->em->getRepository('Entities\Location')->find(1);
$this->pageview->setLocation($this->location);
$this->em->persist($this->pageview);
$this->em->flush();
?>
Or even when I create new entities:
<?php
$pv = new Entities\Pageview;
$lc = new Entities\Location;
$this->em->persist($lc);
$this->em->flush();
$pv->setLocation($lc);
$this->em->persist($pv);
$this->em->flush();
?>
Doctrine never sets the location_id in the database (it is always NULL).
I've checked the SQL queries and they're not even being attempted at being set, all I'm getting is:
INSERT INTO View (field1, field2, created, updated) VALUES ('val1', 'val2', '2013-07-17T12:10:56+01:00', '2013-07-17T12:10:56+01:00')
No reference to locations whatsoever...The weird thing is I can update field1 and field2 fine...and all other relations are working throughout my application...I just can't get views and locations to work...
EDIT
I have the exact some code working now on another computer. I don't know why it wasn't working, but I just moved the files back and restarted my computer and now it is...cacheing problem I guess?
Restarted my computer and the problem got solved...I don't know why it was going wrong!
Maybe something to do with caches or proxies...I dunno...
You could try explicitly referencing the correct columns that Doctrine needs to do a join on.
/**
* #ManyToOne(targetEntity="Location")
* #JoinColumn(name="location_id", referencedColumnName="id")
*/
private $location;
Also, in this example:
$this->pageview = $this->em->getRepository('Entities\View')->find(1);
$this->location = $this->em->getRepository('Entities\Location')->find(1);
$this->pageview->setLocation($this->location);
$this->em->persist($this->pageview);
$this->em->flush();
You do not need to persist the entity if you are just updating the existing data.
I think you need load the view in the location. So you must create a method in your Location entity like this:
public function getViews() {
return $this->views;
}
and then to persist into database, do this:
$location = new Entity\Location();
$view = new Entity\View();
$location->getViews()->add($view);
$this->em->persist($location)
$view->setLocation($location);
$this->em->persist($view);
$this->em->flush();
This is related to the Doctrine ORM cache drivers:
doctrine:
orm:
entity_managers:
default:
metadata_cache_driver: apcu
result_cache_driver: apcu
query_cache_driver: apcu
We used APCu to even on DEV do caching, clearing APCu (by restarting Apache) did the trick.

Categories