I have these two classes with ManyToMany association:
One class:
/**
* #ORM\ManyToMany(targetEntity="Application\Sonata\UserBundle\Entity\User", inversedBy="tripEvents")
* #ORM\JoinTable(name="event_trip_registrators")
*/
private $tripRegistrators;
public function __construct()
{
$this->tripRegistrators = new ArrayCollection();
}
public function getTripRegistrators()
{
return $this->tripRegistrators;
}
public function setTripRegistrators($tripRegistrators)
{
$this->tripRegistrators = $tripRegistrators;
}
public function addTripRegistrator(User $tripRegistrator)
{
$this->tripRegistrators->add($tripRegistrator);
}
public function removeTripRegistrator($tripRegistrator)
{
$this->tripRegistrators->removeElement($tripRegistrator);
}
Second class:
/**
* #ORM\ManyToMany(targetEntity="Bundle\Entity\Event", mappedBy="tripRegistrators")
*/
protected $tripEvents;
public function __construct()
{
parent::__construct();
$this->tripEvents = new ArrayCollection();
}
public function getTripEvents()
{
return $this->tripEvents;
}
public function setTripEvents($tripEvents)
{
$this->tripEvents = $tripEvents;
}
If I call $event->getTripRegistrators() (first class), I only get an empty persistent collection.
Do you have any hint why this happens?
If I save items via SonataAdmin, everything works fine, the database table has correct data.
Related
I have the following problem with doctrine when testing a symfony 5 application. Instead of updating the rows in the database, new rows are created when the persist () method is called or when cascade = {"persist"} is defined in Entity. The above issue only occurs in a test environment. Everything works fine in the app itself.
sample test code (maximally simplified to show the problem)
class GetReadyArticlesTest extends FunctionalDBTest
{
protected function setUp():void
{
parent::setUp();
$this->addFixture(new ConfigurationFixtures());
$this->addFixture(new CopyWriterTextOrderFixtures());
$this->executeFixtures();
}
protected function tearDown(): void
{
parent::tearDown();
}
public function testProcessSaveArticles()
{
$textOrderRepository = $this->entityManager->getRepository(CopywriterTextOrder::class);
$textOrderEntity = $textOrderRepository->find(1);
$textOrderEntity->setCharacters(4500);
$this->entityManager->persist($textOrderEntity);
$this->entityManager->flush();
}
}
Abstract class FunctionalDBTest:
abstract class FunctionalDBTest extends FunctionalTest
{
/**
* #var EntityManagerInterface
*/
protected $entityManager;
/**
* #var ORMExecutor
*/
private $fixtureExecutor;
/**
* #var ContainerAwareLoader
*/
private $fixtureLoader;
protected function setUp(): void
{
parent::setUp();
if ($this->getContainer()->getParameter('kernel.environment') !== 'test') {
die('Wymagane środowisko testowe');
}
$this->entityManager = $this
->getContainer()
->get('doctrine')
->getManager();
$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->updateSchema($this->entityManager->getMetadataFactory()->getAllMetadata());
}
protected function addFixture(FixtureInterface $fixture): void
{
$this->getFixtureLoader()->addFixture($fixture);
}
protected function executeFixtures(): void
{
$this->getFixtureExecutor()->execute($this->getFixtureLoader()->getFixtures());
}
private function getFixtureExecutor(): ORMExecutor
{
if (!$this->fixtureExecutor) {
$this->fixtureExecutor = new ORMExecutor($this->entityManager, new ORMPurger($this->entityManager));
}
return $this->fixtureExecutor;
}
private function getFixtureLoader(): ContainerAwareLoader
{
if (!$this->fixtureLoader) {
$this->fixtureLoader = new ContainerAwareLoader($this->getContainer());
}
return $this->fixtureLoader;
}
protected function tearDown(): void
{
(new SchemaTool($this->entityManager))->dropDatabase();
parent::tearDown();
$this->entityManager->close();
$this->entityManager = null;
}
}
Removing the persist () method in this example fixes the problem. But in case I want to save a new relation to the table, it also generates a new main object. the problem only occurs in tests.
I want to add a field for upload many attached files in a form with Symfony 4. I've two Entity, Member and Documents with a One-To-Many relation but I've an error who doesn't mean anything for me.
In my Entity Document I've this :
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Member", inversedBy="document")
*/
private $member;
/**
* #return mixed
*/
public function getFile()
{
return $this->file;
}
/**
* #param mixed $file
*/
public function setFile($file): void
{
$this->file = $file;
}
public function getMember(): ?Member
{
return $this->member;
}
public function setMember(?Member $member): self
{
$this->member = $member;
return $this;
}
In my Member Entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\MemberRepository")
*/
class Member
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Documents", mappedBy="member", cascade={"persist"}, orphanRemoval=true)
*/
private $document;
public function __construct()
{
$this->years = new ArrayCollection();
$this->document = new ArrayCollection();
$this->atelier = new ArrayCollection();
}
/**
* #return Collection|Documents[]
*/
public function getDocument(): Collection
{
return $this->document;
}
public function addDocument(Documents $document): self
{
if (!$this->document->contains($document)) {
$this->document[] = $document;
$document->setMember($this);
}
return $this;
}
public function removeDocument(Documents $document): self
{
if ($this->document->contains($document)) {
$this->document->removeElement($document);
// set the owning side to null (unless already changed)
if ($document->getMember() === $this) {
$document->setMember(null);
}
}
return $this;
}
}
I've try to dump the value of my form but when I try to send my form, I've this error when I don't understand
Could not determine access type for property "document" in class "App\Entity\Member": The property "document" in class "App\Entity\Member" can be defined with the methods "addDocument()", "removeDocument()" but the new value must be an array or an instance of \Traversable, "App\Entity\Documents" given.
I have 2 foreign keys which are fk_author and fk_bookcase , I am trying to create my function edit() via a folder Repositorie but I am stuck on the syntax again.
Here is my code via the file BookRepository
public function edit($id)
{
$books = Book::find($id);
$authors = Author::all();
$bookcases = Bookcase::all();
return Book::find($id);
}
Then, in my Controller I have this...
public function edit($id)
{
$books = $this->books->edit($id);
return view('admin.books.edit', compact('books', 'authors', 'bookcases'));
}
Do you have an idea of the problem?
Regards
If you want to retrieve the book with the related 'author' and 'bookcase', you must have defined the relations in the models. For ex:
Book Model
public function author()
{
return $this->belongsTo(Author::class, 'fk_author'); // change fk_author for the key you are using
}
public function bookcase()
{
return $this->belongsTo(Bookcase::class, 'fk_bookcase');
}
Author Model
public function books()
{
return $this->hasMany(Book::class);
}
Bookcase Model
public function books()
{
return $this->hasMany(Book::class);
}
And you doesn't need and edit() function in your repository, just a detail() (or the name what you want) which retrive the Book Object with the relations.
BookRepository
public function detail($id)
{
return Book::with([
'author',
'bookcase',
])
->find($id);
}
Then, in the Controller, yes, you have an edit function which get the detail from the repository and return the object to the edit view.
/**
* #var BookRepository
*/
private $books;
public function __construct(BookRepository $books)
{
$this->books = $books;
}
public function edit($id)
{
$book = $this->books->detail($id);
return view('admin.books.edit', compact('book'));
}
If in any case you want to also return all the authors and bookcases, I think it is better to make a repository for each one, so you can also use them from other Controllers or Classes.
AuthorRepository
public function getAll()
{
return Author::all();
}
BookcaseRepository
public function getAll()
{
return Bookcase::all();
}
Then, in the Controller
/**
* #var BookRepository
*/
private $books;
/**
* #var AuthorRepository
*/
private $authors;
/**
* #var BookcaseRepository
*/
private $bookcases;
public function __construct(BookRepository $books, AuthorRepository $authors, BookcaseRepository $bookcases)
{
$this->books = $books;
$this->authors = $authors;
$this->bookscases = $bookcases;
}
public function edit($id)
{
$book = $this->books->detail($id);
$authors = $this->authors->getAll();
$bookcases = $this->bookcases->getAll();
return view('admin.books.edit', compact('book', 'authors', 'bookcases'));
}
Here are 3 entities :
Entity A
class EntityA
{
/**
* #ORM\OneToMany(targetEntity="EntityB", mappedBy="entityA", cascade={"all"}, orphanRemoval=true)
*/
protected $entitiesB;
public function __construct()
{
$this->entitiesB = new ArrayCollection();
}
public function getEntitiesB()
{
return $this->entitiesB;
}
public function setEntitiesB($entitiesB)
{
$this->entitiesB = new ArrayCollection();
return $this->addEntitiesB($entitiesB);
}
public function addEntityB(EntityB $entityB)
{
if (!$this->entitiesB->contains($entityB))
{
$this->entitiesB->add($entityB);
$entityB->setEntityA($this);
}
return $this;
}
public function addEntitiesB($entitiesB)
{
foreach ($entitiesB as $entityB)
{
$this->addEntityB($entityB);
}
return $this;
}
public function removeEntityB(EntityB $entityB)
{
if ($this->entitiesB->contains($entityB))
{
$this->entitiesB->removeElement($entityB);
}
return $this;
}
public function removeEntitiesB($entitiesB)
{
foreach ($entitiesB as $entityB)
{
$this->removeEntityB($entityB);
}
return $this;
}
}
Entity B
class EntityB
{
/**
*
* #ORM\OneToMany(targetEntity="EntityC", mappedBy="entityB", cascade={"all"}, orphanRemoval=true)
*/
protected $entitiesC;
/**
* #ORM\ManyToOne(targetEntity="EntityA", inversedBy="entitiesB")
* #ORM\JoinColumn(name="entity_a_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $entityA;
public function __construct()
{
$this->entitiesC = new ArrayCollection();
}
public function getEntityA()
{
return $this->entityA;
}
public function setEntityA(EntityA $entityA)
{
$this->entityA = $entityA;
$entityA->addEntityB($this);
return $this;
}
public function getEntitiesC()
{
return $this->entitiesC;
}
public function setEntitiesC($entitiesC)
{
$this->entitiesC = new ArrayCollection();
return $this->addEntitiesC($entitiesC);
}
public function addEntityC(EntityC $entityC)
{
if (!$this->entitiesC->contains($entityC))
{
$this->entitiesC->add($entityC);
$entityC->setEntityB($this);
}
return $this;
}
public function addEntitiesC($entitiesC)
{
foreach ($entitiesC as $entityC)
{
$this->addEntityC($entityC);
}
return $this;
}
public function removeEntityC(EntityC $entityC)
{
if ($this->entitiesC->contains($entityC))
{
$this->entitiesC->removeElement($entityC);
}
return $this;
}
public function removeEntitiesC($entitiesC)
{
foreach ($entitiesC as $entityC)
{
$this->removeEntityC($entityC);
}
return $this;
}
}
Entity C
class EntityC
{
/**
* #ORM\ManyToOne(targetEntity="EntityB", inversedBy="entitiesC")
* #ORM\JoinColumn(name="entity_b_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $entityB;
public function getEntityB()
{
return $this->entityB;
}
public function setEntityB(EntityB $entityB)
{
$this->entityB = $entityB;
$entityB->addEntityC($this);
return $this;
}
}
So, now, assume that we have this data :
EntityA [
EntitiesB [
EntityB1 [
EntitiesC [
EntityC1
EntityC2
EntityC3
]
]
EntityB2 [
EntitiesC [
EntityC4
]
]
]
]
That I want is to transfer EntityC4 to the EntityB1[EntitieC] collection.
To achieve that, the process would be :
- EntityB2.EntitiesC::removeEntityC(EntityC4)
- EntityB1.EntitiesC::addEntity(EntityC4)
But it will not do the trick ... EntityC4 is removed and not transferred !
So, it works when orphanRemoval=false on EntityB.EntitiesC, but I want to keep this Doctrine flag.
Is it another way to achieve that properly ?
Thanks for your ideas.
If you read the Doctrine documentation chapter 8.7. Orphan Removal you can see the following:
When using the orphanRemoval=true option Doctrine makes the assumption that the entities are privately owned and will NOT be reused by other entities. If you neglect this assumption your entities will get deleted by Doctrine even if you assigned the orphaned entity to another one.
Seems to me that this this is exactly the mistake you are making in this case.
You simply cannot use orphanRemoval in this case.
I am trying to implement a simple menu composite pattern.
These are the following classes i came up with.
MenuItem:
namespace MYNAME\MYBUNDLE\Entity;
use MYNAME\MYBUNDLE\Menu\MenuComponent;
class MenuItem implements MenuComponent
{
private $id;
private $name;
private $path;
private $parent;
private $visible;
private $createdOn;
private $templating;
private $attr;
private $children;
private $website;
private $position = 1;
public function __construct($name = null, $path = null, $attr = array(), $visible = true)
{
$this->name = $name;
$this->path = $path;
$this->visible = $visible;
$this->attr = $attr;
$this->createdOn = new \DateTime;
}
public function prePersist()
{
$this->createdOn = new \DateTime;
}
public function build()
{
$data['menu_item'] = $this;
$data['options'] = $this->attr;
if($this->hasChildren())
return $this->templating->render('MYBUNDLE:Menu:menu_dropdown.html.twig', $data);
if($this->isChild())
return $this->parent->getTemplating()->render('MYBUNDLE:Menu:menu_item.html.twig', $data);
return $this->templating->render('MYBUNDLE:Menu:menu_item.html.twig', $data);
}
public function __toString()
{
return $this->name;
}
public function setTemplating($templating)
{
$this->templating = $templating;
}
/**
* #return bool
*/
public function isChild()
{
return $this->hasParent();
}
/**
* #return bool
*/
public function hasParent()
{
return isset($this->parent);
}
/**
* #return bool
*/
public function hasChildren()
{
return count($this->children) > 0;
}
}
If left out the getters and setters to make it a bit shorter here.
As you can see this is the entity and it contains a build() function, however this function uses the render method which in my opinion shouldn't be in an entity.
MenuController
<?php
namespace MYNAME\MYBUNDLE\Controller;
use MYNAME\MYBUNDLE\Menu\Menu;
use MYNAME\MYBUNDLE\Entity\MenuItem;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class MenuController extends Controller
{
public function generateAction()
{
$menu = new Menu($this->get('templating'));
// load menu items
$items = $this->getDoctrine()->getRepository('MYBUNDLE:MenuItem')->findOrdered();
foreach($items as $item)
{
if(!$item->hasParent())
$menu->add($item);
}
return new Response($menu->build());
}
}
The MenuController gets called to render the menu:
{{ render(controller('MYBUNDLE:Menu:generate')) }}
I would also like this to be different since it doesn't look right. Perhaps it's better to create a twig function to render the menu?
MenuComponent:
namespace MYNAME\MYBUNDLE\Menu;
interface MenuComponent {
public function build();
}
Menu:
namespace MYNAME\MYBUNDLE\Menu;
class Menu implements MenuComponent
{
private $children;
private $templating;
public function __construct($templating)
{
$this->templating = $templating;
}
public function add(MenuComponent $component)
{
$component->setTemplating($this->templating);
$this->children[] = $component;
}
public function build()
{
return $this->templating->render('MYBUNDLE:Menu:menu.html.twig', array("menu_items" => $this->children));
}
}
Menu Contains the MenuComponents and will render the menu first, in each MenuItem it's build() method is called.
I think it's better to remove the rendering logic from my MenuItem entity and place this somewhere else, however i can't figure out on how to do this properly within this design pattern.
Any help or suggestion is appreciated.