Doctrine - Prevent double flush on using identity through foreign entity - doctrine-orm

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.

Related

Persisting Doctrine Entity with Collection results in exception

Am working on a simple setup with two tables with a one to many (and inversed) relationship. The two corresponding entities are:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ArticlesRepository::class)
* #ORM\Table(name="articles")
*/
class Article
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(name="unique_id", type="integer")
*/
protected int $idx;
/**
* The inverse side
* #ORM\OneToMany(targetEntity="Comment", mappedBy="article")
* #ORM\Column(name="uid", type="integer", nullable=true)
*/
protected $id;
public function __construct()
{
$this->id=new ArrayCollection();
}
}
and
use Doctrine\ORM\Mapping as ORM;
/**
* ORM\Entity(repositoryClass="CommentRepository::class")
* ORM\Table(name="comments")
*/
class Comment
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(name="uid", type="integer")
*/
protected $idx;
/**
* The owning side
* #ORM\ManyToOne(targetEntity="Article", inversedBy="uid")
* #ORM\JoinColumn(name="article", referencedColumnName="uid")
*/
protected $article;
protected $content;
}
Attempting to persist a new blank Article
$article=new Article();
$em->persist($article);
$em->flush();
results in the following exception
An exception occurred while executing 'INSERT INTO articles (uid) VALUES (?)' with params [{}]: PHP Warning: Object of class Doctrine\Common\Collections\ArrayCollection could not be converted to float in ...\vendor\doctrine\dbal\lib\Doctrine\DBAL\Driver\Mysqli\MysqliStatement.php line 164
I went searching for answers and came across suggestions close to but don't touch on this subject. One had to do with removing the type indication of the column. So I did and it gets stuck with the following exception
An exception occurred while executing 'INSERT INTO articles (uid) VALUES (?)' with params [{}]: Cannot add or update a child row: a foreign key constraint fails...
When type is removed, the field defaults to type string which makes no difference, since Doctrine still attempts to store an empty array in an integer field (see the values in the exception above).
Question: How can I get the setup to work properly and persist all objects correctly?
It turns out that the #ORM\OneToMany annotation in the Article entity should be used on a non-existent database field. In other words, property $id should not be an actual field in the database table of the entity.

doctrine - single table inheritance and native query fails

In a symfony 2.8 app I have following class hierarchy configured as single table inheritance:
parent: App\Entity\AbstractUser
child1: App\Entity\UserClient
child2: App\Entity\UserGuest
Further each class from above has a oneToOne relation to a class App\Entity\Profile.
Based on the above I have a native query on the profiles table joining the users table.
The goal is to get the profile data together with the related user data in one query.
It did work for a while but stopped working at some point. Probably after doctrine bundle update.
Now my code below throws the following exception: The provided class "App\Entity\AbstractUser" is abstract, and can not be instantiated.
What can I do to make it work again?
Here is my code:
$rsm = new ResultSetMapping;
$rsm->addEntityResult('App\Entity\AbstractUser', 'br');
$rsm->addFieldResult('br', 'user_id', 'id');
$sql = "
SELECT
rsa.id AS profile_id,
br.id AS user_id
FROM profiles rsa
LEFT OUTER JOIN users br ON rsa.id = br.profile_id
";
try {
$query = $this->_em->createNativeQuery($sql, $rsm);
$results = $query->getResult();
} catch (\Exception $e) {
throw $e;
}
Here are the doctrine definitions:
// Resources/config/doctrine/AbstractUser.orm.yml
App\Entity\AbstractUser:
type: entity
table: users
inheritanceType: SINGLE_TABLE
discriminatorColumn:
name: type
type: string
length: 30
nullable: false
discriminatorMap:
client: App\Entity\UserClient
guest: App\Entity\UserGuest
fields:
id:
id: true
type: integer
generator:
strategy: AUTO
# oneToOne association
oneToOne:
profile:
targetEntity: App\Entity\Profile
cascade: ['persist','remove']
joinColumn:
name: profile_id
referencedColumnName: id
nullable: false
unique: false
// Resources/config/doctrine/UserClient.orm.yml
App\Entity\UserClient:
type: entity
// Resources/config/doctrine/UserGuest.orm.yml
App\Entity\UserGuest:
type: entity
// Resources/config/doctrine/Profile.orm.yml
App\Entity\Profile:
type: entity
table: profiles
fields:
id:
id: true
type: integer
generator:
strategy: AUTO
You should be using either of:
DiscriminatorMap (Single or Class Table Inheritance)
MappedSuperclass
Info on both here
MappedSuperclass
This is similar to PHP's abstract class. You can not instantiate this class. It is therefore a class which must be extended into something proper. As such you could have:
AbstractUser
Guest extends AbstractUser
Admin extends AbstractUser
This would give you a few things:
Some properties and functions are available for both (saves duplicate code - DRY)
Gives the same "parent" class as an instance
Setup & use case
/**
* #ORM\MappedSuperclass
*/
abstract class AbstractEntity
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer", options={"unsigned":true})
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
// getter / setter
}
This use case shows the usage of a common identifier for all entities in my own projects. It also gives an easy entry-point to validate whether or not a specific Entity is in my own projects or supplied by a vendor, doing:
if ($entity instanceof AbstractEntity) { ... }
Single / Class Table Inheritance
Now, let's use the MappedSuperclass in Table Inheritance. We're going with the JOINED strategy in this example, but feel free to use whichever suits your needs best.
You have 3 classes, 2 of which are instantiable:
UserClient
UserGuest
Both stem from: AbstractUser
Now, let's say that "Guest" is the most limited user, we'll create that one first:
/**
* #ORM\Entity
* #ORM\Table(name="user_guests")
*/
class Guest extends AbstractEntity
{
/**
* #var string
* #ORM\Column(name="display_name", type="string", length=255, nullable=false)
*/
protected $displayName;
/**
* #var string
* #ORM\Column(name="email", type="string", length=255, nullable=false, unique=true)
*/
protected $email;
/**
* #var string
* #ORM\Column(name="password", type="text", nullable=false)
*/
protected $password;
// getters / setters
}
So far so good.
Now, we have the need to create "Client". A client should have:
displayName
email
password
clientId
The first 3 are the same as for Guest. So, it might be wise to extend Guest. Will other things (e.g. functionality) be the same for either user type? Such as:
way of logging in/out
being an email recipient/sender
etc.
If a lot of these use cases are the same, then we have a solid case for using Table Inheritance. So, update the Annotation for Guest, like so:
/**
* #ORM\Entity
* #ORM\Table(name="user_guests")
*
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="discr", type="string")
*/
class Guest extends AbstractEntity { ... }
NOTE: We do not declare a DiscriminatorMap. Doctrine will handle this for us. Per the docs:
If no discriminator map is provided, then the map is generated automatically. The automatically generated discriminator map contains the lowercase short name of each class as key.
Next, create the Client:
/**
* #ORM\Entity
* #ORM\Table(name="user_clients")
*/
class Client extends Guest
{
/**
* #var string
* #ORM\Column(name="client_id", type="string", length=50, nullable=false)
*/
protected $clientId;
// getters / setters
}
The SQL generated by Doctrine for these 2, as an example, is this:
CREATE TABLE user_guests
(
id INT UNSIGNED AUTO_INCREMENT NOT NULL,
display_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
password LONGTEXT NOT NULL,
discr VARCHAR(255) NOT NULL,
UNIQUE INDEX UNIQ_2843A78FE7927C74 (email),
PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8
COLLATE utf8_unicode_ci
ENGINE = InnoDB;
CREATE TABLE user_clients
(
id INT UNSIGNED NOT NULL,
client_id VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8
COLLATE utf8_unicode_ci
ENGINE = InnoDB;
ALTER TABLE user_clients
ADD CONSTRAINT FK_58C5307EBF396750 FOREIGN KEY (id) REFERENCES user_guests (id) ON DELETE CASCADE;
Shown visually, that's:
Now you can also add in your Profile Entity very easily. You simply add it to your Guest user, with a OneToOne bi-directional relation.
So, add to your Guest:
// other properties
/**
* #var Profile
* #ORM\OneToOne(targetEntity="Profile", cascade={"persist", "remove"}, fetch="EAGER", inversedBy="Guest")
* #ORM\JoinColumn(name="profile_id", referencedColumnName="id")
*/
protected $profile;
// getter / setter
At this point though, you might wish to consider renaming "Guest" to plain "User", as using that for a name could be less confusing in the future. "User" is usually already a pretty limited member, similar to a Guest (the difference usually being that a "Guest" is not a registered User and a User is).
Now you can simply do:
$profile->getGuest() // or ->getUser() if you renamed
// Returns instance of Guest (might be child instance: Client)
$guest->getProfile() // returns Profile
$client->getProfile() // returns Profile
$user = new Guest();
$user instanceof Guest ? true : false; // true
$user instanceof Client ? true : false; // false
$user = new Client();
$user instanceof Guest ? true : false; // true
$user instanceof Client ? true : false; // true

Symfony2, Doctrine data persistance

I have 5 tabels that have relationships.
Table User and Articles have relation:
In User:
/**
* #ORM\OneToMany(targetEntity="Article", mappedBy="author")
*
* #var ArrayCollection $articles
*/
protected $articles;
In Articles:
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="articles")
*
*/
protected $author;
I create new Article:
$article = new Article();
$article->setTitle("title");
$article->setText("Text test");
$article->setType("image");
$em->persist($article);
$em->flush();
And I add Article to User:
$user->addarticle($article);
$em->persist($user);
$em->flush();
Article is saved in DB, but it don't have any data on field: author_id.
How to make relationship when I add an article to user to automaticaly in save in table Article to be author_id.
Try this schema.
In User:
/**
* #ORM\OneToMany(targetEntity="Article", mappedBy="author")
*/
protected $articles;
In Article:
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="articles")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
protected $author;
Genereate entities, and update schema.
$article = new Article();
$article->setTitle("title");
$article->setText("Text test");
$article->setType("image");
$article->setAuthor($user);
$em->persist($article);
$em->flush();

Doctrine Cascade Options for OneToMany

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.

Adding a timestamp property to a relation in Symfony2

I am trying to model a simple comment system with 2 entities - User and Post. The User class has the regular properties, and the Post class looks like this:
class Post
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* User who posted activity
*
* #var User
* #ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\User")
*/
protected $user;
/**
* #var string $text
*
* #ORM\Column(name="text", type="string", length=255)
*/
private $text;
/**
* #var datetime $timestamp
*
* #ORM\Column(name="timestamp", type="datetime")
*/
private $timestamp;
}
I need to make a simple association - Users can favourite posts. The logical thing to do would be to add ManyToMany association in User class to Post like so
#ORM\ManyToMany(targetEntity="Post")
Which would then create a mapping table with user_id and post_id as primary keys. Now my problem is that I need to add a timestamp property to track when the user liked a post. One possibility is to create a new Entity "FavouritePost", map it to both User and Post class using ManyToOne association, and add a timestamp property to it.
1) Is this the right/only way to do it in Symfony2?
2) If I want to select the most recent 15 posts, and check if the currently logged in user has liked the posts, how can I write a query for that?
Any help appreciated, as I am drawing a blank on how to do it via Symfony
1) Yes you may create a jointure table, and so its FavouritePost entity.
2) You should use a Repository to create all special queries :
#ORM\Entity(repositoryClass="Acme\AppBundle\Repository\FavouritePostRepository")
Then it's easy Doctrine queries, like you will make much more.