How to handle SQL triggers in Symfony2? - symfony

I have a User entity in my Symfony2/Doctrine2 webapp. This user has an attribute last_updated to identify the latest time, anything has changed. I set this attribute to NOT NULL in my database. So far, so good.
I would consider it to be good practice to create a SQL trigger in the database, that sets this last_updated to NOW() on every INSERT or UPDATE. So you don't have to care about this in your application. So that's what I did, I implemented this trigger in my database.
But if I now create a user in my app
$user = new User();
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
I get an error message by Symfony:
An exception occurred while executing 'INSERT INTO User (username, ..., last_updated) VALUES (?, ..., ?)'
with params ["johndoe", ..., null]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'last_updated' cannot be null
The problem is clear: Symfony is trying to fire an INSERT-statement to the database with the parameter null for last_updated, which is not allowed - as this attribute may not be null.
I could quickly think of two workarounds:
One workaround would be to take the last_updated field out of the entity description. Then Symfony would not try to pass anything to the database for this column, and the trigger would set the appropriate value. But I don't think this is a good way, because as soon as I would try to update the db schema (doctrine:schema:update --force) I would loose my last_updated-column.
Another workaround: Simply do $user->setLastUpdated(new \DateTime()) before I persist() and flush(). But this would minimize the advantage of using a trigger on my database to avoid having to care about it in my application.
Is there any way to let Symfony/Doctrine know that there is a trigger running on my database? If not, (how) can I hook into Symfony/Doctrine to implement a proper workaround?

To quote a response to this question from a google group:
Database side code (such as Triggers and Functions) tend to break the benefits of developing software using an ORM like Propel or Doctrine as one of the biggest advantages of using ORM's is to be database agnostic. By having database side Triggers and Functions you are tying yourself to the database and therefore gain little to no benefit using an ORM. -GarethMc
https://groups.google.com/forum/#!topic/symfony-users/MH_ML9Dy0Rw
For this it is best to use the Life Cycle Callbacks as Faery suggests. One simple function will handle updating that field so that you dont have to worry about it if you decide to change databases in the future.
//In Your Entity File EX: SomeClass.php
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class SomeClass
{
....
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function prePersistPreUpdate()
{
$this->last_modified = new \DateTime();
}
}
See also references for lifecycle callbacks
Symfony reference
Doctrine reference
In your case you would add the lifecycle call back function and annotation to your User entity class. SomeClass is simply an example class showing that lifecycle callbacks are good for more than just your User entity.

Another (easier and more generalized) option would be to use the Timestampable Doctrine extension by Gedmo. In this way, you could simply annotate your entity fields to be timestamped on create or on update.
Example:
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
class MyEntity
{
...
/**
* #var \DateTime $lastUpdated
*
* #Gedmo\Timestampable(on="update")
* #ORM\Column(name="last_updated", type="datetime")
*/
private $lastUpdated;
...
}
https://packagist.org/packages/gedmo/doctrine-extensions

Related

doctrine nested entities cascade persist : how to reuse existing entities

