I have a question out of curiosity about the inner workings of Doctrine2. I as a User see a really clean and robust interface, but there must be some heavy magic at work under the hood.
When I generate a simple entity it looks something like this:
class SimpleEntity
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $title;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
}
As you will notice one thing is conspicuously absent, there is no way to set the id, but nevertheless doctrines factories return entities with set ids. How can that work? I have tried to look through the source, but lost the track somewhere.
How can it be possible to overwrite protected values without being in the class hierarchy?
The answer is Reflection. See http://www.doctrine-project.org/docs/orm/2.1/en/tutorials/getting-started-xml-edition.html#a-first-prototype
Without digging into the Doctrine source, I'd say it makes use of ReflectionProperty::setValue()
Related
I tried find solution to my issue but didn't find anything.
I use: Symfony, Doctrine, PhpUnit
I have one entity class InvoiceNumerator:
/**
* InvoiceNumerator
*
* #ORM\Table(name="invoice_numerator")
* #ORM\Entity(repositoryClass="AppBundle\Repository\InvoiceNumeratorRepository")
*/
class InvoiceNumerator
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="translatedFormat", type="string", length=64)
*/
private $translatedFormat;
/**
* #var int
*
* #ORM\Column(name="currentValue", type="integer", options={"default": 0})
*/
private $currentValue = 0;
/**
* #var string
*
* #ORM\Column(name="current_number", type="string", length=64)
*/
private $currentNumber = '';
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set translatedFormat
*
* #param string $translatedFormat
*
* #return InvoiceNumerator
*/
public function setTranslatedFormat($translatedFormat)
{
$this->translatedFormat = $translatedFormat;
return $this;
}
/**
* Get translatedFormat
*
* #return string
*/
public function getTranslatedFormat()
{
return $this->translatedFormat;
}
/**
* Set currentValue
*
* #param integer $currentValue
*
* #return InvoiceNumerator
*/
public function setCurrentValue($currentValue)
{
$this->currentValue = $currentValue;
return $this;
}
/**
* Get currentValue
*
* #return int
*/
public function getCurrentValue()
{
return $this->currentValue;
}
/**
* #return string
*/
public function getCurrentNumber(): string
{
return $this->currentNumber;
}
/**
* #param string $currentNumber
* #return InvoiceNumerator
*/
public function setCurrentNumber(string $currentNumber): InvoiceNumerator
{
$this->currentNumber = $currentNumber;
return $this;
}
}
and I need in my tests to mock this class, but my setters should be left the same - no stubs - working code.
To mock this class, I made simple mock method:
public function getInvoiceNumerator()
{
$invoiceNumerator = $this->createMock(InvoiceNumerator::class);
$invoiceNumerator->method('getTranslatedFormat')
->willReturn('FS-CM/{n}/2018/01');
$invoiceNumerator->method('getCurrentValue')
->willReturn('1');
$invoiceNumerator->method('getCurrentNumber')
->willReturn('FS-CM/1/2018/01');
return $invoiceNumerator;
}
but in this case my setters are not working.
I can also set values on new Entity object:
public function getInvoiceNumerator()
{
$invoiceNumerator = new InvoiceNumerator();
$invoiceNumerator->setTranslatedFormat('FS-CM/{n}/2018/01');
$invoiceNumerator->setCurrentValue(1);
$invoiceNumerator->setCurrentNumber('FS-CM/1/2018/01');
return $invoiceNumerator;
}
In this case my setters working properly.
Question:
Is there any better way to do this? What is the best practice?
You almost have the answer in your question “Phpunit partial mock + proxy Entity”: there is a createPartialMock() method which you can use like this:
$invoiceNumerator = $this-> createPartialMock(
InvoiceNumerator::class,
['nameOfMockedMethod1', 'nameOfMockedMethod2']
);
This method has been available in PHPUnit 5.5 and newer. If you are using an older version, you can use setMethods(), but have to call it on the result returned by getMockBuilder(), not on the object returned by createMock() (which is the reason of the error you got after trying the approach from the 1st answer):
$subject = $this->getMockBuilder(MyClass::class)
->setMethods(['method1', 'method2'])
->getMock();
However, please note that createPartialMock() does slightly more. For instance, it will automatically disable the original constructor – which is almost always what you want in your tests (and what you have to do explicitly when using setMethods()). See documentation for exact information.
Basically you can set your mock to only mock specific methods:
$invoiceNumerator = $this->getMockBuilder(InvoiceNumerator::class)
->setMethods(["getTranslatedFormat","getCurrentValue", "getCurrentNumber"])
->getMock();
according to the documentation
setMethods(array $methods) can be called on the Mock Builder object to specify the methods that are to be replaced with a configurable test double. The behavior of the other methods is not changed. If you call setMethods(null), then no methods will be replaced.
Update:
Since PHPUnit 8 the setMethods function has been deprecated and replaced with onlyMethods for methods that exist on the mocked class and addMethods for methods that don't yet exist on the mock class (e.g. they will be implemented in the future but you want to text their dependencies assuming they already exist).
I'm looking for a solution to automatically translate the entities of my Symfony application. I'm stuck with a legacy database where translations are stored in the same table as extra fields:
id | name | name_de | name_fr
1 | cat | Katze | chat
2 | dog | Hund | chien
My entity is mapped accordinggly:
class Animal
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #var integer
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=64, nullable=false)
*
* #var string
*/
private $name;
/**
* #ORM\Column(name="name_de", type="string", length=64, nullable=false)
*
* #var string
*/
private $nameDe;
/**
* #ORM\Column(name="name_fr", type="string", length=64, nullable=false)
*
* #var string
*/
private $nameFr;
/* Followed by getters and setters */
}
I've already looking into the Translatable extension, but that cannot match my database schema. I also started with a custom annotation hooking into the postLoad event, but then I was stopped by the simple issue that postLoad may be triggered in the proxy state of an entity.
Next I'd look into a custom query walker (basically a modified approach of the Translatable extension), but I'd hope there's a less complex solution out there.
Cheers
Matthias
There are quite a few solutions here and I guess I haven't looked at half of them.
Anyhow, the least complex and at least a little bit clean solution I came up with so far is using a static class for translating. This could look something like this:
class Translation
{
/**
* #var string
*/
private static $language;
/**
* tries to get a translated property by prepending the
* configured language to the given entities getter
*
* #param object $entity
* #param string $getter
* #return mixed
*/
public static function getTranslated($entity, $getter) {
$language = self::getLanguage();
$translatedGetter = $getter.$language;
if(method_exists($entity, $translatedGetter)) {
return $entity->$translatedGetter();
} else {
return $entity->$getter;
}
}
/**
* #return string
*/
public static function getLanguage()
{
return self::$language;
}
/**
* #param string $language
*/
public static function setLanguage($language)
{
self::$language = ucfirst(strtolower($language));
}
}
You then set the language whenever your application starts and implement the translations either in your entities:
/**
* #return string
*/
public function getName()
{
return Translation::getTranslated($this, __FUNCTION__);
}
or call it from outside:
Translation::getTranslated($animal, "getName");
So with the first method this code:
Translation::setLanguage("DE");
// far more code
/** #var Animal[] $animals */
$animals = $entityManager->getRepository(Animal::class)->findAll();
foreach ($animals as $animal) {
echo $animal->getName()."<br >";
}
would put out:
Katze
Hund
This is just one way to do it with a static class, of course.
I have recently startet with Zend Framework 2 and came now across Doctrine 2, which I would now like to integrate in my first project.
I have now got the following situation and even after days, I can not find a solution.
I have 3 Tables:
Advert
advert_id
advert_title
etc
Category
category_id
name
label
etc
advert2category
advert2category_category_id
advert2category_advert_id
An Advert can be in different Categories and different Categories have different Adverts, therefore the table Advert2Category (ManytoMany).
After reading through the www, I have decided that it should be a ManytoMany Bidirectional, with the "owning side" at the Advert Entity.
Don't ask me why I decided that, I still don't understand Doctrine fully. Anyway, I created 3 Entities, but guess I only need Advert and Category Entity.
I now want the following to happen.
I click on a Category and want to see a list of Articles within this category., that means I have to read out the Table advert2category. I have created the Entities, here my Advert Entity:
So here is first my Advert Entity:
namespace Advert\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Advert
*
* #ORM\Table(name="advert")
* #ORM\Entity
*/
class Advert
{
/**
* #var integer
*
* #ORM\Column(name="advert_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $advertId;
/**
* #var string
*
* #ORM\Column(name="advert_title", type="string", length=255, nullable=true)
*/
private $advertTitle;
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="advertCategory", cascade={"persist"})
* #ORM\JoinTable(name="advert2category",
* joinColumns={#ORM\JoinColumn(name="advert2category_category_id", referencedColumnName="category_id")},
* inverseJoinColumns={#ORM\JoinColumn(name="advert2category_advert_id", referencedColumnName="advert_id")}
* )
*/
protected $category;
public function __construct()
{
$this->category = new ArrayCollection();
}
/**
* Get advertId
*
* #return integer
*/
public function getAdvertId()
{
return $this->advertId;
}
/**
* Set advertTitle
*
* #param string $advertTitle
* #return Advert
*/
public function setAdvertTitle($advertTitle)
{
$this->advertTitle = $advertTitle;
return $this;
}
/**
* Get advertTitle
*
* #return string
*/
public function getAdvertTitle()
{
return $this->advertTitle;
}
/**
* Set category
*
* #param \Advert\Entity\User $category
* #return Advert
*/
public function setCategory(\Advert\Entity\Category $category = null)
{
$this->category = $category;
return $this;
}
/**
* Get category
*
* #return \Advert\Entity\Category
*/
public function getCategory()
{
return $this->category;
}
}
And my Category Entity:
namespace Advert\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Category
*
* #ORM\Table(name="category")
* #ORM\Entity
*/
class Category
{
/**
* #var integer
*
* #ORM\Column(name="category_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $categoryId;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Advert", mappedBy="category")
**/
private $advertCategory;
public function __construct()
{
$this->advertCategory = new ArrayCollection();
}
/**
* Get categoryId
*
* #return integer
*/
public function getCategoryId()
{
return $this->categoryId;
}
/**
* Set name
*
* #param string $name
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
}
Just as a first test, I have now tried the following in my Controller:
//Below Controller now works to echo the categories ArrayCollection
$data = $this->getEntityManager()->getRepository('Advert\Entity\Advert')->findAll();
foreach($data as $key=>$row)
{
echo $row->getAdvertTitle();
echo $row->getUser()->getUsername();
$categories = $row->getCategory();
foreach($categories as $row2) {
echo $row2->getName();
}
What am I doing wrong here? Can anyone give me an advice? Thank you very much in advance !
Honestly, and it's a very honest and fine thing, that this is way overcomplicating what you want to do, but only in specific areas.
If you used Composer to include Doctrine (the recommended way), also include symfony/console and you will get a whole mess of awesome tools to help you on your quest. There is a very specific command that will kick you in your seat for how awesome it is: $ doctrine orm:schema-tool:update --force --dump-sql. This will get Doctrine to run through your Entities (you only need the two) and will generate your tables and even setup the *To* associations for you. Int he case of ManyToOne's it will generate the appropriate Foreign Key schema. In the case of ManyToMany's it will automatically create, AND manage it's own association table, you just need only worry about giving the table a name in the Entity.
I'm not kidding you, Do this. It will make your life worth living.
As for your entity setup, this is all you need:
<?php
namespace Advert\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Advert
*
* #ORM\Table(name="advert")
* #ORM\Entity
*/
class Advert
{
/**
* #var integer
*
* #ORM\Column(name="advert_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $advertId;
/**
* #var string
*
* #ORM\Column(name="advert_title", type="string", length=255, nullable=true)
*/
private $advertTitle;
/**
* #ORM\ManyToMany(targetEntity="Category", cascade={"persist"})
* #JoinTable(name="advert_categories")
*/
protected $category;
public function __construct()
{
$this->category = new ArrayCollection();
}
/**
* Get advertId
*
* #return integer
*/
public function getAdvertId()
{
return $this->advertId;
}
/**
* Set advertTitle
*
* #param string $advertTitle
* #return Advert
*/
public function setAdvertTitle($advertTitle)
{
$this->advertTitle = $advertTitle;
return $this;
}
/**
* Get advertTitle
*
* #return string
*/
public function getAdvertTitle()
{
return $this->advertTitle;
}
/**
* Set category
*
* #param ArrayCollection $category
* #return Advert
*/
public function setCategory(ArrayCollection $category)
{
$this->category = $category;
return $this;
}
/**
* Get category
*
* #return ArrayCollection
*/
public function getCategory()
{
return $this->category;
}
}
Notice that the getters and setters are Documented to Set and Return ArrayCollection, this is important for IDE's and tools that read PHPDoc and Annotations to understand how in-depth PHP class mapping works.
In addition, notice how much simpler the ManyToMany declaration is? The #JoinTable annotation is there to give a name to the table that doctrine will generate and manage. That's all you need!
But now, you probably should remove the $advertCategory property out of the Category Entity. Doctrine is going to auto-hydrate embedded Entities in properties with the Entity Association Mappings.
This is also potentially dangerous as it can result in infinite recursion. Basically, if all you requested was an Advert with ID of 1, it would go in and find ALL of the Category Entities associated to Advert 1, but inside of those Categories it's re-referencing Advert 1, which Doctrine will sub-query for and inject, which will contain a Category association, which will then Grab those categories, and so on and so fourth until PHP kills itself from lack of memory.
Once everything is good to go, and you got some Categories associated with your Advert, using the Getter for your category in the Advert entity will return an array of Category Entities. Simply iterate through them:
foreach($category as $advert->getCategories()) {
echo $category->getName();
}
or
echo current($advert->getCategories())->getName();
After I successfuly created TaskBundle with One-to-Many relation between category and tasks, now I'm trying to create a new TaskBundle with Many-to-Many relation. I get also problem with checking checkbox in this relation, but now it is not a primary problem (maybe after solving this). I deleted all tables, which is TaskBundle using and trying to create a new, but here is problem (description at the bottom).
My Task object:
<?php
namespace Acme\TaskBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="tasks")
*/
class Task
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=200)
* #Assert\NotBlank(
* message = "Task is empty"
* )
* #Assert\Length(
* min = "3",
* minMessage = "Task is too short"
* )
*/
protected $task;
/**
* #ORM\Column(type="datetime")
* #Assert\NotBlank()
* #Assert\Type("\DateTime")
*/
protected $dueDate;
/**
* #Assert\True(message = "You have to agree.")
*/
protected $accepted;
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="tasks")
* #ORM\JoinTable(name="categories")
*/
protected $category;
/**
* Constructor
*/
public function __construct()
{
$this->category = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set task
*
* #param string $task
* #return Task
*/
public function setTask($task)
{
$this->task = $task;
return $this;
}
/**
* Get task
*
* #return string
*/
public function getTask()
{
return $this->task;
}
/**
* Set dueDate
*
* #param \DateTime $dueDate
* #return Task
*/
public function setDueDate($dueDate)
{
$this->dueDate = $dueDate;
return $this;
}
/**
* Get dueDate
*
* #return \DateTime
*/
public function getDueDate()
{
return $this->dueDate;
}
/**
* Add category
*
* #param \Acme\TaskBundle\Entity\Category $category
* #return Task
*/
public function addCategory(\Acme\TaskBundle\Entity\Category $category)
{
$this->category[] = $category;
return $this;
}
/**
* Remove category
*
* #param \Acme\TaskBundle\Entity\Category $category
*/
public function removeCategory(\Acme\TaskBundle\Entity\Category $category)
{
$this->category->removeElement($category);
}
/**
* Get category
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCategory()
{
return $this->category;
}
}
and Category object
<?php
namespace Acme\TaskBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="categories")
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=200, unique=true)
* #Assert\NotNull(message="Categories cannot be empty", groups = {"adding"})
*/
protected $name;
/**
* #ORM\ManyToMany(targetEntity="Task", mappedBy="category")
*/
private $tasks;
public function __toString()
{
return strval($this->name);
}
/**
* Constructor
*/
public function __construct()
{
$this->tasks = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add tasks
*
* #param \Acme\TaskBundle\Entity\Task $tasks
* #return Category
*/
public function addTask(\Acme\TaskBundle\Entity\Task $tasks)
{
$this->tasks[] = $tasks;
return $this;
}
/**
* Remove tasks
*
* #param \Acme\TaskBundle\Entity\Task $tasks
*/
public function removeTask(\Acme\TaskBundle\Entity\Task $tasks)
{
$this->tasks->removeElement($tasks);
}
/**
* Get tasks
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTasks()
{
return $this->tasks;
}
}
So, after i put doctrine:schema:update --force i'll get error: Table 'symfony.categories' already exists. I've tried to delete all caches, but same problem. Any idea?
There's only problem, if it is as m2m relation.
PS: I was looking for this problem at the Google, but no one answers at this problem. There were only questions, but not correct answers, where the problem is and how to solve it.
Looks like you already have table named "categories" in that database. Remove this line #ORM\JoinTable(name="categories") and try without it.
P.S. "Categories" is really a strange name for join table. You should probably follow some conventions and let doctrine name it. Common names for join tables are category_task or category2task as they are more self-explanatory. Nothing that important, just trying to suggest what I consider good practice.
The thing is that doctrine doesn't understand how your existing table should be used. But you can give him some help.
You have two options :
You don't care about the existing table : simple, you can remove the #ORM\JoinTable(name="categories") annotation, and doctrine will create an other table etc.
You want to keep your existing table, which sounds pretty logical : you have to be more explicit in your annotation by adding #ORM\JoinColumn annotation.
Here is an example:
class
<?php
...
/**
* #ORM\Entity
* #ORM\Table(name="tasks")
*/
class Task
{
...
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="tasks")
* #ORM\JoinTable(name="categories",
* joinColumns={#ORM\JoinColumn(name="category_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="task_id", referencedColumnName="id")})
*/
protected $category;
...
}
and Category object
<?php
...
/**
* #ORM\Entity
* #ORM\Table(name="categories")
*/
class Category
{
...
/**
* #ORM\ManyToMany(targetEntity="Task", mappedBy="category")
* #ORM\JoinTable(name="categories",
* joinColumns={#ORM\JoinColumn(name="task_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="category_id", referencedColumnName="id")})
*/
private $tasks;
...
Doing so, you will be able to keep your table without any doctrine error.
My fix for this, as far as I can tell, was a case-sensitivity issue with table names. Doctrine let me create a Users and a users table but afterwards would die on migrations:diff or migrations:migrate .
I used the -vvv option to get more detail on this error message; it seems that the error happens when Doctrine is loading up its own internal representation of the current database's schema. So if your current database has table names that Doctrine doesn't understand (like two tables that are identical, case-insensitive) then it will blow up in this fashion.
Seems like most of the answers above assume that the error is in your code, but in my case it was in the database.
I got this error with 2 ManyToMany targeting the same entity (User in the exemple below).
To create the table name doctrine use the entity and target entity name.
So in my case it was trying to create two time the table thread_user
To debug this it's easy. Just use the '#ORM\JoinTable' annotation and specify the table name.
Here is a working exemple.
/**
* #ORM\ManyToMany(targetEntity="App\Entity\User")
* #ORM\JoinTable(name="thread_participant")
*/
private $participants;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\User")
* #ORM\JoinTable(name="thread_recipient")
*/
private $recipients;
in Symfony4.1 you can force the migration using the migration version
doctrine:migrations:execute <migration version>
ex
for migration version123456.php use
doctrine:migrations:execute 123456
there is another using the table name ,you can search it in your project . Maby be demo,I think it...
sorry for my chinese english !
Try to drop everything inside of your proxy directory.
I fix same issue after check other entities on each bundles, be aware of this.
So, I have been playing round with using doctrine for a while now and have it in some basic projects, but i decided to go back and have an in depth look into what it can do.
Ive now decided to switch to symfony 2 as my framework of choice and am looking into what doctrine 2 can do in more depth.
One thing i have been trying to get my head around is the many to many relationship within doctrine. I am starting to build a recipe system and am working on the relation between recipe and ingredients which gave me 3 entities, recipe, recipeIngredient and ingredient. The reason i cannot use a direct many to many relation is because i want to store two additional columns in the join table ( unit and quantity ) for each ingredient.
The problem i am having at the moment is that the entities persist ok, but the recipe_id in the join table is not inserted. I have tried everything i can think off and been through every thread and website looking for an answer . I am sure it is something completely obvious that i am missing. Please help, below is the code i have so far:
<?php
namespace Recipe\RecipeBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="recipe")
* #ORM\HasLifecycleCallbacks()
*/
class Recipe{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="RecipeIngredient", mappedBy="recipe", cascade= {"persist"})
*/
protected $ingredients;
/**
* #ORM\Column(type="string")
* #var string $title
*
*/
protected $title;
/**
* Constructor
*/
public function __construct()
{
$this->ingredients = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Add ingredients
*
* #param \Recipe\RecipeBundle\Entity\RecipeIngredient $ingredients
* #return Recipe
*/
public function addIngredient(\Recipe\RecipeBundle\Entity\RecipeIngredient $ingredients)
{
$ingredients->setRecipe($this);
$this->ingredients[] = $ingredients;
return $this;
}
/**
* Remove ingredients
*
* #param \Recipe\RecipeBundle\Entity\RecipeIngredient $ingredients
*/
public function removeIngredient(\Recipe\RecipeBundle\Entity\RecipeIngredient $ingredients)
{
$this->ingredients->removeElement($ingredients);
}
/**
* Get ingredients
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getIngredients()
{
return $this->ingredients;
}
/**
* Set title
*
* #param string $title
* #return Recipe
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
}
and recipeIngredient
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Recipe", inversedBy="ingredients")
* */
protected $recipe;
/**
* #ORM\ManyToOne(targetEntity="Ingredient", inversedBy="ingredients" , cascade={"persist"})
* */
protected $ingredient;
/**
* #ORM\Column(type="string")
* #var string $quantity
*
*/
protected $quantity;
/**
* #ORM\Column(type="string")
* #var string $unit
*
*/
protected $unit;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set quantity
*
* #param string $quantity
* #return RecipeIngredient
*/
public function setQuantity($quantity)
{
$this->quantity = $quantity;
return $this;
}
/**
* Get quantity
*
* #return string
*/
public function getQuantity()
{
return $this->quantity;
}
/**
* Set unit
*
* #param string $unit
* #return RecipeIngredient
*/
public function setUnit($unit)
{
$this->unit = $unit;
return $this;
}
/**
* Get unit
*
* #return string
*/
public function getUnit()
{
return $this->unit;
}
/**
* Set recipe
*
* #param \Recipe\RecipeBundle\Entity\Recipe $recipe
* #return RecipeIngredient
*/
public function setRecipe(\Recipe\RecipeBundle\Entity\Recipe $recipe = null)
{
$this->recipe = $recipe;
return $this;
}
/**
* Get recipe
*
* #return \Recipe\RecipeBundle\Entity\Recipe
*/
public function getRecipe()
{
return $this->recipe;
}
/**
* Set ingredient
*
* #param \Recipe\RecipeBundle\Entity\Ingredient $ingredient
* #return RecipeIngredient
*/
public function setIngredient(\Recipe\RecipeBundle\Entity\Ingredient $ingredient = null)
{
$this->ingredient = $ingredient;
return $this;
}
/**
* Get ingredient
*
* #return \Recipe\RecipeBundle\Entity\Ingredient
*/
public function getIngredient()
{
return $this->ingredient;
}
}
Your basic idea is the correct one. If you want to have a ManyToMany relation, but you need to add extra fields in the join table, the way to go is exactly as you have described: using a new entity having 2 ManyToOne relations and some additional fields.
Unfortunately you have not provided your controller code, because most likely your problem is there.
Basically if you do something like:
$ri = new RecipeIngredient;
$ri->setIngredient($i);
$ri->setRecipe($r);
$ri->setQuantity(1);
$em->persist($ri);
$em->flush();
You should always get a correct record in your database table having both recipe_id and ingredient_id filled out correctly.
Checking out your code the following should also work, although I personally think this is more sensitive to mistakes:
$ri = new RecipeIngredient;
$ri->setIngredient($i);
$ri->setQuantity(1);
// here we assume that Recipe->addIngredient also does the setRecipe() for us and
// that the cascade field is set correctly to cascade the persist on $ri
$r->addIngredient($ri);
$em->flush();
For further reading I would suggest the other topics on this subject, such as: Doctrine2: Best way to handle many-to-many with extra columns in reference table
If I understand this model correctly the construction of a recipe and its associated recipeIngredients are concurrent. You might not have an id until you persist and without an id if receipeIngredient->setRecipe() is called the default null will be place in the recipeIngredient->recipe field. This is often handled with cascade: "persist" (not present for the recipe field in your example, but you can handle it explicitly in the controller:
/**
* Creates a new Recipe entity.
*
*/
public function createAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(new RecipeType());
$form->bind($request);
if ($form->isValid()){
$data = $form->getData();
$recipeId = $data->getId();
$recipeIngredients=$data->getIngredients();
$recipe=$em->getRepository('reciperecipeBundle:Recipe')
->findOneById($RecipeId);
if (null === $Recipe)
{$Recipe=new Recipe();}
foreach ($recipeIngredients->toArray() as $k => $i){
$recipeIngredient=$em->getRepository('reciperecipeBundle:recipeIngredient')
->findOneById($i->getId());
if (null === $recipeIngredient)
{$recipeIngrediente=new RecipeIngredient();}
$recipe->addIngredient($i);
// Next line *might* be handled by cascade: "persist"
$em->persist($recipeIngredient);
}
$em->persist($Recipe);
$em->flush();
return $this->redirect($this->generateUrl('Recipe', array()));
}
return $this->render('reciperecipeBundle:Recipe:new.html.twig'
,array('form' => $form->createView()));
}
Im not really sure if this would be a solution, but its easy yo try it, and probably it will help.
When I create a relationshiop of this kind, I use to write another anotation, the #ORM\JoinColumn, like in this example:
We have an entity A, an entity B, and an class AB wich represents the relationships, and adds some other fields, like in you case.
My relationship would be as follows:
use Doctrine\ORM\Mapping as ORM;
/**
*
*
* #ORM\Table(name="a_rel_b")
* #ORM\Entity
*/
class AB
{
/**
* #var integer
* #ORM\Id
* #ORM\ManyToOne(targetEntity="A", inversedBy="b")
* #ORM\JoinColumn(name="a_id", referencedColumnName="id")
**/
private $a;
/**
* #var integer
* #ORM\Id
* #ORM\ManyToOne(targetEntity="B", inversedBy="a")
* #ORM\JoinColumn(name="b_id", referencedColumnName="id")
**/
private $b;
// ...
name means the name of the field in the relationship table, while referencedColumnName is the name of the id field in the referenced entity table (i.e b_id is a column in a_rel_b that references the column id in the table B )
You can't, because it wouldn't be a relationship anymore [which is, by def, a subset of the cartesian product of the sets of the two original entities].
You need an intermediate entity, with references to both Recipe and Ingredient - call it RecipeElement, RecipeEntry or so, and add the fields you want.
Either, you can add a map to your Recipe, in which you save the attributes for each Ingredient you save, easy to maintain if there are no duplicates.
For further reading, have a look at this popular question.