Many-to-many self relation with extra fields? - symfony

I am trying to develop a friends system, and I need a Many-To-Many relation on my User entities ; for now, this is what I've done :
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="friends")
*/
protected $friendsWith;
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="friendsWith")
* #JoinTable(name="friends",
* joinColumns={#JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="friend_user_id", referencedColumnName="id")}
* )
*/
protected $friends;
But I would like to have some extra fields for these relations, for example the creation date or the state (accepted, pending, ...) ; I've created another entity "Friend", and I would like this entity to be used as a link between friends. But I don't really know how to manage this...
Do you have some ideas ?
Thanks !

I'm afraid you need an extra class to make such an association.
Here is the tip from doctrine documentation:
Why are many-to-many associations less common? Because frequently you
want to associate additional attributes with an association, in which
case you introduce an association class. Consequently, the direct
many-to-many association disappears and is replaced by
one-to-many/many-to-one associations between the 3 participating
classes.
http://www.doctrine-project.org/docs/orm/2.1/en/reference/association-mapping.html#many-to-many-unidirectional
I guess it should be Friend -> Special Association Class (with fileds: user_id, friend_id, date created) ->Friend.
And you associate Friend to special class in two filed $myFriends and $imFriendOf :)

Related

Symfony: Ordered one-to-many relationship

I have two separate entities that I want to link by a one-to-many relationship. But I want this relationship to be ordered, meaning every time I call on the first entity, the elements of the second entity come in a pre-ordered way. I cannot use 'order by' calls because this order has nothing to do with the fields of the second entity. I thought about having one field of the first entity be an array of entities, but I'm not sure how to accomplish that either..
EDIT
So far I have something like this:
/**
* #ORM\OneToMany(targetEntity="FusionDesign\BlogBundle\Entity\Element", mappedBy="page")
*/
private $elements;
and
/**
* #ORM\ManyToOne(targetEntity="FusionDesign\BlogBundle\Entity\Page", inversedBy="elements")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id")
*/
private $page;
I'm aware that I can put "ORDER BY whatever ASC" somewhere in there but that orders according to a column in Element, and that's not what I need, because Element entities and Page entities would never be persisted at the same time, nor by the same process. What I want to do is constructing a basic CMS where the user could generate new pages. First choose the kind of elements a page could potentially have (like header image, banner, title, and so on) and persist Element entities with fields describing the html, routing and controller content according to those choices. Then, when a new page is created, give the user the choice to order those potential elements at will, and bind Element entities following an order that reflects the layout desired.
I thought about having something like this
/**
* #var array
*
* #ORM\Column(name="structure", type="array")
*/
private $structure;
Where the array stores Element entities but I have no idea how to do that.
You just need to define the orderBy attribute in doctrine's mapping configuration for the relation.
YAML Mapping Example:
'Example\Entity\Article':
# [..]
oneToMany:
images:
targetEntity: 'Example\Entity\Article\Image\ArticleImage'
mappedBy: 'article'
orderBy: # <--- here
position: 'ASC'

Entity associated with non-entity

