Symfony2 ManyToMany embedded forms - php

I have a Post and Tag entity in my application, and I need many to many association between them. I think I managed it right, but not enirely sure. Here are my entities:
Post:
/**
* #ORM\Table(name="posts")
*/
class Post
{
( ... )
/**
* #ORM\OneToMany(targetEntity="PostTag", mappedBy="post_id")
*/
private $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
( ... )
}
Tag:
class Tag
{
/**
* #ORM\Column(name="tagname", unique=true, type="string", length=255)
*/
private $tagname;
/**
* #ORM\OneToMany(targetEntity="PostTag", mappedBy="tag_id")
*/
private $posts;
public function __construct()
{
$this->posts = new ArrayCollection();
}
( ... )
}
I also created a PostTag entity to store these relations:
/**
* #ORM\Table(name="post_tags")
* #ORM\Entity
*/
class PostTag
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Post", inversedBy="tags")
* #ORM\JoinColumn(name="post_id", referencedColumnName="id")
*/
private $post_id;
/**
* #ORM\ManyToOne(targetEntity="Tag", inversedBy="posts")
* #ORM\JoinColumn(name="tag_id", referencedColumnName="id")
*/
private $tag_id;
( ... )
}
Of course all 3 with appropriate getters/setters. Are the relations okay this way?
I believe I have it right, but now I'm struggling to make an embedded form for the Post entity. What I need is, to create a tags field in the PostType, where one could type in tags which are saved in the tags table and the id of both the newly created tag and post in the post_tags table. I also want the already saved tags to be pickable in another field, that's why I have the entities build this way.
I tried to write this, but got really confused with bad codes, so I don't even try to copy what I had. Can someone briefly enlighten me how should I accomplish this?
Thanks

You don't need intermediary entity between Post and Tag. I myself struggled to get it working a few months back, but after carefully reading Many-To-Many, Unidirectional, I managed to do it.
The point is that you don't create Many-To-One and One-To-Many relations but a single Many-To-Many.
Regarding the embedded forms, once you establish Many-To-Many relation between Post and Tag you'll need to use collection field form type. Basically, you'll be saying: "OK, I have a form that has fields of Post which can have many Tags.
Of course, I would suggest you try managing data manually (persist, update, delete) before trying to make it work with forms. If you have an error in your model it'll be much more difficult to locate the source of a problem, as forms themselves can be tricky.
Official Symfony docs have a great article on this, although, I must say, it's a little overwhelming for a Symfony beginner as I was in a time of reading it.

Related

Doctrine in Symfony: use a single “Author” associative entity related to different entities

I'm developing a custom content management system with Symfony 5 and Doctrine.
I'm trying to implement a relation between the entities Document and Video (actually there are many more, but for simplicity sake let's say are just two) and the User entity.
The relation represent the User who wrote the document or recorded the video. So the relation here is called Author. Each document or video can have one or more author. Each User can have none or more document or video.
I would like to use just a single associative Author associative entity, like this:
entity_id|author_id|entity
Where:
entity_id: is the id of the document or video
author_id: is the user_id who authored the entity
entity: is a constant like document or video to know to which entity the relation refer to
The problem is that I cannot understand how to build this in Doctrine. Was this a classic SingleEntity<-->Author<-->Users relationship I would have build it as a ManyToMany item, but here it's different.
Author would probably contain two ManyToOne relations (one with the User entity and one with either the Document or the Video entity) plus the entity type field, but I really don't know how to code the "DocumentorVideo`" part. I mean:
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=??????????, inversedBy="authors")
* #ORM\JoinColumn(nullable=false)
*/
private $entity; // Document or Video
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="articles")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\Column(type="smallint")
*/
private $entityType;
How should I manage the first field?
Don't know if would be better to store it under two differents attributes. If not and mandatory, I think those "objects" should have a common interface or something, so take a look to doctrine inheritance that should fulfill your needs
My suggestion is to store the entity namespace Ex. Acme\Entity\Document in a property and the id in another and to use the entity manager to get the entity.
Edit: Though you won't have the relation, I prefer that way over others because it is reusable and the performance is rather the same. Also if I need to pass it to a JSON response, I just create a normalizer and I am good to go.
/**
* #ORM\Column(type="string")
*/
private $entityNamespace;
/**
* #ORM\Column(type="integer")
*/
private $entityId;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getEntity()
{
return $this->em->getRepository($this->entityNamespace)->find($this->entityId);
}

Symfony 4 Validation on whole Entity