If entity A contains multiple entity B and has cascade:persist, how to reuse existing entities B when persisting ?
B entity has one primary key, an integer, and the id of the A parent. The only data it contains is the primary key.
Example:
A has 2 B entities, identified by their id, 14 and 23.
A.Bs = [{id=14, AId=A.id}, {id=23, AId=A.Id}]
Now if I modify this managed entity, to add a B entity to A, with id = 56.
A.Bs = [{id=14, AId=A.id}, {id=23, AId=A.Id}, {id=56}]
Relationships
Entity A
/**
* #var B[]|ArrayCollection
*
* #ORM\OneToMany(targetEntity="B", mappedBy="A", cascade={"persist", "remove"}, orphanRemoval=true)
* #Assert\Valid
*/
private $Bs;
Entity B
/**
* #var A
*
* #ORM\ManyToOne(targetEntity="A", inversedBy="Bs")
* #ORM\JoinColumn(name="A_id", referencedColumnName="A_id")
* #Assert\NotNull()
*/
private $A;
If I try to persist I get Integrity constraint violation, because Doctrine tries to persist the existing entities, that have id 14 and 23.
I understand this is expected behaviour, but how can I make it persist new entities, and reuse existing ones ?
More details:
If I get an existing entity A with $em->find($id) and directly use persist and flush, I will get UniqueConstraintException because it tries to persist the already persisted B entities.
Example code:
/** #var A $existingEntityA */
$existingEntityA = $this->getEntity($id);
$this->serializerFactory->getComplexEntityDeserializer()->deserialize(json_encode($editedEntityADataJson), A::class, 'json', ['object_to_populate' => $existingEntityA]);
$this->entityValidator->validateEntity($existingEntityA);
$this->_em->flush();
Example error : Integrity constraint violation: 1062 Duplicate entry '777111' for key 'PRIMARY'
If I understand your example properly - you're doing something like this:
$b = new B();
$b->setId(56);
$a->getB()->add($b);
and you having a row with primary key 56 into database table that is represented by B?
If my assumption is correct - it is wrong way to go. Reason is that Doctrine internally stores so called "identity map" that keeps track of all entities that either being fetched from database or persisted by calling EntityManager::persist(). Every entity that is scheduled for commit but not available into identity map is considered as "new" and scheduled for insertion. If row with same primary key is already available in database - you're receiving UniqueConstraintException.
Doctrine doesn't handle a case "let me look if there is an entity with such primary key in database" by itself because it will hurt performance significantly and is not needed in most cases. Each such test will result into database query, imagine if you will have thousands of such entities. Since Doctrine doesn't know business logic of your application - it will spend even more resources with attempts to guess optimal strategy so this is intentionally left out of scope.
Correct way for you would be to get your entity by itself before adding to collection:
$newB = $em->find(B::class, 56);
if ($newB) {
$a->getB()->add($newB);
}
In this case new entity will internally have "managed" status and will be correctly handled by Doctrine at a time of commit.

Symfony - Get Entity from within another entity

I would like to retrieve a record from another entity (or record from the DB) within a entity.
They there are no relationship between the two entities.
I am using #ORM\HasLifecycleCallbacks() and #ORM\PrePersist so when the main entity is created it will also create another entity (save a record to another table)
The above is working fine, there are no issues with this.
What I am having an issue with is I would like to link that entity with another table but I need to retrieve the object based on the value of the first entity.
Usually I would write a function in the entity repository but I am not calling the entity manager within the entity.
An Entity in Doctrine is an object representation of a concept, with attributes and methods. It is meant to be lightweight, a POPO (plain old php object). It must not know anything about its persistence. Therefore if you see reference to the EntityManager in a model, it probably stinks.
Solutions? You could use an entity listener called on entity creation and then use a service dedicated only to properly compose your object(s), maybe something like a Factory. In this way, your entity stays lightweight, the lifecycle management is satisfied and the entity composing is responsibility only of your service.
Entity manager is accessible in an entity repository. You can legally use it to fetch data from other entities and to compose your business logic. This is what entity repositories are made for: Doctrine Custom Repositories, Symfony Custom Repository Classes.
/**
* #ORM\Entity
*/
class Beta {}
/**
* #ORM\Entity
*/
class Alpha {}
class AlphaRepository extends EntityRepository
{
public function getDataFromAnotherEntity($something)
{
$query = 'select * from MyBundle\Entity\Alpha alpha where alpha.id = :something';
return $this->getEntityManager()
->createQuery($query)
->setParameter('something', $something)
->getResult();
}
}
In Symfony 3.1 you can use the entityManager to set a reference. This is still lightweight as it does not instance a complete Doctrine Record.
Example: I have an entity Status which has some states, and it's referenced in another entity. On create i use this method inside EventSubscriber:
public function preAction(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$entityManager = $args->getObjectManager();
if (method_exists($entity, 'setStatus')) {
if ($entity->getStatus() === null) {
$entity->setStatus($entityManager->getReference('AppBundle\Entity\Status', Status::STATUS_REGULAR));
}
}
}

Doctrine "A new entity was found through the relationship" error