I have an interface SupplierInterface with 2 implementations: B2BSupplier (a Doctrine entity), RetailSupplier (a static object).
<?php
namespace MyBundle\Model;
interface SupplierInterface {
const B2B = 'B2B';
const RETAIL = 'Retail';
/**
* #return string
*/
public function getSupplierType();
/**
* #return string
*/
public function __toString();
}
Another entity, Supply has a many-to-one relationship with a Supplier. Normally this isn't problematic. But because RetailSupplier is not a Doctrine entity, I'm a bit flummoxed about how to proceed.
Supply looks like this:
<?php
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Blameable\Traits\BlameableEntity;
use Gedmo\Timestampable\Traits\TimestampableEntity;
/**
* Supply
*
* #ORM\Table(name="cir_supply")
* #ORM\Entity()
*/
class Supply
{
use BlameableEntity;
use TimestampableEntity;
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="B2BSupplier")
* #ORM\JoinColumn(name="supplier_id", referencedColumnName="id", nullable=true)
*/
protected $supplier; // <-- PROBLEM, since supplier could be B2BSupplier entity, or it could be vanilla object RetailSupplier
/**
* #ORM\ManyToOne(targetEntity="Chemical", inversedBy="supplies")
* #ORM\JoinColumn(name="chemical_id", referencedColumnName="id", nullable=false)
*/
protected $chemical;
/**
* #ORM\Column(name="external_id", type="string")
*/
protected $externalId;
//getters and setters ...
}
How do I specify a Doctrine relationship when that relationship might not always be valid?
From my experience I'm 99% sure you can't do what you want in your current setup. That being said, there are a few workarounds I can think of. Also before I go into the workarounds. You should think if you really want OneToOne relation on 'supplier' or will ManyToOne work better. OneToOne has some Lazy loading issues and also Workaround 3 work better with ManyToOne.
Workaround 1:
Remove the relation and make the supplier filed contain the id, without having a relation defined.
Extend SupplierRepository 'find' method to handle the cases where id is
2.1 'null' there is no relation in witch case it returns RetailSupplier
2.2 call parent::find for all other cases
2.3 Optional: if null relations are required change 2.1 to use '0' instead of null (adds con 3)
Pros:
fast to achieve from your current setup
keep database foreign key (if step 2.3 is ignored)
Cons:
hidden behavior of the 'find' method
you loose the your doctrine relation
not scalable for other types of Suppliers
source of the information is split between the app and the database
if step 2.3 is required, you loose database foraign key ('0' will not be a foraign key)
Workaround 2:
Modify getSupplier to return RetailSupplier if $this->supplier is null
Modify setSupplier to set null if $supplier is instance of RetailSupplyer
Optinal: Change the first 2 steps to handle '0' as RetailSupplyer and 'null' as no relation
Pros:
fast to achieve from your current setup
keep database foreign key (if step 3 is ignored)
keep doctrine relation
Cons:
hidden behavior of the setter and getter
not scalable for other types of Suppliers
if step 3 is required, you loose database foraign key ('0' will not be a foraign key)
source of the information is split between the app and the database
Workaround 3 (doctrine inheritance mapping):
Create an abstract (called Supplier) this will be inherited by RetailSupplyer and B2BSupplier
Add inheritance metadata to Supplier abstract something like this
Create an entity for RetailSupplyer and a database table with one single line to start (the first RetailSupplier)
Change your database to match your inheritance mapping settings (for more info http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html)
Change your relation to ManyToOne on $supplier and make it to point to Supplier
Pros:
source of the information is only the database
no hidden behavior in your code
scalable for other types of suppliers and other more retail suppliers
Cons:
harder to achieve from your current setup (database changes, new doctrine setup, possibly some refactor)
pros/cons: Depending on the selected inheritance type you can have full relation path in your database (with foraign key), or you can have no relations. This is up to you ;) after you read the documentation for inheritance mapping.
PS: If I had to choose i will go with Workaround 3. It is hardest to achieve, but solid do it.
Hope this helps and happy coding
Alexandru Cosoi

How to limit OneToMany/ManyToOne associations depth/loop in Doctrine2?

I've 3 doctrine entities. One is User, second is Product and third is ProductUsers.
So, User have OneToMany association with ProductUsers and the same Product have OneToMany association with ProductUsers. ProductUsers has ManyToOne association with both User and Product. Like so:
class Product
{
/**
* #var ProductUsers
*
* #ORM\OneToMany(targetEntity="ProductUsers", mappedBy="product")
*/
private $productUsers;
}
class ProductUsers
{
/**
* #var Product
*
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Product", inversedBy="productUsers")
* #ORM\JoinColumn(name="product_ID", referencedColumnName="ID")
*/
private $product;
/**
* #var User
*
* #ORM\Id
* #ORM\ManyToOne(targetEntity="User", inversedBy="productUsers")
* #ORM\JoinColumn(name="user_ID", referencedColumnName="ID")
*/
private $user;
// extra fields ...
}
class User
{
/**
* #var ProductUsers
*
* #ORM\OneToMany(targetEntity="ProductUsers", mappedBy="user")
*/
private $productUsers;
}
A user can use multiple products and a product can have multiple users. ProductUsers has some extra info about the relation other than just the relation.
The problem is when I fetch one User object it comes with associated ProductUsers and it's associated Product. Not only that but the Product also comes with all it's associated ProductUsers and it's respective User objects which is quite an overhead.
This question closely relates to my problem.
I'm looking to limit that at doctrine level just like what JMSSerializerBundle MaxDepth does. Is there a way to limit such overhead in doctrine?
I faced this issue long time back. I tried lazy loading. Which didn't work as expected and that was not a proper solution to my issue. So I did some R&D and came up with a solution that I don't need a bidirectional relationship from Product to ProductUsers.
I can manage same relationship with unidirectional handling only from ProductUsers side. You will need One-To-Many Association when you need a cascade-persist or similar feature. I wrote a small blog regarding this as well.
So, for your solution, just have Many-To-One association from ProductUsers with both Product and User entity. You will not need any change in your database association.
And when you need Products associated for a single user, you can always save a Querybuilder in Repository to use when you need associated data.
It will save a lot of performance. Hope it helps!

