简体   繁体   中英

Doctrine doesn't save data in my OneToMany association

I use Symfony 4 with Sonata Admin. I have One Project To Many News association. And I found a problem trying in my Project admin page add some news and update the Project. The problem is News have not been added to the Project. And then I solved this problem by adding this code into my ProjectsAdmin.php file:

 public function prePersist($project)
    {
        $this->preUpdate($project);
    }

    public function preUpdate($project)
    {
        $project->setNews($project->getNews());
    }

But there is still some problems. The second one is I can't delete News from Project, after clicking update button nothing happens. Sure if I use 'orphanRemoval=true' in my Projects Entity for field news it would work but it would delete News that I only want to remove from Project. How can I fix this problem?

And the last but not least I have PreUpdate event listener that checks: if I update Project entity than add to this Project all News. The thing is it doesn't work when I do it for Projects Entity but when I do the same for News Entity it works. I forgot to mention that it is so similar with my problems in admin panel, because when I go to News Admin and there try to add Project to News it works without any fixes and when I try to delete Project from News in News Admin it also works as expected. So on inversedBy side everything works but on mappedBy side I have problems.

Here is my Event Listener:

 public function PreUpdate(LifecycleEventArgs $args): void {
        $entity = $args->getEntity();
        $newsRepo = $args->getEntityManager()->getRepository(News::class);
        if ($entity instanceof Projects) {
            foreach ($newsRepo as $new){
                $news = $args->getEntityManager()->getReference(News::class, $new->getId());
                $entity->setNews($news);
            }
        }
    }

My Projects Entity:

/**
 * @ORM\Entity(repositoryClass=ProjectsRepository::class)
 * @ORM\HasLifecycleCallbacks()
 */
class Projects {
   /**
     * @ORM\OneToMany(targetEntity=News::class, mappedBy="project", orphanRemoval=true)
     */
    private $news;

    public function __construct() {
        $this->news = new ArrayCollection();
    }
  
      /**
     * @return Collection|News[]
     */
    public function getNews(): Collection {
        return $this->news;
    }

    /**
     * @param mixed $news
     * @return Projects
     */
    public function setNews($news) {
        if (count($news) > 0) {
            foreach ($news as $i) {
                $this->addNews($i);
            }
        }
        return $this;
    }

    /**
     * @param News $news
     */
    public function addNews(News $news) {
        $news->setProject($this);
        $this->news->add($news);
    }

    /**
     * @param News $news
     */
    public function removeNews(News $news) {
        $this->news->removeElement($news);
    }
}

News Entity:

/**
 * @ORM\Entity(repositoryClass="App\Repository\NewsRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class News {
    /**
     * @ORM\ManyToOne(targetEntity=Projects::class, inversedBy="news")
     * @ORM\JoinColumn(nullable=true)
     */
    private $project;

    public function getProject(): ?Projects {
        return $this->project;
    }

    public function setProject(?Projects $project): self {
        $this->project = $project;

        return $this;
    }
}

Projects Repository:

/**
 * @method Projects|null find($id, $lockMode = null, $lockVersion = null)
 * @method Projects|null findOneBy(array $criteria, array $orderBy = null)
 * @method Projects[]    findAll()
 * @method Projects[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class ProjectsRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Projects::class);
    }
}

News Repository:

/**
 * @method News|null find($id, $lockMode = null, $lockVersion = null)
 * @method News|null findOneBy(array $criteria, array $orderBy = null)
 * @method News[]    findAll()
 * @method News[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class NewsRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, News::class);
    }
}

First of all your ProjectsAdmin:prePersist and ProjectsAdmin:preUpdate methods do not appear to accomplish anything. Second, your event listener does not look right either. I do not believe any of those bits of code are necessary as you should be able to solve this at the entity level.

The reason everything works as expected in the NewsAdmin is that News is the owning side of the relation, so when you edit the relation from the Projects side you must also call the corresponding methods on the News side. Also, you say you are able to successfully add News items to Projects but cannot remove them so, let's compare your Projects:addNews method with your Projects:removeNews method. You will see the working Projects:addNews method affects change on the owning News side, while the non-working Projects:removeNews method only acts on the Projects side. You should only need to modify Projects:removeNews to set the News:project property to null .

/**
 * @param News $news
 */