First off I want to say I've read through all the docs and googled this plenty before posting this question. I know what that error means (un-persisted entity in a relationship)
I'm getting this error where I think I shouldn't be getting it.
I have a OneToMany Bi-Directional relationship as follow:
Class Channel
{
/**
* #ORM\OneToMany(targetEntity="Step", mappedBy="channel", cascade={"all"}, orphanRemoval=true)
* #ORM\OrderBy({"sequence" = "ASC"})
*/
protected $steps;
}
Class Step
{
/**
* #ORM\ManyToOne(targetEntity="Channel", inversedBy="steps")
*/
protected $channel;
}
One Channel can have many Steps and the owning side is Channel. After I upgraded from Doctrine 2.4 to 2.5 I'm getting this error:
Doctrine\ORM\ORMInvalidArgumentException: A new entity was found
through the relationship 'Company\MyBundle\Entity\Step#channel' that
was not configured to cascade persist operations for entity
why is it even finding new relationships from the inverse side? Here's my code:
$channel = new Channel();
$step = new Step();
$channel->addStep($step);
$em->persist($channel);
$em->flush();
Thanks!
You're right: Doctrine looks only for changes into owning side but you're wrong: owning side of your relationship is Step, not Channel.
Why is step the owning side? Because is the entity that has foreign key. Even Doctrine documentation says to you
The owning side has to use the inversedBy attribute of the OneToOne,
ManyToOne, or ManyToMany mapping declaration. The inversedBy attribute
contains the name of the association-field on the inverse-side.
Possible solutions:
Try to invert cascade operations by putting cascade={"all"} into Step entity (are you sure that all is the correct choice?)
Persist explicitly both entities:
$channel = new Channel();
$step = new Step();
$channel->addStep($step);
$em->persist($channel);
$em->persist($step);
$em->flush();
here you can read why second way provided here is fine too
You try to persist $channel, but it has Step entity inside. So in Doctrine now you have 2 entities that are queued for inserting. Then Doctrine order entities in the order where first is Step because it has channel_id foreign key (that is empty now). Doctrine try persist this entity and when it understands that channel_id is empty it sees for cascade rules for persisting. It doesn't see any cascade rules and throw you this exception.

Doctrine one-to-many situation: how to easily fetch related entities