Definitions of one-to-many, many-to-many etc

I'm building a database application using Doctrine2. I'm getting somewhat confused by the foreign key mappings. I'm wondering, have I got these examples correct:
One-To-One: An X has exactly one Y.
One-To-Many: An X can have multiple Ys.
Many-To-One: Multiple Xs can have the same Y.
Many-To-Many: Multiple Xs can have multiple Ys.
This is the specific situation that got me confused:
A User has exactly one HomeTown. Many users can belong to the same home town, so the link for the User is:
/**
* #ORM\ManyToOne(targetEntity="HomeTown", inversedBy="localUsers")
*/
$homeTown;
And, the corresponding HomeTown link is:
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="homeTown")
*/
$localUsers;
OR is it:
/**
* #ORM\OneToMany(targetEntity="User", mappedBy="homeTown")
*/
$localUsers;
Some clarification would be much appreciated!
I've been looking at http://doctrine-orm.readthedocs.org/en/latest/reference/association-mapping.html
When you have OneToMany association, the inverted has to be ManyToOne. Saying that, your second option is correct.
TIP: Using Doctrine CLI command orm:validate-schema might also help to identify this issue.
The full path in Symfony app: php app/console doctrine:schema:validate
If you want one city to have many users the mapping should be as it follows
Entity City
/**
* #ORM\OneToMany(targetEntity="User", mappedBy="homeTown")
*/
private $users;
...
public function __construct()
{
$this->users = new ArrayCollection();
}
...
Entity User
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="users")
* #ORm\JoinColumn(name="home_town", referencedColumnName="id")
*/
private $homeTown;
This mapping shows us that the City which is owning side has On-To-Many relation with User(target Entity). Respectively the User which is inversed side has to be annotated with ManyToOne relation because many users have same city. Of course, here the target entity should be City. Its important to specify which column is pointing the foreignkey with the referencedColumnName attribute in JoinColumn annotation. It shows what column of other table points this key. In this example in table User there is column named "home_town" which is a foreign key pointing to column id of table City
In ManyToOne relation you shod use JoinColumn annotation
This mapping is also Bidirectional.
You can make id Unidirectional as in the User Entity do not use "inversedBy=" attribute and remove OneToMany annotation with $user property from the City entity. It is something like when you have to know the city of a particular user, but you do not need to know all users for a specific city

symfony2: How to remove related entity with doctrine / restrict annotation?

I have some bit of troubles with delete constraint in an entity.
I have an entity merchandise and an entity vehicle with a relation many to one in merchandise, so a merchandise only could be in one vehicle, and a vehicle could have many merchandise. So I have:
class Merchandise{
/**
* #ORM\ManyToOne(targetEntity="Vehicle",inversedBy="merchandise")
* #ORM\JoinColumn(name="vehicle", referencedColumnName="id")
*/
private $vehicle;
}
class Vehicle{
/**
* #ORM\OneToMany(targetEntity="Merchandise",mappedBy="vehicle")
*/
private $merchandise;
}
What I want to get is that when I try to delete a Merchandise which have a vehicle, the Merchandise couldn't be deleted.
But I don't know how can I put an ORM Level restrict constraint. I tried restrict={"remove"} but it doesn't exist in #ORM\OneToMany.
I also try to put a preRemove function which return false, but it doesn't work :(
Any idea?
Thanks!!!
ManyToOne / inversedBy is the OWNING side of the bidirectional relation from doctrine's point of view - which can lead to confusion.
To resolve your issue add cascade operation to your merchandise entity. example:
/**
* #ORM\ManyToOne(targetEntity="Vehicle",mappedBy="merchandise", cascade={"all"})
*/
cascade can be set to a combination of :
persist
remove
merge
detach
all
Improve further by adding cascade ( ORM-level ) to your Vehicle entity aswell. example:
/**
* #ORM\OneToMany(targetEntity="Merchandise", mappedBy="vehicle", cascade={"persist","remove"})
*/
... or use onDelete ( database-level ) with one of
SET NULL
CASCADE
... like this
/**
* #ORM\OneToMany(targetEntity="Merchandise", inversedBy="vehicle", onDelete="CASCADE")
*/
Now if you remove a Vehicle - the related Merchandise entities will be removed. Added Merchandises will automatically be saved.
... finally update your schema and drop -> re-create your database if constraints have not been updated and errors occur. Make sure both sides use the cascade option.
Read more in the documentation chapter Transitive persistence / Cascade Operations.

Resources