Doctrine 2 relationship usage - php

I have two database tables
Articles
Archive
Each article can have multiple records in Archive table.
This is how Archive entity looks like (shown only needed to understand the question)
/**
* Archive
*
* #ORM\Table(name="archive")
* #ORM\Entity
*/
class Archive
{
.....................................................
/**
* #var Articles
*
* #ORM\ManyToOne(targetEntity="Articles")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="article_id", referencedColumnName="id")
* })
*/
private $article;
.....................................................
/**
* Set article
*
* #param Articles $article
* #return Archive
*/
public function setArticle(\Entities\Articles $article = null)
{
$this->article = $article;
return $this;
}
/**
* Get article
*
* #return Articles
*/
public function getArticle()
{
return $this->article;
}
....................................................
}
In this entity there is a reference to Article entity BUT in Article entity there is no reference to Archive entity.
So the question is - do I need reference to Archive in Article entity and what benefits and downsides it will have?
As I understand having a lot of references is bad (http://docs.doctrine-project.org/en/latest/reference/best-practices.html#constrain-relationships-as-much-as-possible). So where is that point where I can tell do I need it or not?

It's primarily a decision of architecture and convenience.
Does Article need to know about the archived entries?
Do I need to access archived entries via an article?
The first case is mostly just about how you want to represent your data model. Is an article something that should know of its items in archive? Or does only the archive need to know about articles? How is their relation in the data model?
The second case is more about programming convenience: If you need to often access archived items for an article, it can be more convenient to have $article->getArchives() (or whatever) vs. having to fetch them via a repository or a query. This may go against the data model, so you need to weigh both cases and decide for yourself how to model it and what makes most sense.

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);
}

How to serialize slice of ArrayCollection using JMS Serializer?

I want to serialize into JSON entity Category with collection of Presentation entities (see below) to use for REST API.
The endpoint will look something like this /api/v1/categories/1
When dataset is small and when Category only has only 5-10 related Presentations then the resulting response is not too large. However when Category starts to have let's say 100 or 200 related Presentations then obviously I do not want to return all of them, but would like to "paginate" the results, eg. when calling endpoint:
/api/v1/categories/1?page=2 - would return only "2nd page"
/api/v1/categories/1/page=3 - would return "3rd page"
or even it can be with offset and limit:
/api/v1/categories/1?offset=20&limit=10
but the problem is: how to make JMS serializer serialize only a slice of the collection?
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\CategoryRepository")
*/
class Category
{
/**
* #var string
* #ORM\Column(type="string")
* #JMS\Expose()
* #JMS\Groups({"get-category"})
*/
private $title;
// ...
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Presentation", mappedBy="categories", fetch="EXTRA_LAZY")
* #JMS\Groups({"get-category"})
* #JMS\Expose()
*/
private $presentations;
// ...
}
ps. I know that for example if I want to get always first 5 elements of the collection, I can add created #VirtualProperty and slice the doctrine ArrayCollection as shown below. But the problem here is that I cannot pass the offset parameters to this method. As it would be called internally by JMSSerializer somewhere...
/**
* #JMS\VirtualProperty()
*
*/
public function getFirstFivePresentations(){
return $this->presentations->slice(0,5);
}
You are trying to implement the incorrect approach in your REST API. Each entity must have it's own path.
The right way is to have two different endpoints:
/api/v1/categories/1 -> Serialized category with no presentations
/api/v1/categories/1/presentations -> Serialized collection of presentaions
And here you should use pagination
/api/v1/categories/1/presentations?offset=20&limit=10

How to restrict associations to a subset of another association in Doctrine?

I got a bit stuck with multiple mappings of the same object in Doctrine. The app is build on Symfony btw, hence the slightly different annotations.
Basically I have the following objects:
Organisation: an umbrella holding attributes about an organisation
Department: a department within the organisation
User: a generic user object
Those objects are related as follows:
An organisation always has one and only one owner, which is a User
An organisation has many members, which are all User's
A department consists of many User's, but only members of the Organisation the Department is a part of are allowed
I'm a bit stuck at the third requirement... First of all, this is how my objects more or less look like atm:
/**
* #ORM\Entity
* #ORM\Table(name="organisations")
*/
class Organisation
{
// ...
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="organisation")
*/
private $owner;
/**
* ORM\OneToMany(targetEntity="User", mappedBy="organisation")
*/
private $members
}
/**
* #ORM\Entity
* #ORM\Table(name="departments")
*/
class Department
{
// ...
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="departments")
*/
private $members
/**
* #ORM\ManyToOne(targetEntity="Organisation", inversedBy="departments")
*/
private $organisation;
}
/**
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User
{
// ...
/**
* The organisation this user "owns"
*
* #ORM\OneToOne(targetEntity="Organisation", mappedBy="owner", nullable=true)
*/
private $owning_organisation;
/**
* #ORM\ManyToOne(targetEntity="Organisation", inversedBy="members")
*/
private $organisations;
/**
* #ORM\ManyToMany(targetEntity="Department", inversedBy="members")
* #ORM\JoinTable(name="users_departments")
*/
private $departments;
}
Now this basically works, if and only of in the controllers I do all the checking (something like (if( $user->isPartOfOrganisation($department-getOrganisation()) { $department->addMember($user); }).
But is there a way to restrict possible object associations on design level? So basically what I want is that if a user is added to a department, it is solely possible if the user is already part of the organisation the department is also a part of. Or should I do the check in the addMember() method of the Department object? I can imagine (but cannot find it) that there is some kind of a subset-restriction (Department::members is subset of Organisation::members).
To implements this check low-level as possible (nearest to the db) I think the only solution is a Doctrine Event Listener that in the pre-persist event check for your custom constraint. Read more about Doctrine Event System .
BTW I think you can manage this situation in a more simply manner: I suggest you to incapsulate the business logic into a service (so you can reuse it more simply) and use it in a custom validator that you will use in the form where you manage this situation.
Let me know if you need more tips to develop one of this solutions or if you found something more useful.
Hope this help

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 ManyToMany embedded forms

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.

Categories