I'm working with symfony 3.4.6 and fosrestbundle. I have three entities related as follow:
Embarque class
class Embarque{
//...
/**
* #var EmbarqueContenedor[]|ArrayCollection
*
* #Serializer\SerializedName("contenedores")
* #ORM\OneToMany(targetEntity="AppBundle\Entity\EmbarqueContenedor",mappedBy="embarque",cascade={"persist"})
*/
private $contenedores;
public function addEmbarqueContenedor($embarqueContenedor)
{
if (!$this->contenedores->contains($embarqueContenedor)) {
$this->contenedores->add($embarqueContenedor);
//$embarqueContenedor->setEmbarque($this);
}
}
public function removeEmbarqueContenedor($embarqueContenedor)
{
if ($this->contenedores->contains($embarqueContenedor)) {
$this->contenedores->removeElement($embarqueContenedor);
}
}
}
EmbarqueContenedor class
class EmbarqueContenedor{
/**
* #var Embarque
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Embarque",inversedBy="contenedores",)
*/
private $embarque;
/**
* #var Contenedor
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Contenedor",inversedBy="embarques")
*/
private $contenedor;
}
Contenedor class
class Contenedor{
/**
* #var EmbarqueContenedor[]|ArrayCollection
*
* #Serializer\SerializedName("contenedorEmbarques")
* #ORM\OneToMany(targetEntity="AppBundle\Entity\EmbarqueContenedor",mappedBy="contenedor")
*/
private $embarques;
public function addEmbarqueContenedor($embarqueContenedor)
{
if (!$this->embarques->contains($embarqueContenedor)) {
$this->embarques->add($embarqueContenedor);
$embarqueContenedor->setContenedor($this);
}
}
public function removeEmbarqueContenedor($embarqueContenedor)
{
if ($this->embarques->contains($embarqueContenedor)) {
$this->embarques->removeElement($embarqueContenedor);
}
}
}
in forms is as follow
class EmbarqueType{
$builder->add('contenedores', CollectionType::class, [
'entry_type' => EmbarqueContenedorType::class,
'allow_add' => true,
]);
}
class EmbarqueContenedorType{
$builder->add('contenedor', EntityType::class, [
'class' => Contenedor::class,
])
}
The entity contenedor is create apart and selected in EmbarqueContenedorType when adding or editing, the EmbarqueContenedorEntity is created from EmbarqueType.
The problem is that the records are persisted in the database but with out any reference. The EmbarqueContenedor table has no reference for the Embarque or Contenedor tables.
There is no error because the data is persisted but not referenced. How could this be??
Thanks in advance!
Edit
I noticed that I was not serializing the Id property of Contenedor Entity so is imposible to make the reference, now is fixed but the Embarque entity still not being referenced.
I think the problem is in the design of the tables relationships. The table/entity EmbarqueContenedor is unnecessary. When you have a many-to-many relation just say that to the Doctrine and the Doctrine takes care of the rest (Doctrine will create all necessary tables).
So the solution should be to define your relations with ManyToMany annotation.
Related
In my Symfony 5 application, I have an entity class Product which has two properties $categories and $bundles. The product class has a ManyToMany relation with both the properties. When I comment out either one of the properties the Product serialization works perfectly. But incase both properties are present the serialization times out.
The code excerpt from Product class.
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #MaxDepth(1)
*/
private $categories;
}
The code for the serialization is below.
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', [
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
return $object->getId();
}
]);
I have tried using the #ORM/JoinTable annotation suggested on some other Stackoverflow answers and #MaxDepth as well but no luck. The code works if any of the properties are commented out. Would be grateful for any advice on this.
okay, 20 products is actually not much. so I guess you're outputting the same objects over and over again if you let the relations be serialized unhindered.
I actually don't know how to achieve this reliably with the serializer. But the standard ways would just be enough probably. I like serializing via the JsonSerializable interface on your entities like this (omitting the ORM stuff for brevity):
class Product implements \JsonSerializable {
public $name;
public $categories; // relation
// getters + setters omitted
// this implements \JsonSerializable
public function jsonSerialize() {
return [
'name' => $this->name,
'categories' => array_map(function($category) {
return $category->jsonSerializeChild();
}, $this->categories),
];
}
// this function effectively stops recursion by leaving out relations
public function jsonSerializeChild() {
return [
'name' => $this->name,
];
}
}
If you implement this on all your entities you can very effectively limit the depth of serialization to two (i.e. the "base" entities and their connected entities).
also, the symfony serializer will use the JsonSerializable interface if it's defined if your serializing to JSON. Obviously, this is not as elegant as some fancy annotation-based serialization or a "smart" serializer, that actually manages to stop ... but it'll probably work better...
Pointed out by #Jakumi the serializer was looping over and over the object properties $categories and $bundles. I avoided that by using the Serialization groups.
The product class
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $categories;
}
The category class
class Category
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups("product_listing:read")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups("product_listing:read")
*/
private $name;
}
The call to serializer
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', ['groups' => 'product_listing:read']);
I hope this helps someone in future.
I have three entities, Block, BlockPlacement, BlockPosition:
class BlockEntity
{
private $bid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="block",
* cascade={"remove"})
*/
private $placements;
}
class BlockPlacementEntity
{
/**
* The id of the block postion
*
* #ORM\Id
* #ORM\ManyToOne(targetEntity="BlockPositionEntity", inversedBy="placements")
* #ORM\JoinColumn(name="pid", referencedColumnName="pid", nullable=false)
*/
private $position;
/**
* The id of the block
*
* #var BlockEntity
* #ORM\Id
* #ORM\ManyToOne(targetEntity="BlockEntity", inversedBy="placements")
* #ORM\JoinColumn(name="bid", referencedColumnName="bid", nullable=false)
*/
private $block;
private $sortorder;
}
class BlockPositionEntity
{
private $pid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="position",
* cascade={"remove"})
* #ORM\OrderBy({"sortorder" = "ASC"})
*/
private $placements;
}
So, you can see the relationship: Block < OneToMany > Placement < ManyToOne > Position.
Now I am trying to construct a form to create/edit a block:
$builder
->add($builder->create('placements', 'entity', [
'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
'choice_label' => 'name',
'multiple' => true,
'required' => false
]))
;
This gives me a good select box with multiple selections possible with a proper list of positions to choose from. But it does not show previous selections for placement (I am using existing data) e.g. marking positions as 'selected'. I have not tried creating a new Block yet, only editing existing data.
I suspect I will need to be using addModelTransformer() or addViewTransformer() but have tried some of this an cannot get it to work.
I've looked at the collection form type and I don't like that solution because it isn't a multi-select box. It requires JS and isn't as intuitive as a simple select element.
This seems like such a common issue for people. I've searched and found no common answer and nothing that helps.
Update: please look at this example repo
Update 2: i've updated the repo.
I did it with form event listeners and unmapped choice field.
Take a closer look at BlockType form type
Feel free to ask any questions about it.
OK - so in the end, I found a different way. #Stepan Yudin's answer worked, but is complicated (listeners, etc) and not quite like I was hoping.
So, I have the same three entities. BlockPlacement and BlockPosition remain the same (and so aren't reposted, see above) but I have made some changes to the BlockEntity:
class BlockEntity
{
private $bid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="block",
* cascade={"remove", "persist"},
* orphanRemoval=true)
*/
private $placements;
/**
* Get an ArrayCollection of BlockPositionEntity that are assigned to this Block
* #return ArrayCollection
*/
public function getPositions()
{
$positions = new ArrayCollection();
foreach($this->getPlacements() as $placement) {
$positions->add($placement->getPosition());
}
return $positions;
}
/**
* Set BlockPlacementsEntity from provided ArrayCollection of positionEntity
* requires
* cascade={"remove, "persist"}
* orphanRemoval=true
* on the association of $this->placements
* #param ArrayCollection $positions
*/
public function setPositions(ArrayCollection $positions)
{
// remove placements and skip existing placements.
foreach ($this->placements as $placement) {
if (!$positions->contains($placement->getPosition())) {
$this->placements->removeElement($placement);
} else {
$positions->removeElement($placement->getPosition()); // remove from positions to add.
}
}
// add new placements
foreach ($positions as $position) {
$placement = new BlockPlacementEntity();
$placement->setPosition($position);
// sortorder is irrelevant at this stage.
$placement->setBlock($this); // auto-adds placement
}
}
}
So you can see that the BlockEntity is now handling a positions parameter which doesn't exist in the entity at all. Here is the relevant form component:
$builder
->add('positions', 'Symfony\Bridge\Doctrine\Form\Type\EntityType', [
'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
'choice_label' => 'name',
'multiple' => true,
])
note that I have changed to Symfony 2.8 form style since my first post
This renders a multiple select element on the page which accepts any number of positions and converts them to an ArrayCollection on submit. This is then handled directly by the form's get/set position methods and these convert to/from the placement property. The cascade and orphanRemoval are important because they take care to 'clean up' the leftover entities.
because it is references above here is the BlockPlacement setBlock($block) method:
public function setBlock(BlockEntity $block = null)
{
if ($this->block !== null) {
$this->block->removePlacement($this);
}
if ($block !== null) {
$block->addPlacement($this);
}
$this->block = $block;
return $this;
}
I'm currently working on a language assessment project which enables you to take an exam in the language you want and evaluate your level. I use Symfony2 framework and work with Doctrine2 as well. My issue is the following one:
I have two entities Exam and Question linked by a Many-To-Many relation (Exam being the owner). Each exam can be related to several questions, and each question can be related to several exams.
Here is my code:
Exam entity
/**
* Exam
*
* #ORM\Table(name="cids_exam")
* #ORM\Entity(repositoryClass="LA\AdminBundle\Entity\ExamRepository")
*/
class Exam
{
...
/**
* #ORM\ManyToMany(targetEntity="LA\AdminBundle\Entity\Question", cascade={"persist"})
* #ORM\JoinTable(name="cids_exam_question")
*/
private $questions;
...
/**
* Constructor
*/
public function __construct()
{
$this->questions = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add questions
*
* #param \LA\AdminBundle\Entity\Question $questions
* #return Exam
*/
public function addQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions[] = $questions;
return $this;
}
/**
* Remove questions
*
* #param \LA\AdminBundle\Entity\Question $questions
*/
public function removeQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions->removeElement($questions);
}
/**
* Get questions
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getQuestions()
{
return $this->questions;
}
}
As long as it is a unidirectional relation, there is no 'exams' attribute in my Question class.
Now, what I want to do is getting all the questions related to a specific exam, calling the getQuestions() method, like this:
$questions = $exam->getQuestions();
But this method returns an empty array, even if I have data in my database. If I var_dump the $exam variable, I can see the questions array is empty:
object(LA\AdminBundle\Entity\Exam)[47]
private 'id' => int 5
...
private 'questions' =>
object(Doctrine\ORM\PersistentCollection)[248]
private 'snapshot' =>
array (size=0)
empty
private 'owner' => null
private 'association' => null
private 'em' => null
private 'backRefFieldName' => null
private 'typeClass' => null
private 'isDirty' => boolean false
private 'initialized' => boolean false
private 'coll' =>
object(Doctrine\Common\Collections\ArrayCollection)[249]
private '_elements' =>
array (size=0)
...
I think I could maybe write a findByExam() function in my QuestionRepository, but I don't really know how to implement the joins in this case.
Any help would be great!
To have a findByExam() method in your QuestionRepository do the following:
public function findByExam($exam)
{
$q = $this->createQueryBuilder('q')
->where('q.exam = :exam')
->setParameter('exam', $exam)
->getQuery();
return $q->getResult();
}
You could also create a bi-directional relationship not uni-directional !
Each exam can be related to several questions, and each question can
be related to several exams.
Create a bi-directional relationship by adding this to your Question entity:
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Vendor\YourExamBundle\Entity\ExamInterface;
class Question
{
protected $exams;
public function __construct()
{
$this->exams = new ArrayCollection();
}
public function getExams()
{
return $this->exams;
}
public function addExam(ExamInterface $exam)
{
if !($this->exams->contains($exam)) {
$this->exams->add($exam);
}
return $this;
}
public function setExams(Collection $exams)
{
$this->exams = $exams;
return $this;
}
// ...
Afterwards you can use...
$question->getExams()
... in your controller.
To automatically join your related entities doctrine's fetch option can be used with:
LAZY ( loads the relations when accessed )
EAGER ( auto-joins the relations )
EXTRA_LAZY ( manual fetching )
example:
/**
* #ManyToMany(targetEntity="Question",inversedBy="exams", cascade={"all"}, fetch="EAGER")
*/
Though eager loading has a downside in terms of performance it might be an option for you.
Doctrine Fetch with EAGER
Whenever you query for an entity that has persistent associations and
these associations are mapped as EAGER, they will automatically be
loaded together with the entity being queried and is thus immediately
available to your application.
Read more about it in the Doctrine Documentation.
Another option you should check when working with relations is the cascade option.
See the Doctrine - Working with Associations chapter of the documentation.
Tip:
You should create interfaces for exams and questions and use them instead of the original entity in your set and add methods to allow easier extending.
Bi-Directional Relations using Doctrine2 ORM with association table exam_questions
exam_id question_id
<?php
class Exams
....OTHER PROPERTIES...
/**
* Owning Side
*
* #ManyToMany(targetEntity="Questions", inversedBy="exams")
* #JoinTable(name="exam_questions",
* joinColumns={#JoinColumn(name="exam_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="question_id", referencedColumnName="id")}
* )
*/
private $questions;
..OTHER CODES..
}
class Questions{
..OTHER CODES..
/**
* Inverse Side
*
* #ManyToMany(targetEntity="Exams", mappedBy="questions")
*/
private $exams;
..OTHER CODES..
}
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annref-manytomany
I have a fairly common use case which I am trying implement but am running into some issues with the Symfony Sonata Admin Bundle (ORM). My model has a relationship between a Facility and a Sport which is based on three entity classes: Sport, Facility and SportsFacility. I followed the example http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/form_field_definition.html#advanced-usage-one-to-many and defined in the following classes (relevant parts only).
class Sport {
/**
* Bidirectional - Many facilies are related to one sport
*
* #ORM\OneToMany(targetEntity="SportsFacility", mappedBy="sport")
*/
protected $facilities;
public function getFacilities() {
return $this->facilities;
}
public function setFacilities($facilities) {
$this->facilities = $facilities;
return $this;
}
}
class Facility {
/**
* Bidirectional - Many sports are related to one facility
*
* #ORM\OneToMany(targetEntity="SportsFacility", mappedBy="facility")
*/
protected $sports;
public function getSports() {
return $this->sports;
}
public function setSports($sportsFacilities) {
$this->sports = $sportsFacilities;
return $this;
}
public function addSports($sportsFacilities) {
$this->sports = $sportsFacilities;
return $this;
}
}
class SportsFacility {
/**
* #var integer $sportsFacilityId
*
* #ORM\Column(name="sportsFacilityId", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $sportsFacilityId;
/**
* Bidirectional - Many Sports are related to one Facility (OWNING SIDE)
*
* #ORM\ManyToOne(targetEntity="Sport", inversedBy="facilities"))
* #ORM\JoinColumn(name="sportId", referencedColumnName="sportId"))
*/
protected $sport;
/**
* Bidirectional - Many Facilities are related to one Sport (OWNING SIDE)
*
* #ORM\ManyToOne(targetEntity="Facility", inversedBy="sports"))
* #ORM\JoinColumn(name="facilityId", referencedColumnName="facilityId"))
*/
protected $facility;
public function getSportsFacilityId() {
return $this->sportsFacilityId;
}
public function setSportsFacilityId($sportsFacilityId) {
$this->sportsFacilityId = $sportsFacilityId;
return $this;
}
public function getSport() {
return $this->sport;
}
public function setSport($sport) {
$this->sport = $sport;
return $this;
}
public function getFacility() {
return $this->facility;
}
public function setFacility($facility) {
$this->facility = $facility;
return $this;
}
}
In my FacilityAdmin class I have:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name')
->with('Sports')
->add('sports', 'sonata_type_collection',
array('by_reference' => false),
array(
'edit' => 'inline',
'inline' => 'table',
))
->end();
}
When I try to add a new relation, I get the following error:
Expected argument of type "array or \Traversable", "Clarity\CoachTimeBundle\Entity\SportsFacility" given in "vendor/sonata-project/admin-bundle/Sonata/AdminBundle/Form/EventListener/ResizeFormListener.php at line 88"
Finally found where we have the problem; in your class Facility you added the missing method for sonata, but it shouldn't do that you think it should do (yup yup) :
class Facility {
...
/**
* Alias for sonata
*/
public function addSports($sportFacility) {
return $this->addSport($sportFacility);
}
}
I presume addSport() is the default doctrine generated method to add new instance of SportsFacilityto collection.
This is due to how sonata generate the method to add a new entity that is different how doctrine do :
//file: Sonata/AdminBundle/Admin/AdminHelper.php
public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
{
...
$method = sprintf('add%s', $this->camelize($mapping['fieldName']));
...
}
Someone got the same problem but the documentation didn't evolve
I am trying to persist an user entity with a profile entity from a single form submit. Following the instructions at the Doctrine2 documentation and after adding additional attributes this seemed to be sufficient to achieve the goal.
Entities
Setting up the entites in accordance is pretty straight forward and resulted in this (I left out the generated getter/setter):
// ...
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
/**
* #ORM\OneToOne(targetEntity="Profile", mappedBy="user", cascade={"persist", "remove"})
*/
private $Profile;
// ...
}
// ...
/**
* #ORM\Entity
*/
class Profile
{
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User")
*/
private $user;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
// ...
}
Forms
Now modifiying the forms is not too difficult as well:
// ...
class ProfileType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('data')
;
}
public function getName()
{
return 'profile';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Acme\TestBundle\Entity\Profile');
}
}
// ...
class TestUserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('data')
->add('Profile', new ProfileType())
;
}
public function getName()
{
return 'user';
}
}
Controller
class UserController extends Controller
{
// ...
public function newAction()
{
$entity = new User();
$form = $this->createForm(new UserType(), $entity);
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
public function createAction()
{
$entity = new User();
$request = $this->getRequest();
$form = $this->createForm(new UserType(), $entity);
$form->bindRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('user_show',
array('id' => $entity->getId())));
}
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
// ...
}
But now comes the part where testing takes place. I start to create a new user-object, the embedded form shows up as expected, but hitting submit returns this:
Exception
Entity of type Acme\TestBundle\Entity\Profile is missing an
assigned ID. The identifier generation strategy for this entity
requires the ID field to be populated before EntityManager#persist()
is called. If you want automatically generated identifiers instead
you need to adjust the metadata mapping accordingly.
A possible solution I am already aware of is to add an additional column for a stand-alone primary key on the Profile entity.
However I wonder if there is a way to keep the mapping roughly the same but deal with persisting the embedded form instead?
After debating for quite a while with a couple of people via IRC I modified the mapping and came up with this:
Entities
// ...
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
/**
* #ORM\OneToOne(targetEntity="Profile", cascade={"persist", "remove"})
*/
private $Profile;
// ...
}
// ...
/**
* #ORM\Entity
*/
class Profile
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
// ...
}
So what does this change? First of all I removed the mappedBy and inversedBy options for the relation. In addition the OneToOne annotation on the Profile-entity was not needed.
The relation between User and Profile can be bi-directional however a uni-directional relation with User being the owning side is sufficient to have control over the data. Due to the cascade option you can be sure there are no left-over Profiles without Users and Users can maintain a Profile but do not have to.
If you want to use a bi-directional relation I recommand taking a look at Github: Doctrine2 - Tests - DDC117 and especially pay attention to Article and ArticleDetails' OneToOne relation. However you need to be aware that saving this bi-directional relation is a bit more tricky as can be seen from the test file (link provided in comment): you need to persist the Article first and setup the constructor in ArticleDetails::__construct accordingly to cover the bi-directional nature of the relationship.
The problem from what I can see is that you're only creating / saving a User object.
As the User / Profile is a One to One relation (with User being the owning side) would it be safe to assume that a User will always have a Profile relation, and so could be initialised in the Users construction
class User
{
public function __construct()
{
$this->profile = new Profile();
}
}
After all you've set User up to cascade persistence of the related Profile object. This will then have your entity manager create both Entities and establish the relation.