Doctrine 2 and Many-to-many link table with an extra field - php

(Sorry for my incoherent question: I tried to answer some questions as I was writing this post, but here it is:)
I'm trying to create a database model with a many-to-many relationship inside a link table, but which also has a value per link, in this case a stock-keeping table. (this is a basic example for more problems I'm having, but I thought I'd just test it with this before I would continue).
I've used exportmwb to generate the two Entities Store and Product for this simple example, both are displayed below.
However, the problem now is that I can't figure out how to access the stock.amount value (signed int, as it can be negative) using Doctrine. Also, when I try to create the tables using doctrine's orm:schema-tool:create function
This yielded only two Entities and three tables, one as a link table without values and two data tables, as many-to-many relationships aren't entities themselves so I can only have Product and Store as an entity.
So, logically, I tried changing my database model to have stock as a separate table with relationships to store and product. I also rewrote the fieldnames just to be able to exclude that as a source of the problem:
Then what I found was that I still didn't get a Stock entity... and the database itself didn't have an 'amount'-field.
I really needed to be able to bind these stores and products together in a stock table (among other things)... so just adding the stock on the product itself isn't an option.
root#hdev:/var/www/test/library# php doctrine.php orm:info
Found 2 mapped entities:
[OK] Entity\Product
[OK] Entity\Store
And when I create the database, it still doesn't give me the right fields in the stock table:
So, looking up some things here, I found out that many-to-many connections aren't entities and thus cannot have values. So I tried changing it to a separate table with relationships to the others, but it still didn't work.
What am I doing wrong here?

A Many-To-Many association with additional values is not a Many-To-Many, but is indeed a new entity, since it now has an identifier (the two relations to the connected entities) and values.
That's also the reason why Many-To-Many associations are so rare: you tend to store additional properties in them, such as sorting, amount, etc.
What you probably need is something like following (I made both relations bidirectional, consider making at least one of them uni-directional):
Product:
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Table(name="product") #ORM\Entity() */
class Product
{
/** #ORM\Id() #ORM\Column(type="integer") */
protected $id;
/** ORM\Column(name="product_name", type="string", length=50, nullable=false) */
protected $name;
/** #ORM\OneToMany(targetEntity="Entity\Stock", mappedBy="product") */
protected $stockProducts;
}
Store:
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Table(name="store") #ORM\Entity() */
class Store
{
/** #ORM\Id() #ORM\Column(type="integer") */
protected $id;
/** ORM\Column(name="store_name", type="string", length=50, nullable=false) */
protected $name;
/** #ORM\OneToMany(targetEntity="Entity\Stock", mappedBy="store") */
protected $stockProducts;
}
Stock:
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Table(name="stock") #ORM\Entity() */
class Stock
{
/** ORM\Column(type="integer") */
protected $amount;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Entity\Store", inversedBy="stockProducts")
* #ORM\JoinColumn(name="store_id", referencedColumnName="id", nullable=false)
*/
protected $store;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Entity\Product", inversedBy="stockProducts")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", nullable=false)
*/
protected $product;
}

Doctrine handles many-to-many relationships just fine.
The problem that you're having is that you don't need a simple ManyToMany association, because associations can't have "extra" data.
Your middle (stock) table, since it contains more than product_id and store_id, needs its own entity to model that extra data.
So you really want three classes of entity:
Product
StockLevel
Store
and two associations:
Product oneToMany StockLevel
Store oneToMany StockLevel

Related

Doctrine ManyToMany Relationship