So, I need a validation for a reservation for a sports club.
A reservation has a start and an end datetime and you can reservate for 1 or more tables.
So the Entity looks like
class Reservation
{
use TimestampAble;
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="reservations")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Table", inversedBy="reservations")
*/
private $tables;
/**
* #ORM\Column(type="datetime")
*/
private $start;
/**
* #ORM\Column(type="datetime")
*/
private $end;
...
}
(Don't worry about the entity "Table" - the db table for it is named "snooker_table" ;))
Now I need to validate, that in the requested time range with the requested tables no other reservation already exist.
And this gives me headaches...
I know I can make it "manually" in the Controller Actions create / update. But I'm also using Symfonys Easy-Admin, so I need to put the code there as well.
I thought about putting the validation as an annotation directly into the entity. But I don't know where... If I put it on "$tables" I just get an ArrayCollection without the needed start and end datetimes. And it's also not an unique entity (as I need to go on a range of datetimes and so on).
So: any ideas how to achieve this in the entity directly? or at least in the form type (and for easy admin i care later)?
Thx in advance.
Ok, I'm going to do it in this way: Symfony 2 UniqueEntity repositoryMethod fails on Update Entity
Creating a repository method (not a constraint) for validation and use it for the UniqueEntity constraint on the whole entity itself. Feels a little bit dirty but ok...
Hope this works in easy admin as well.

Doctrine - mapping doesn't work

I have problem with Doctrine mapping. First of all I'll introduce my two entites:
/**
* #ORM\Entity
* #ORM\Table(name="header_fotos")
*/
class HeaderFoto extends BaseEntity{
....
/**
*
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="\App\Webpage\Webpage", inversedBy="headerFotos")
* #ORM\JoinColumn(name="webpage_id", referencedColumnName="id")
*/
protected $webpage;
...
}
Second entity:
/**
* #ORM\Entity
* #ORM\Table(name="webpages")
*/
class Webpage extends BaseEntity{
...
/**
* #ORM\OneToMany(targetEntity="\App\Webpage\HeaderFoto", mappedBy="webpage")
*/
protected $headerFotos;
public function __construct() {
parent::__construct();
$this->headerFotos = new ArrayCollection();
}
My problem is with mapping. When I load entity Webpage and try to access all entities of type HeaderFoto it cannot find any relation. I was trying to compare with another working project with Doctrine and everything is the same.
I was trying to change association to OneToOne on the both sides, just for sure. But in this case it returned Exception No mapping found for field webpage.
I will appreciate every help and advice. Thanks for help.
EDIT
To be more specific, it has problem in class \Doctrine\ORM\Persisters\BasicEntityPersister.php in method getOneToManyStatement. It tries to load associated objects, but the array associationMappings in this class is empty. This is the last thing what I have found out.

Symfony Association Mapping OneToOne and OneToMany to Same Entity

I have a View entity that represents the primary page record, and then I have an associated entity called ViewVersion which stores multiple versions of the entity as it's changed over time. The View entity sets the current "Published" ViewVersion in the VersionId field. This makes for a simple OneToOne association. But in some contexts I will also want to get all the versions associated with this View entity, e.g. if I want to allow the user to review older versions and revert back. So I will need another mapping which is a OneToMany. The first viewVersion will map to the active "published" version, and the second viewVersions will show all the versions.
Entity Definitions
/**
* #ORM\Entity
* #ORM\Table(name="view")
* #ORM\Entity(repositoryClass="Gutensite\CmsBundle\Entity\View\ViewRepository")
*/
class View extends Entity\Base {
/**
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", inversedBy="view", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\JoinColumn(name="versionId", referencedColumnName="id")
*/
protected $viewVersion;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $versionId = NULL;
/**
* #ORM\OneToMany(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", mappedBy="viewAll", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $viewVersions;
}
/**
* #ORM\Entity
* #ORM\Table(name="view_version")
* #ORM\Entity(repositoryClass="Gutensite\CmsBundle\Entity\View\ViewVersionRepository")
*/
class ViewVersion extends Entity\Base {
/**
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", mappedBy="viewVersion", cascade={"persist"})
*/
protected $view;
/**
* #ORM\ManyToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", inversedBy="viewVersions")
* #ORM\JoinColumn(name="viewId", referencedColumnName="id")
*/
protected $viewAll;
/**
* The primary view entity that this version belongs to.
* #ORM\Column(type="integer", nullable=true)
*/
protected $viewId;
}
This "works" but is it recommended to have two associations with the same entity like this? Or is this a really bad idea?
The ViewVersion entity will reference a single View entity in both cases, but the mapped associations need two separate variables, e.g. View and ViewAll. I'm not exactly sure how the internals work for the association, and how the reference variable with the mapping is used.
Alternatively, I could get rid of the OneToOne association, and just set a ViewRepository function to get the current published version based on the versionId (just like the old mapped entity used to do with the getVersion()). That would work, but is it more internal overhead, because it would make two queries... or will Doctrine be smart enough to optimize this, just like it did with the getVersion().
NOTE:
These other answers are not complete.
References:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html
http://doctrine-orm.readthedocs.org/en/2.0.x/reference/association-mapping.html#one-to-many-bidirectional
Typically, I have found the best approach is to solve this in a different way.
One common pattern I have seen before is you use a single table to hold all records, and have an 'active' flag.
If your query to select the active one works like so:
SELECT * FROM table WHERE active = true ORDER BY updated_at DESC LIMIT 1;
Then enabling a new one becomes as simple as:
UPDATE table SET active = 1, updated_at = '<timestamp>' WHERE id = <new id>;
UPDATE table SET active = 0, updated_at = '<timestamp>' WHERE id = <old id>;
Your new page will be active as soon as the first query hits, and your second query will avoid any sort of weirdness as that row will already be no longer active.
If you have other models that depend on a consistent ID to reference, then another route which also maintains some sanity would be to have one table for the active entries (in whole, not in part) and then a second table with additional metadata to track versions.
The latter approach could be nicely handled via Doctrine's inheritance system (http://docs.doctrine-project.org/en/2.0.x/reference/inheritance-mapping.html) which would let you define the base View class, and then for the "ViewRevision" model, extend View and add a "Revised on" type timestamp.
Per the advice from #jmather I've decided this model is "okay", because I need a single View entity that other entities can access (e.g. Routing urls that point to a single View, i.e. "page").
I've changed the OneToOne relationship for View to be unidirectional only, because the ViewVersion already has an association back to the View via the other OneToMany (so it doesn't need two paths back).
This allows me to keep a simple method for $view->getPublished() handy and seems more logical.
/**
* #ORM\Entity
* #ORM\Table(name="view")
*/
class View extends Entity\Base {
/**
* This is a OneToOne Unidirectional association, just so that we can get the
* current published version easily, based on the publishedId.
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\TestVersion")
* #ORM\JoinColumn(name="publishedId", referencedColumnName="id")
*/
protected $published;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $publishedId = NULL;
/**
* This is the regular OneToMany Bi-Directional Association, for all the versions.
* #ORM\OneToMany(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", mappedBy="view", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $versions;
}
/**
* #ORM\Entity
* #ORM\Table(name="view_version")
*/
class ViewVersion extends Entity\Base {
/**
* #ORM\ManyToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", inversedBy="versions")
* #ORM\JoinColumn(name="viewId", referencedColumnName="id")
*/
protected $view;
/**
* The primary view entity that this version belongs to.
* #ORM\Column(type="integer", nullable=true)
*/
protected $viewId;
}
However, I've discovered that as long as the $view->publishedId is set the view can't be deleted from the database because of foreign key constraints (even though it's uni-directional). So I have to break that foreign key link before removing. I think that's fine. I posted details about that here: Overlapping Entity Association causing Database Foreign Key Constraint Errors when Removing Entity

Symfony2/Doctrine - form using two entities and a jointable/joinentity with metadata

So I have three entities...
Contract (id, name, services)
Service (id, name, recommendedPrice)
ContractService (client_id, service_id, adjustedPrice)
I would like to have a form for creating/editing a 'Contract', that displays a checkbox for each possible 'Service', and a text field for the adjusted price of that service.
I have spent days trying to figure out the best way to structure all of this and am having a really hard time. I would assume it is similar in design to a form you would use to enable/disable permissions for a user, but I cannot find any good examples.
Right now I have my 'Contract' entity, which has an oneToMany association to its 'ContractServices', as a Contract can have many ContractServices. The 'ContractServices' entity has a manyToOne association to 'Services', is manyToOne correct here?
When I try to use the ContractServiceType form to collect services, I get no data on the form unless I have assigned one or more dummy 'ContractService' entities to the 'Contract' entity prior to rendering the form (similar to the task/tags embedded form tutorial on the Symfony site).
Also, once the data is persisted, the 'ContractServiceType' forms start duplicating, once for the dummy and again for any that have been persisted to the database for that 'Contract'. This is the closest I've been so far, so I decided I could write logic in the controller to decide which dummy entities to create later, based on which have already been associated with the 'Contract', though it seems like there should be a better solution.
So my solution right now is to use the 'Service' entity repo, to query for all unique 'Services', and then use those services to generate a number of dummy 'ContractService' entities to use when the form is built. It yields what I am looking for, in that I get a checkbox(to "enable") and adjustedPrice field for each of the possible 'services'.
Is this the best way to handle this?
If so, is there a way to reference the associated 'Service' name (should be available via the joined table?) in the ContractService form builder?
Couldn't you do a ManytoMany?
This is how ZfcRbac does it in ZF2/Doctrine2
RbacRole Entity
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Admin\Entity\RbacPermission", inversedBy="role")
* #ORM\JoinTable(name="rbac_role_permission",
* joinColumns={
* #ORM\JoinColumn(name="role_id", referencedColumnName="role_id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="perm_id", referencedColumnName="perm_id")
* }
* )
*/
protected $perm;
/**
* Constructor
*/
public function __construct()
{
$this->perm = new \Doctrine\Common\Collections\ArrayCollection();
}
RbacPermission Entity
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Admin\Entity\RbacRole", mappedBy="perm")
*/
protected $role;
/**
* Constructor
*/
public function __construct()
{
$this->role = new \Doctrine\Common\Collections\ArrayCollection();
}

Categories