Doctrine Cascade Options for OneToMany - doctrine-orm

I'm having a hard time making sense of the Doctrine manual's explanation of cascade operations and need someone to help me understand the options in terms of a simple ManyToOne relationship.
In my application, I have a table/entity named Article that has a foreign key field referencing the 'id' field in a table/entity named Topic.
When I create a new Article, I select the Topic from a dropdown menu. This inserts an integer into the 'topic_id' foreign key field in the Article table.
I have the $topic association set up in the Article entity like this:
/**
* #ManyToOne(targetEntity="Topic")
* #JoinColumn(name="topic_id", referencedColumnName="id", nullable=false)
*/
private $topic;
The Topic entity doesn't have any reciprocating annotation regarding the Article entity. Topics don't care what Articles reference them and nothing needs to happen to a Topic when an Article that references the Topic is deleted.
Because I'm not specifying the cascade operation in the Article entity, Doctrine throws an error when I try to create a new Article: "A new entity was found through a relationship that was not configured to cascade persist operations. Explicitly persist the new entity or configure cascading persist operations on the relationship."
So I know I need to choose a cascade operation to include in the Article entity, but how do I know which operation to choose in this situation?
From reading the Doctrine manual, "detach" sounds like the right option. But researching others' similar questions here and here makes me think I want to use "persist" instead.
Can anyone help me understand what "persist," "remove," "merge," and "detach" mean in terms of a simple ManyToOne relationship like the one I've described?

In the Doctrine2 documentation "9.6. Transitive persistence / Cascade Operations" there are few examples of how you should configure your entities so that when you persist $article, the $topic would be also persisted. In your case I'd suggest this annotation for Topic entity:
/**
* #OneToMany(targetEntity="Article", mappedBy="topic", cascade={"persist", "remove"})
*/
private $articles;
The drawback of this solution is that you have to include $articles collection to Topic entity, but you can leave it private without getter/setter.
And as #kurt-krueckeberg mentioned, you must pass the real Topic entity when creating new Article, i.e.:
$topic = $em->getRepository('Entity\Topic')->find($id);
$article = new Article($topic);
$em->persist($article);
$em->flush();
// perhaps, in this case you don't even need to configure cascade operations
Good luck!

If you have a #OneToMany unidirectional association, like that described in section 6.10 of the Doctrine Reference, then most likely you forgot to persist the Topic before calling flush. Don't set the topic_id primary key in Article. Instead set the Topic instance.
For example, given Article and Topic entities like these:
<?php
namespace Entities;
/**
#Entity
#Table(name="articles")
*/
class Article {
/**
* #Id
* #Column(type="integer", name="article_id")
* #GeneratedValue
*/
protected $id;
/**
* #Column(type="text")
*/
protected $text;
/**
* #ManyToOne(targetEntity="Topic", inversedBy="articles")
* #JoinColumn(name="topic_id", referencedColumnName="topic_id")
*/
protected $topic;
public function __construct($text=null)
{
if (!is_null($text)) {
$this->text = $text;
}
}
public function setArticle($text)
{
$this->text = $text;
}
public function setTopic(Topic $t)
{
$this->topic = $t;
}
}
<?php
namespace Entities;
/**
#Entity
#Table(name="topics")
*/
class Topic {
/**
* #Id
* #Column(type="integer", name="topic_id")
* #GeneratedValue
*/
protected $id;
public function __construct() {}
public function getId() {return $this->id;}
}
After you generate the schema:
# doctrine orm:schema-tool:create
your code to persist these entities would look like something this
//configuration omitted..
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);
$topic = new Entities\Topic();
$article1 = new Entities\Article("article 1");
$article2 = new Entities\Article("article 2");
$article1->setTopic($topic);
$article2->setTopic($topic);
$em->persist($article1);
$em->persist($article2);
$em->persist($topic);
try {
$em->flush();
} catch(Exception $e) {
$msg= $e->getMessage();
echo $msg . "<br />\n";
}
return;
I hope this helps.

Related

Symfony 4: Updating an existing entity through a deserialized entity ends up with a new related item or with an ORMInvalidArgumentException

That's my first question here on Stackoverflow.com and before I'll write to much. First the controller function:
/**
* #Rest\Patch("/identifiers/v2/{id}")
*
* #ParamConverter("identifier")
* #ParamConverter("identifierPatch", converter="fos_rest.request_body")
*/
public function patchAction(Identifier $identifier, Identifier $identifierPatch)
{
$identifier->setLandingPage($identifierPatch->getLandingPage());
$identifier->setIdentifier($identifierPatch->getIdentifier());
$identifier->setIsUsed($identifierPatch->getIsUsed());
$this->entityManager->flush();
/**
* Just for debugging...
*/
$view = $this->view([
'identifier' => $identifier,
'identifierPatch' => $identifierPatch
]);
return $this->handleView($view);
}
When i try to UPDATE an existing entity this way I get an ORMInvalidArgumentException with a message "A new entity was found through the relationship (...)"
When I set cascade={"persist"} on the related entity:
/**
* #ORM\ManyToOne(targetEntity="App\Entity\LandingPage", inversedBy="identifiers")
* #ORM\JoinColumn(nullable=false)
* #Assert\NotNull()
* #Serializer\Type("App\Entity\LandingPage")
*/
private $landing_page;
... the related entity will be inserted as new entity and that's not what I am looking for.
I could use $this->entityManager->merge($identifier) but that's not what I am looking for aswell, because I'll need to do some manual validations in future and I would like to return the entity as response (the related entity will be null when not updated) and $this->entityManager->merge() will be deprecated in Doctrine 3.
Question: Is there any way to update the given entity with the deserialized entity?
Greetings,
Danny Endert
EDIT (1):
Well, I guess i found a solution regarding this "issue".
services.yaml
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: false
Now I'm not getting any exception and the related entity will not be inserted as new entity.

Must I sanitize my inputs when using setter methods to populate a Doctrine Entity?

I am under impression that ORM uses some kind of sanitation technique, but I am not sure. I looked at http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/security.html and was not clear on the issue.
Question
Will it be safe to use
$product = new Product();
$product->setModel($_POST['model']);
where POST is NOT sanitized previously, or must I always sanitize/validate my values first before sending them to Doctrine?
For reference
/**
* #Entity
*/
class Product
{
/**
* #var integer #Column(name="id", type="integer", nullable=false)
* #Id #GeneratedValue
*/
private $id;
/**
* #var string #Column(type="string")
*/
private $model;
}
You should always validate/sanitize user input. Even though Doctrine is using a prepared queries (which prevents SQL injections) you are not safe against other attacks.
Check this page, to see how to deal with inputs in Doctrine:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/security.html#user-input-and-doctrine-orm
Your are right, Doctrine ORM is doing all the sanitization automatically. Therefore, as long as you are using ORM, you are perfectly safe.
So in your example no additional sanitization is required.
I would only say that instead of using raw $_POST array you are supposed to use the Request object that is automatically injected in your controller:
$product = new Product();
$product->setModel($request->get('model'));

Doctrine - Prevent double flush on using identity through foreign entity

I've got 2 entities - TestUser and TestAddress. Address has OneToOne relation with User and it's primary key is also foreign key to User.
/**
* #ORM\Entity
*/
class TestUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
}
/**
* #ORM\Entity
*/
class TestAddress
{
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="TestUser")
*/
private $user;
/**
* #param $user
*/
public function setUser($user)
{
$this->user = $user;
}
}
When I try to save both entities through entityManager I get an Exception.
$em = $this->getEntityManager();
$user = new TestUser();
$address = new TestAddress();
$address->setUser($user);
$em->persist($user);
$em->persist($address);
$em->flush();
Exception:
Doctrine\ORM\ORMInvalidArgumentException: The given entity of type 'Entity\TestAddress' (Entity\TestAddress#000000004fcda1bf000000002f07b49b) has no identity/no id values set. It cannot be added to the identity map.
Only way to save both entities is "flush" with User and then persist and flush with address.
$em->persist($user);
$em->flush();
$em->persist($address);
$em->flush();
Question: Is there way to use autoincrement on TestUser id and save TestUser and TestAddress entities with one flush?
class TestAddress
{
/**
* #ORM\OneToOne(targetEntity="TestUser", cascade={"persist"})
*/
private $user;
I know it is not even an answer to your question, but IMO you should rethink the purpose of this schema choice when the ID of the Address entity must correspond to the ID of the User entity. I do not think this is necessary and you would be better off by going with separate foreign key and primary key fields on Address entity.
However one other thing you could try is making the relationship bi-directional, that is defining the testAddress variable on the User object and then adding the corresponding mapping information to it:
/**
* #OneToOne(targetEntity="TestAddress", mappedBy="user", cascade={"persist"})
*/
private $testAddress;
then you will probably also need to create the setTestAddress() method on the user entity, and set the user object for TestAddress explicitly in there:
public function setTestAddress($address)
{
$address->setUser($this);
$this->testAddress = $address;
}
then you could call $user->setTestAddress($address) and try flushing only the user entity:
$user = new TestUser();
$address = new TestAddress();
$user->setTestAddress($address);
$em->persist($user);
$em->flush();
I am not quite familiar with the order in which doctrine persists entities, but by having the entity be persisted by the TestUser entity and not the other way around(as was suggested) User should be persisted first and than its ID added to the Address before it is even persisted.
I didn't test the code so there might be some errors in it.

Doctrine2 - Get entity ID before flush

Is there any way to get an entity ID before the persist/flush?
I mean:
$entity = new PointData();
$form = $this->createForm(new PointDataType(), $entity);
If I try $entity->getId() at this point, it returns nothing.
I can get it working by:
$em->persist($entity);
$em->flush();
(supposing $em = $this->getDoctrine()->getEntityManager();)
How can I achieve this?
If you want to know the ID of an entity before it's been persisted to the database, then you obviously can't use generated identifiers. You'll need to find some way to generate unique identifiers yourself (perhaps some kind of hash function can produce unique-enough values).
This is rarely a good idea, though, so you should be careful.
I would think very carefully about why I need to know the identifier before flush. Doctrine is quite good at letting you build up a big object graph, and persist/flush it all at once. It seems likely that you've got something ugly in your architecture that you're trying to work around. It might be a good idea to review that before going down the application-generated-id route.
You can use the #PostPersist annotation. A method annotated with that will be executed just before the flush terminate and the entity Id is available already.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/events.html
postPersist - The postPersist event occurs for an entity after the entity has been made persistent. It will be invoked after the database insert operations. Generated primary key values are available in the postPersist event.
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class PointData
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
...
/**
* #ORM\PostPersist
*/
public function onPostPersist()
{
// Put some simple logic here that required the auto-generated Id.
$this->doSomething($this->id);
}
...
}
you can use an auto generate ID to get a key like universally unique identifiers (UUID) or you can take the events of symfony:
postFlush - The postFlush event occurs at the end of a flush operation.
Doctrine best practices says,
You should avoid auto-generated identifiers. because:
Your DB operations will block each other
You are denying bulk inserts
You cannot make multi-request transactions
Your object is invalid until saved
Your object does not work without the DB
So you can use UUIDS instead
public function __construct() {
$this->id = Uuid::uuid4();
}
Also, Doctrine supports the UUID generation strategy since version 2.3.
Not sure why you need the ID before flushing, but, if you really need to persist the entity without saving to the database you can try using Transactions.
Try something like this:
$em->beginTransaction();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
//do some stuff and save when ready
$em->commit();
$em = $this->getDoctrine()->getManager();
$entity = new PointData();
$em->persist($entity);
$entity->getId() <-- return <int>
$em->flush();
after persist you can get id

How to get a Respository from an Entity?

I have an Entity called Game with a related Repository called GameRepository:
/**
* #ORM\Entity(repositoryClass="...\GameRepository")
* #ORM\HasLifecycleCallbacks()
*/
class Game {
/**
* #ORM\prePersist
*/
public function setSlugValue() {
$this->slug = $repo->createUniqueSlugForGame();
}
}
In the prePersist method, I need to ensure that the Game's slug field is unique, which requires a database query. To do the query, I need access to the EntityManager. I can get the EntityManager from inside GameRepository. So: how do I get the GameRespository from a Game?
You actually can get the repository in your entity and only during a lifecycle callback. You are very close to it, all you have to do is to receive the LifecycleEventArgs parameter.
Also see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* #ORM\Entity(repositoryClass="...\GameRepository")
* #ORM\HasLifecycleCallbacks()
*/
class Game {
/**
* #ORM\prePersist
*/
public function setSlugValue( LifecycleEventArgs $event ) {
$entityManager = $event->getEntityManager();
$repository = $entityManager->getRepository( get_class($this) );
$this->slug = $repository->createUniqueSlugForGame();
}
}
PS. I know this is an old question, but I answered it to help any future googlers.
You don't. Entities in Doctrine 2 are supposed to not know of the entity manager or the repository.
A typical solution to the case you present would be to add a method to the repository (or a service class) which is used to create (or called to store) new instances, and also produces a unique slug value.
you can inject the doctrine entity manager in your entity
(using JMSDiExtraBundle)
and have the repository like this:
/**
* #InjectParams({
* "em" = #Inject("doctrine.orm.entity_manager")
* })
*/
public function setInitialStatus(\Doctrine\ORM\EntityManager $em) {
$obj = $em->getRepository('AcmeSampleBundle:User')->functionInRepository();
//...
}
see this : http://jmsyst.com/bundles/JMSDiExtraBundle/1.1/annotations
In order to keep the logic encapsulated without having to change the way you save the entity, instead of the simple prePersist lifecycle event you will need to look at using the more powerful Doctrine events which can get access to more than just the entity itself.
You should probably look at the DoctrineSluggableBundle or StofDoctrineExtensionsBundle bundles which might do just what you need.