To simplify, two entities are defined: User and Comment. User can post many comments and every comment has only one user assigned, thus Comment entity has:
/**
* #var \Frontuser
*
* #ORM\ManyToOne(targetEntity="Frontuser")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="ownerUserID", referencedColumnName="id")
* })
*/
private $owneruserid;
However, when in action:
$orm = $this->getDoctrine()->getManager();
$repo = $orm->getRepository('CompDBBundle:Comment');
$repo->findBy(array('owneruserid' => $uid);
Error occured, that there's no such field like owneruserid.
How can I fetch all the user's comments then? The same happens to similar relations in my DB - looks likes you cannot run find() with foreign keys as parameters. I believe a function $user->getComments() should be automatically generated/recognised by Doctrine to allow efficient, quick access to related entities.
The example's simple but, what if there are more entities related to my User in the same way? Do I have to declare repositories for each and try to fetch them by it's owneruserid foreign keys?
Using doctrine, when you define a related entity it's type is the entity class (in this case FrontUser). Therefore firstly your related entity variable name is misleading. It should be e.g.
private $ownerUser;
Then, in order to do a findBy on a related entity field you must supply an entity instance e.g.
$orm = $this->getDoctrine()->getManager();
$userRepo = $orm->getRepository('CompDBBundle:FrontUser');
$user = $userRepo->findById($uid);
$commentRepo = $orm->getRepository('CompDBBundle:Comment');
$userComments = $commentRepo->findByOwnerUser($user);
If you don't have or want to retrieve the user entity you could use a DQL query with the 'uid' as a parameter instead.

Extending entities in Symfony2 with Doctrine2

I'm having trouble finding a way to appropriately extend an Entity across bundles in Symfony2 using Doctrine2 as the ORM.
Currently there are three methods that I've found to extending entities in Symfony2 using Doctrine2 as the ORM. Mapped Superclass, Single Table Inheritance and Class Table Inheritance. None of these work for what I'm looking to do.
I have two Bundles: UserBundle and BlogBundle. I want to be able to use the UserBundle in projects that do not have the BlogBundle, but the BlogBundle will always be used in projects that have the User Bundle. It's ok if the BlogBundle has dependencies on the UserBundle, but not the other way around.
I have two entities:
BlogBundle\Entity\Post and
UserBundle\Entity\User
Relationship:
There needs to be a One to Many relationship between Users and Blog Posts. This is achieved through a Author_ID property (column) on the BlogBundle\Entity\Post object (table) which is mapped to UserBundle\Entity\User.id
The Problem:
I can call the UserBundle\Entity\User entity directly from within the BlogBundle and achieve what I'm looking for using a Uni-Directional mapping. This does not allow me to access all posts by a user from within a User object. I can access the data via custom queries but this is not as clean as accessing posts by a user through the user object.
What I'd like to do is extend the UserBundle\Entity\User object from within the BlogBundle, and add the methods and properties to this object that establish the One to Many mapping used within the BlogBundle. None of this is persisted, it simply defines the relationship and allows me to logically access all posts created by a user in an application that implements both the BlogBundle and UserBundle by adding needed functionality to the User object within the blog bundle (thus avoiding a dependency from the UserBundle to the BlogBundle).
When I create a BlogBundle\Entity\User object and extend UserBundle\Entity\User I must declare #ORM\Table(name="usertablename"). If I don't, any attempt to access the BlogBundle\Entity\User object will fail to access the database. Since none of the additions in the extended object persist, this works fine across bundles. The issue with this is when I call "php app/console doctrine:schema:update --force", there is a conflict since two entities try to map to & create the same table. I have tried using the ResolveTargetEntityListener feature that was recently implemented but this, along with Mapped Superclas, STI and CTI all force a dependency on the BlogBundle from the UserBundle.
Below are my objects to help illustrate my my setup. They have been abbreviated for clarity. I realize some of the semantics aren't correct but it's intended to communicate the ideas & configuration.
UserBundle\Entity\User
#ORM\Table(name="app_user")
#ORM\Entity
class User implements UserInterface
{
...
}
BlogBundle\Entity\Post
#ORM\Table(name="app_post")
#ORM\Entity
class Post
{
...
#ORM\Column(name="author_id", type="integer")
protected $author_id;
#ORM\ManyToOne(targetEntity="\App\BlogBundle\Entity\User", inversedBy="posts")
#ORM\JoinColumn(name="author_id", referencedColumnName="id")
protected $author;
}
BlogBundle\Entity\User
use App\UserBundle\Entity\User as BaseUser
#ORM\Entity
#ORM\table(name="app_user")
class User extends BaseUser
{
....
#ORM\OneToMany(targetEntity="App\BlogBundle\Entity\Post", mappedBy="author")
protected $posts;
public function __construct()
{
parent::_construct();
$this->posts = new \Doctrine\Common\Collections\ArrayCollection();
}
....
/* Getters & Setters, nothing that defines #ORM\Column, nothing persisted */
}
This works but the problem is that I'm mapping two entities in the project to the same table. The extended object doesn't grab the #ORM\Table(name="app_user") from it's parent so it must be defined in BlogBundle\Entity\User. If not any reference to this object from a controller will not access the database. Since nothing is persisted from the extended object nothing is broken except for when I try to update the database schema from the console.
I can use a unidirectional relationship, but this limits how I can access the data from within a controller.
You can see in this link to know about inheritance: http://docs.doctrine-project.org/en/latest/reference/inheritance-mapping.html#single-table-inheritance
You must declare in UserBundle\Entity\User:
/**
* #Entity
* #InheritanceType("SINGLE_TABLE")
* #DiscriminatorColumn(name="discr", type="string")
* #DiscriminatorMap({"baseuser" = "UserBundle\Entity\User", "blogUser" = "BlogBundle\Entity\User"})
*/
class User implements UserInterface
{
...
}
And BlogBundle\Entity\User
use App\UserBundle\Entity\User as BaseUser;
/**
* #ORM\Entity
*/
class User extends BaseUser
{
....
}
Goodluck!
I think you could find this Bundle interesting:
https://github.com/mmoreram/SimpleDoctrineMapping
It allows you to define by parameters what files are mapping your entities, allowing to override every entity of your generic bundles.
For example:
parameters:
#
# Mapping information
#
test_bundle.entity.user.class: "TestBundle\Entity\User"
test_bundle.entity.user.mapping_file_path: "#TestBundle/Mapping/Class.orm.yml"
test_bundle.entity.user.entity_manager: default
test_bundle.entity.user.enable: true
The only contra I see is that you have to define all the next entities the same way cause you disabled auto_mapping...

Resources