public function addNews(News $news) {
    // next line makes this work
    $news->setProject($this);
    $this->news->add($news);
}

/**
 * @param News $news
 */
public function removeNews(News $news) {
    // make this work the same way with next line
    $news->setProject(null);
    $this->news->removeElement($news);
}

Well, after hours of researching I finally found a solution for one of my problems. But firstly I should explain why exactly I use that piece of code in my event listener:

public function PreUpdate(LifecycleEventArgs $args): void {
        $entity = $args->getEntity();
        $newsRepo = $args->getEntityManager()->getRepository(News::class);
        if ($entity instanceof Projects) {
            foreach ($newsRepo as $new){
                $news = $args->getEntityManager()->getReference(News::class, $new->getId());
                $entity->setNews($news);
            }
        }
    }

Let's imagine you have an API with many entities and database full of content. And one day your project manager say we need to use this API for two different clients that have exactly the same methods but different design and different content. So I created a new Entity that I called Project and I want all the existing content in the database bind to the new Project with id 1 that I've already created in my Sonata admin. I have almost 5000 users and I want using this PreUpdate update my Projects entity and all the 5000 users after click on "Update" button should be bound to Project id 1. (In code I used news instead users but with the same purpose.) But it hasn't worked. Actually it has when I use it in reverse way when I updated News, Project successfully bound. And I really was disappointed, because as far as I'm concerned I have all the methods in my entity Project to successfully set News. Once again Projects entity:

/**
 * @ORM\Entity(repositoryClass=ProjectsRepository::class)
 * @ORM\HasLifecycleCallbacks()
 */
class Projects {
   /**
     * @ORM\OneToMany(targetEntity=News::class, mappedBy="project", orphanRemoval=true)
     */
    private $news;

    public function __construct() {
        $this->news = new ArrayCollection();
    }
  
      /**
     * @return Collection|News[]
     */
    public function getNews(): Collection {
        return $this->news;
    }

    /**
     * @param mixed $news
     * @return Projects
     */
    public function setNews($news) {
        if (count($news) > 0) {
            foreach ($news as $i) {
                $this->addNews($i);
            }
        }
        return $this;
    }

    /**
     * @param News $news
     */
    public function addNews(News $news) {
        $news->setProject($this);
        $this->news->add($news);
    }

    /**
     * @param News $news
     */
    public function removeNews(News $news) {
        $this->news->removeElement($news);
    }
}

Correct me if I'm not right, what should I add there?

To the solution, I really found I think not the best one but it worked and to be honest I'm happy. I created Projects controller:

/**
 * @Route("/api",name="api_")
 */
class ProjectsController extends AbstractFOSRestController {

    /**
     * @Rest\Get("/v2/projects/update_entity", name="news_update_entity")
     * @param \App\Entity\News $news
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function updateEntity(NewsRepository $newsRepository, ProjectsRepository $projectsRepository, EntityManagerInterface $em): void {
        $news = $newsRepository->findAll();
        $project = $projectsRepository->findOneBy(['id' => 44]);
        foreach ($news as $new) {
            if ($new->getProject()){
                continue;
            }
            $newsUpdate = $em->getReference(News::class, $new->getId());
            $projectPersist = $project->setNews($newsUpdate);
            $em->persist($projectPersist);
            $em->flush();
        }
    }
}

And it's worked as expected. This code successfully bound my news to the project. What I really can't understand why in my controller it worked but PreUpdate in Event Listener doesn't. Any explanation would be highly appreciated. Also I would be thankful if you tell me the best solution for this task. Maybe migrations? And I still want to know what should I do with remove from project and add to project using admin panel, because it's not working.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM