doctrine - single table inheritance and native query fails - doctrine-orm

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

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.

How to tell Doctrine to save ID

I have some code that has Doctrine annotations for how to save it into the DB.
I have an ID string that has already been generated by the system. I want to tell Doctrine to use the id as the primary key, and also save it into the database when persisting the object.
Currently it seems that Doctrine is just not writing that field.
Doctrine\DBAL\Exception\NotNullConstraintViolationException: An
exception occurred while executing 'INSERT INTO voting_motion (type,
name, start_datetime, close_datetime, created_at, updated_at) VALUES
(?, ?, ?, ?, ?, ?)' with params ["personal_opinion", "Question about
food", "2020-07-02 12:00:00", "2020-07-07 13:00:00", "2020-05-27
11:16:49", "2020-05-27 11:16:49"]:
i.e. the insert into statement is just not including the ID field.
I thought the GeneratedValue annotation would be the appropriate thing to do here. But it seems not.
How do I tell Doctrine that "this is the primary id, and I do want you to write it when saving the entity"?
This is what my code for the entity with the relevant annotations looks like.
/**
* #Entity
* #Table(name="voting_motion")
* #HasLifecycleCallbacks
*/
class VotingMotion
{
/**
* #Id()
* #Column(name="id", type="string", unique=true)
* #GeneratedValue(strategy="NONE")
*/
private string $id;
/** #Column(type="string") **/
private string $type;
/** #Column(type="string") **/
private string $name;
// ...
// ...
}
These annotations work fine for me, you don't need to use #GeneratedValue(strategy="NONE") annotation. Also, run migrations:diff after changes in the GeneratedValue annotation.
/**
* #ORM\Id()
* #ORM\Column(type="guid", unique=true)
*/
private $id;
I use strategy="UUID" and works fine
/**
* #ORM\Id
* #ORM\Column(type="guid")
* #ORM\GeneratedValue(strategy="UUID")
*/
protected $id;

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.

Doctrine 2.0 vs 2.1 cascade remove OneToMany

Hello I have problem when trying to cascade remove entities in OneToMany relations.
After a few hours of debugging I tried to downgrade the doctrine from the latest 2.1.2 to 2.0.2 and It suddenly starts working.
Imagin two entities Company and Address in relation 1:N.
/**
* #Entity
*/
class Company extends Entity
{
/**
* #var integer
* #id #Column(type="integer")
* #generatedValue
*/
private $id;
/**
* #var Collection
* #OneToMany(targetEntity="Address",mappedBy="company", cascade={"persist","remove"})
*/
private $addresses;
}
/**
* #Entity
*/
class Address extends Entity
{
/**
* #var integer
* #id #Column(type="integer")
* #generatedValue
*/
private $id;
/**
* #var Company
* #ManyToOne(targetEntity="Company", inversedBy="addresses")
* #JoinColumn(name="company_id", referencedColumnName="id",nullable=false)
*/
private $company;
}
when I try to remove the entity Company, I would like the assigned addresses will be removed as well.
$em->remove($company);
$em->flush();
In doctrine 2.1.2 the deletion of addresses is not performed so the integrity constraint fails. In version 2.0.2 there it works perfectly. Wierd thing on it is, if I use EntityAudit extension https://github.com/simplethings/EntityAudit the LogRevisionListener is corretly versioning the addresses entities (set them revtype = DEL) in doctrine 2.1.2 (of course in 2.0.2 as well) but the UnitOfWork is not removing it.
Is there any difference how to handle cascade removing in 2.0.2 and in 2.1.2?
Thank you very much
Try using this on the addresses attribute of your Company Class
#OneToMany(targetEntity="Address",mappedBy="company",
cascade={"persist"}, orphanRemoval=true)
I had the same problem... Relations were added or updated, but not deleted, even if I had cascade: [persist, remove].
I found out that I didn't need the "remove" attribute in "cascade", but I had to add the orphanRemoval: true.
I was going crazy, you made my day!
I have met the same problem and i have solved him with that code :
$em->remove($object);
$em->flush();
$em->remove($user);
$em->flush();
Maybe you can use a findAll on your company for the addresses and remove this with a foreach like that :
// Return all the addresses of the company
$addresses = $em->getRepository(...)->findAllAddressesByCompany($company);
$em->remove($company);
foreach ($address in $addresses)
{
$em->remove($address);
}
That's not a very good method but for now, that's all I've found.

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.