Very simple (I thought!), I have an Invoice entity, and a Coupon entity. Invoices can have many coupons applied to them. Coupons conversely, can be used in many invoices.
Excluding getters/setters:
Invoice
namespace Application\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="invoices")
*/
class Invoice
/**
* #ORM\ManyToMany(targetEntity="Application\Entity\Coupon")
* #ORM\JoinTable(name="invoices_coupons")
*/
protected $coupons;
public function addCoupon( Coupon $coupon ){
if( !$this->coupons )
$this->coupons = new ArrayCollection();
$this->coupons->add($coupon);
}
}
Coupon
/**
* #ORM\Entity
* #ORM\Table(name="coupons", indexes={#ORM\Index(name="code_idx", columns={"code"})})
*/
class Coupon implements CandidateInterface
{
/**
* #var \Ramsey\Uuid\Uuid
*
* #ORM\Id
* #ORM\Column(type="uuid")
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=32, unique=true)
*/
protected $code;
}
When I run the helper tool to generate the schema, as expected, it creates a join table invoices_coupons that contains coupon_id, invoice_id (perfect).
So in the code, I have an existing stored Invoice and a similarly existing Coupon.
Seems I cannot do:
// runs a QB to return the coupon, returns a Coupon Entity
$coupon = $couponMapper->getActiveByCode('SAVEBIG');
$invoice->addCoupon( $coupon );
$invoiceMapper->getEntityManager()->update( $invoice );
$invoiceMapper->getEntityManager()->flush();
I get this error:
A new entity was found through the relationship \Application\Entity\Invoice#coupons that was not configured to cascade persist operations for entity: (coupon toString). To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={\u0022persist\u0022}).
Now, I don't want this to create new coupons; any why is it trying this? The coupon already exists, it was loaded from the ER, and it is being added to an existing entity.
If I do what the error message says, it tries to duplicate a new Coupon into the coupon table.
Thanks for your suggestions.
Doctrine\ORM\EntityManager::update() doesn't appear to be a thing that exists. You shouldn't have to do anything between the addCoupon() call and the flush() call.
If simplifying your code doesn't magically fix it, your next step should be to ensure that $couponMapper->getEntityManager() === $invoiceMapper->getEntityManager().
It's not clear how you're instantiating these mapper classes, but it's important to understand that each EntityManager maintains its own internal Identity Map for entities. So if your DIC for some reason is instantiating two different EMs (one for each Mapper), then $invoiceMapper's EM doesn't recognize the $coupon as a managed entity.
It would be weird for that to be the case. Assuming you're using ZF2's ServiceManager, you'd have to explicitly set your EntityManger service to not be shared.
But somehow having two different EntityManagers is the most obvious thing I can think of given the code you've provided.

Doctrine2 - Annotation - "findOneBy" on column in "OneToOne" joined table

Using Doctrine2, I am trying to execute a findOneBy on an entity, where I have a joined a table using OneToOne, and I want to search for columns in the joined table.
The two PHP entities in play are (in simplified versions):
Page:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="Page")
* #ORM\HasLifecycleCallbacks()
*/
class Page extends EntityInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="boolean")
*/
public $isActive;
/**
* #ORM\OneToOne(targetEntity="\PageLocalization")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id", referencedColumnName="pageId")
* })
**/
public $pageLocalization;
}
PageLocalization:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="PageLocalization")
* #ORM\HasLifecycleCallbacks()
*/
class PageLocalization extends EntityInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
public $pageId;
/**
* #ORM\Column(type="string")
*/
public $localeCode;
/**
* #ORM\Column(type="string")
*/
public $title;
}
The entities work and I can extract data just fine through e.g. $entityRepository->findOneBy(["id"=>1]).
Now, for instance, I want to search for Page.id = 1, Page.isActive = true and PageLocalization.localeCode = "en-US". How is this search performed?
My attempt below doesn't work.
$entityRepository->findOneBy([
"id" => 1,
"isActive" => true,
"pageLocalization" => [
"localeCode" => "en-US"
]
]);
I had no luck finding the answer through Google, Stackoverflow, or the Doctrine2 documentation.
The most frequent solutions I see is that people settle for are constructing the queries manually using $entityManager->createQueryBuilder(). However, I feel this approach defies the purpose of having the entities.
Is this type of search possible at all using purely annotations and entities?
From what I've gathered thus far, it is indeed not possible to perform a search like this across multiple entities (joined tables) using any of the EntityRepository's "find" methods; find, findAll, findBy, and findOneBy.
Stackoverflow answers to similar questions:
https://stackoverflow.com/a/16730168/1879194
https://stackoverflow.com/a/12258270/1879194
https://stackoverflow.com/a/19489147/1879194
The list goes on...
Conclusion
One must perform this type of searches using DQL or the QueryBuilder.
Thoughts
I find this limitation to be highly obnoxious and a serious feature inadequacy in Doctrine2.
If half the time, when I want to touch the database, I have to build the queries myself, why would I bother adhering to and maintaining two drastically different approaches; (1) an ORM with entities reflecting database tables and (2) constructing pure SQL requests through a DBAL, e.g. PDO or Doctrine's DQL?
Doctrine2 considerations (slightly off-topic)
I really want to learn how to work with and utilize Doctrine2 efficiently. And while Doctrine2 does have a grand array of nice features, it certainly also consists of some massive rebar walls, which one keeps running into face-first. Google and Stackoverflow certainly has a bunch of "Why can't I ...?" questions related to Doctrine2 and joining tables in particular.

Doctrine MappedSuperclass and unique constraints

I have a scenario where I need to use the MappedSuperclass functionality of Doctrine (using Symfony2), and also create a unique constraint on some superclass columns. Let's say:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\MappedSuperclass
*/
class Base
{
/**
* #ORM\Column(type="integer", nullable=false)
*/
private $someColumn;
}
/**
* #ORM\Entity
* #ORM\Table(uniqueConstraints={#ORM\UniqueConstraint(name="column_idx", columns={"someColumn"})})
*/
class Concrete extends Base
{
}
The problem is at processing of #ORM\Table annotation during schema generation:
[Doctrine\DBAL\Schema\SchemaException]
There is no column with name 'someColumn' on table 'Concrete'.
Is there a way to define a unique constraint of a mapped superclass?
Since the answer author didn't post the answer himself, let me quote him:
Try to use protected instead of private for entity field. You should always use protected or public for entity fields

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 mapped superclass in the middle of class table inheritance

I currently have a model structure as follows:
/**
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="related_type", type="string")
* #ORM\DiscriminatorMap({"type_one"="TypeOne", "type_two"="TypeTwo"})
*/
abstract class BaseEntity {
... (all the usual stuff, IDs, etc)
/**
* #ORM\OneToMany(targetEntity="Comment", mappedBy="baseEntity")
*/
private $comments;
}
/**
* #ORM\Entity
*/
class TypeOne extends BaseEntity {
/**
* #ORM\Column(type="string")
*/
private $name;
/**
* #ORM\Column(type="string")
*/
private $description;
}
/**
* #ORM\Entity
*/
class TypeTwo extends BaseEntity {
/**
* #ORM\Column(type="string")
*/
private $name;
/**
* #ORM\Column(type="string")
*/
private $description;
}
/**
* #ORM\Entity
*/
class Comment {
... (all the usual stuff, IDs, etc)
/**
* #ORM\ManyToOne(targetEntity="BaseEntity", inversedBy="comments")
*/
private $baseEntity;
}
The idea here is to be able to tie a comment to any of the other tables. This all seems to be working ok so far (granted, I'm still exploring design options so there could be a better way to do this...), but the one thing I've noticed is that the subclasses have some common fields that I'd like to move into a common parent class. I don't want to move them up into the BaseEntity as there will be other objects that are children of BaseEntity, but that won't have those fields.
I've considered creating a MappedSuperclass parent class in the middle, like so:
/**
* #ORM\MappedSuperclass
*/
abstract class Common extends BaseEntity {
/**
* #ORM\Column(type="string")
*/
private $name;
/**
* #ORM\Column(type="string")
*/
private $description;
}
/**
* #ORM\Entity
*/
class TypeOne extends Common {}
/**
* #ORM\Entity
*/
class TypeTwo extends Common {}
I figured this would work, but the doctrine database schema generator is complaining that I can't have a OneToMany mapping on a MappedSuperclass. I didn't expect this to be a problem as the OneToMany mapping is still between the root BaseEntity and the Comment table. Is there a different structure I should be using, or other way to make these fields common without adding them on the BaseEntity?
From the Docs:
A mapped superclass is an abstract or concrete class that provides
persistent entity state and mapping information for its subclasses,
but which is not itself an entity. Typically, the purpose of such a
mapped superclass is to define state and mapping information that is
common to multiple entity classes.
That said, how can you associate one entity with one that is not?
More from the docs:
A mapped superclass cannot be an entity, it is not query-able and
persistent relationships defined by a mapped superclass must be
unidirectional (with an owning side only). This means that One-To-Many
assocations are not possible on a mapped superclass at all.
Furthermore Many-To-Many associations are only possible if the mapped
superclass is only used in exactly one entity at the moment. For
further support of inheritance, the single or joined table inheritance
features have to be used.
Source: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html
Update
Because your MappedSuperClass extends BaseEntity it also inherits the BaseEntity's associations, as if it were its own. So you effectively DO have a OneToMany on a MappedSuperClass.
To get around it, well, you'd need to modify/extend doctrine to work the way you want.
As far as native functionality goes you have two options:
Class Table Inheritance
You Common class and the resulting DB representation would have the common fields and child classes will now only have the fields specific to themselves. Unfortunately this may be a misrepresentation of your data if you are simply trying to group common fields for the sake of grouping them.
Make Common an Entity
It appears that all a Mapped Super Class is is an Entity that isn't represented in the DB. So, make common a Entity instead. The downside is that you'll end up with a DB table, but you could just delete that.
I recommend that you take a second look at your data and ensure that you are only grouping fields if they are common in both name and purpose. For example, a ComputerBox, a ShoeBox, a Man, and a Woman may all have the "height" property but in that case I wouldn't suggest have a Common class with a "height" property that they all inherit from. Instead, I would have a Box with fields common to ComputerBox and ShoeBox and I'd have a Person with fields common to Man and Woman. In that situation Class Table Inheritance or single table if you prefer would work perfectly.
If your data follows that example go with Single Table or Class Table Inheritance. If not, I might advise not grouping the fields.

Categories