Update "updated" property every time a property has been changed - symfony

My doctrine entity has a datetime property called updated. This should get the current time everytime (changed) values of the object are written to the DB. I know how to do this in MySql, but I'm looking for a doctrine/symfony solution.
Is there a way to
hook into something before an INSERT/UPDATE of an instance is sent to the DB.
update the updated property to the current time and make sure it's written to the DB without triggering a second UPDATE statement.

See the Timestampable behavior of DoctrineExtensions. There is StofDoctrineExtensionsBundle to intergrate it into Symfony.

You don't need any extensions, just use Lifecycle Callbacks.
Basically, mark your entity that it has event callbacks configured with the HasLifecycleCallbacks annotation:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* ...
*/
class MyEntityClass {
...
And then mark instance methods to be run on specific events occuring, e.g. to set updated:
...
class MyEntityClass {
...
/**
* #ORM\PreUpdate
*/
public function onPreUpdate() {
$this->updated = new \DateTime();
}
...

Related

Inconsistent behavior of Doctrine's entity listener

I am creating a small app using Symfony 4 & Doctrine. There are users (User entities) and they are owning some kind of content called radio tables (RadioTable entity). Radio tables are containing radio stations (RadioStation entity). RadioStation.radioTableId is related to RadioTable (many to one) and RadioTable.ownerId is related to User (many to one).
Maybe I should notice that this is my first project with SF.
Entities are configured using annotations, this way:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable, EncoderAwareInterface
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\RadioTable", mappedBy="owner", orphanRemoval=true)
*/
private $radioTables;
/**
* #ORM\Column(type="date")
*/
private $lastActivityDate;
}
// -----------------
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\RadioTableRepository")
* #ORM\EntityListeners({"App\EventListener\RadioTableListener"})
*/
class RadioTable
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="radioTables")
* #ORM\JoinColumn(nullable=false, onDelete="cascade")
*/
private $owner;
/**
* #ORM\Column(type="datetime")
*/
private $lastUpdateTime;
}
// -----------------
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\RadioStationRepository")
* #ORM\EntityListeners({"App\EventListener\RadioStationListener"})
*/
class RadioStation
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\RadioTable")
* #ORM\JoinColumn(nullable=false, onDelete="cascade")
*/
private $radioTable;
}
I need to update $lastUpdateTime in a proper RadioTable entity when radio stations are added, removed or modified. Also, I need to update $lastActivityDate of the radio table owner (User class), when radio table is created, removed or updated. I am trying to achieve this by using entity listeners:
<?php
namespace App\EventListener;
class RadioStationListener
{
/**
* #PreFlush
* #PreRemove
*/
public function refreshLastUpdateTimeOfRadioTable(RadioStation $radioStation)
{
$radioStation->getRadioTable()->refreshLastUpdateTime();
}
}
// -----------------------------
namespace App\EventListener;
class RadioTableListener
{
/**
* #PreFlush
* #PreRemove
*/
public function refreshLastActivityDateOfUser(RadioTable $radioTable, PreFlushEventArgs $args)
{
$radioTable->getOwner()->refreshLastActivityDate();
/* hack */
$args->getEntityManager()->flush($radioTable->getOwner());
/* hack */
}
}
(In refresh*() methods I am just creating a new instance of \DateTime for proper entity field.)
I encountered the problem. When I tried to update/remove/create radio stations, RadioStation listener worked properly and related RadioTable class was successfully updated. But when I tried to update radio table, User class was updated but was not persisted to the database by Doctrine.
I was confused because the structure of the code in these entity listeners is very similar.
Partially I found the cause of the problem. It's obvious that only owner can modify its own radio tables and the user has to be logged in to modify them. I am using Security component from Symfony to support login-in mechanism.
When I temporarily hacked controller code to disable Security and tried to update the radio table as anonymous, RadioTable entity listener worked properly and User entity was successfully modified and persisted to database.
To fix the problem I need to manually talk with Doctrine's entity manager and call flush() with User entity as an argument (without argument I am doing endless loop). This line is marked by /* hack */ comment.
After this looong story, I want to ask the question: WHY I have to do it? WHY I have to manually call flush() for User object but only if Security component is used and the user is logged in?
I solved the problem.
Doctrine processes entities in a specified order. First, newly created entities (scheduled for INSERT) have precedence. Next, persisted entities (scheduled for UPDATE) are processed in the same order as they were fetched from the database. From inside entity listener, I am not able to predict or enforce the preferred order.
When I'm trying to update User's last activity date inside RadioTable's entity listener, changes made in User entity are not persisted. It's because in very early stage Security component loads my User object from DB and then Symfony prepares RadioTable object for the controller (by param converter for example).
To fix the issue I need to tell Doctrine to recalculate User entity changeset. Here is what I did.
I created small trait for my entity listeners:
<?php
namespace App\EventListener\EntityListener;
use Doctrine\Common\EventArgs;
trait EntityListenerTrait
{
// There is need to manually enforce update of associated entities,
// for example when User entity is modified inside RadioTable entity event.
// It's because associations are not tracked consistently inside Doctrine's events.
private function forceEntityUpdate(object $entity, EventArgs $args): void
{
$entityManager = $args->getEntityManager();
$entityManager->getUnitOfWork()->recomputeSingleEntityChangeSet(
$entityManager->getClassMetadata(get_class($entity)),
$entity
);
}
}
Inside entity listeners I am doing this:
<?php
namespace App\EventListener\EntityListener;
use App\Entity\RadioTable;
use Doctrine\Common\EventArgs;
use Doctrine\ORM\Mapping\PreFlush;
use Doctrine\ORM\Mapping\PreRemove;
class RadioTableListener
{
use EntityListenerTrait;
/**
* #PreFlush
* #PreRemove
*/
public function refreshLastActivityDateOfUser(RadioTable $radioTable, EventArgs $args): void
{
$user = $radioTable->getOwner();
$user->refreshLastActivityDate();
$this->forceEntityUpdate($user, $args);
}
}
There is another solution. It's possible to call $entityManager->flush($user) but it works properly only for UPDATEs, generates endless loop for INSERTs. To avoid endless loop it's possible to check $unitOfWork->isScheduledForInsert($radioTable).
This solution is worse because it generates additional transaction and SQL queries.

Deserialize and persist relationships with JMS Serializer

I'm trying to get the following working:
I've got an entity like:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* Contact
*
* #ORM\Table()
* #ORM\Entity()
*/
class Contact
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ServiceClient", inversedBy="contacts")
* #ORM\JoinColumn(name="service_client", referencedColumnName="service_client")
*
* #JMS\Type("AppBundle\Entity\ServiceClient")
* #JMS\SerializedName("serviceClient")
*/
private $serviceClient;
}
I'm sending the following JSON over an HTTP request (Post, it's a new Contact, no ID):
{
"name": "Lorem Ipsum",
"serviceClient": {"service_client": "ipsum"}
}
What I expect is for the JMS Serializer to parse that relationship, and leting me persist the Contact object like this:
<?php
$contact = $this->get('serializer')->deserialize(
$request->getContent(),
Contact::class, 'json'
);
$this->em->persist($contact);
$this->em->flush();
In fact I got that working (I swear it was working) but now it's giving me the follwing error:
A new entity was found through the relationship
'AppBundle\Entity\Contact#serviceClient' that was not configured to
cascade persist operations for entity:
AppBundle\Entity\ServiceClient#000000006fafb93e00007f122bd10320. To
solve this issue: Either explicitly call EntityManager#persist() on
this unknown entity or configure cascade persist this association in
the mapping for example #ManyToOne(..,cascade={\"persist\"}). If you
cannot find out which entity causes the problem implement
'AppBundle\Entity\ServiceClient#__toString()' to get a clue."
So it's tryign to persist the entity... a thing I do not want since the entity already exists. I just want Doctrine to put the reference, the foreign key.
Edit: It seems it's the constructor, if I set it to the doctrine_object_constructor it works like magic, the thing I do not understand is why it stop working in the first place.
Can anyone share any ideas or a cleaner way to do what I did?
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: false
This problem happens when Doctrine cannot map your relationship to an existing record in the database, so it will try to create a new one with the data from the JSON object.
In your case, the JSON object: {"service_client": "ipsum"} cannot be mapped to an existing ServiceClient instance.
It's because the default JMS object constructor call the unserialize function (will be the one from your Entity if you defined this method) to construct the object, which mean this object will always be treated by Doctrine as new (has never been persisted).
By using doctrine_object_constructor, JMS will get the object from Doctrine. The object came from Doctrine not only have the attributes and methods you define in your entity, but also meta-data about whether it's an existing one, it's corresponding row from the database ( so Doctrine can detect update made on the record later and handle it), therefore Doctrine are able to avoid incorrect persisting.
Doctrine will try to persist the Contact with a reference of a ServiceClient entity given in the deserialization. In the entity definition at the level of the manyToOne definition you need to add :
#ORM\ManyToOne(targetEntity="AppBundle\Entity\ServiceClient", inversedBy="contacts", cascade={"persist"})

How to handle SQL triggers in Symfony2?

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

association mapping when one entity isn't managed by Doctrine

I have 2 entities in a one-to-one association. The first, Person, is stored in a MySQL database and handled by Doctrine. The second, AdUserRecord, describes an ActiveDirectory user record. It is read-only. It does not need to know about Person. Also, AdUserRecord properties should never be stored in the MySQL db for privacy reasons.
An AdUserRecord is retrieved using a service, AdSearcher, which can search by samaccountname or objectGUID. Whenever a search is successful, the service checks to see if there is a corresponding Person record and creates one if there is not. That works fine.
My problem occurs when I start with a Person object. Mostly, I don't need to access a Person's AdUserRecord so I'd prefer not to query Active Directory unless it's required. That means, I think, that Person::getAdrecord() needs to have access to the AdSearcher service. Something like this:
public function getAdrecord(){
if($this->adrecord) return $this->adrecord;
$searcher = ???; //get AdSearcher service somehow
$record = $search->getRecordByUserGuid($this->ad_guid);
if(!$record) throw new \Exception('this person no longer exists');
$this->adrecord = $record;
return $this->adrecord;
}
I've been reading the Symfony docs pretty assiduously, but I'm still stumped.
Questions
how do I get a service into an entity? Should it be injected via the constructor, or just where it's needed, in the getter? If it only occurs in the getter, do I have to inject it or is there a way to import it?
is adding a service to an entity the canonical way of handling these types of situations? Would it be preferable to build an entity manager for AdUserRecords?
what interfaces do I need to implement if I have to build an entity manager?
Person class
namespace ACRD\DefaultBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use ACRD\DefaultBundle\Entity\AdUserRecord;
/**
* #ORM\Entity
* #Orm\Table(name="person")
*
*/
class Person {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="AD_guid", type="string", length=36, unique=true)
*/
protected $ad_guid;
/**
* #var AdUserRecord
*/
protected $adrecord;
//usual getters and setters
}
It looks like Doctrine's postLoad event is the best solution.
// src/Acme/DemoBundle/EventListener/ActiveDirectorySubscriber.php
namespace Acme\DemoBundle\EventListener;
use Acme\DemoBundle\Model\AdAwareInterface;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
// for doctrine 2.4: Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\DependencyInjection\ContainerAware
class ActiveDirectorySubscriber extends ContainerAware implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
'postLoad',
);
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!($entity instanceof AdAwareInterface)) {
return:
}
$adSearcher = $this->getContainer()->get('acme_demo.ad_searcher');
if ($adPerson = $adSearcher->find($entity->getAdGuid())) {
$entity->setAdPerson($adPerson);
}
}
}
You also mentioned that most of the time you don't need to use the active directory stuff. Before optimizing I highly suggest you actually measure how much of a performance impact there is. If, however, you do notice a performance problem, consider using a proxy object to mitigate the AdPerson searching right to the point where you actually need something from it.
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!($entity instanceof AdAwareInterface)) {
return:
}
$adSearcher = $this->getContainer()->get('acme_demo.ad_searcher');
$entity->setAdPerson(new AdPersonProxy($adSearcher));
}
The AdPersonProxy would basically extend from your AdPerson class, wrap each and every public method with a call to load the actual AdPerson object and then act as a facade between the two. Consider the following implications before you start coding though:
it adds complexity to your codebase (the more code, the more there is to maintain);
it will be a pain to debug - for example you might get an exception inside your
template that will leave you scratching your head for a long time (been there,
done that);
The bottom line is that in theory services should (mostly) not be injected inside entities.
Regarding your third question:
EntityManagers implement Doctrine/Common/Persistence/ObjectManager - have a look at the interface on github.
Further:
a somewhat clean implementation would be similar to the Document<->Entity mapping (called references) provided by gedmo/doctrine-extensions.
Take a glimpse at the documentation to see how it works here and here.
If that's what you want start diving into the code of the ReferenceListener :)

Symfony/Doctrine 2 - Use config parameter in Entity

I have a tree of Employee objects (they are in a tree-like hierarchy, with everyone having one leader, and all leaders having more employees). All the Employees have a integer parameter called units.
/**
* #ORM\Entity
* #ORM\Table(name="employees")
*/
class Employee
{
/**
* #ORM\Id
* #ORM\Column(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Employee", mappedBy="leader")
*/
protected $employees;
/**
* #ORM\ManyToOne(targetEntity("Employee", inversedBy="employees")
*/
protected $leader;
}
I need to get all the employees, who have at most N units, where N is defined in config.yml. At first, I was trying to push $configContainer into $GLOBALS, and use it in ArrayCollection::filter()'s Closure. Now I found a method, so I can use variables in the Closure:
public function getBestEmployees(&$configContainer)
{
return $this->getAllEmployees()->filter(
function bestEmployees($employee) use ($configContainer)
{
return ($employee->getUnits() >= $configContainer->getParameter('best_unit_count'));
}
);
}
Now I wonder if there is any other way to access the configuration parameters from an Entity, or do I really have to pass the whole configContainer as a reference? Or am I doing it totally wrong?
You shouldn't be accessing the service container at all inside entities. The value itself should be passed instead
public function getBestEmployees($bestUnitCount)
{
return $this->getAllEmployees()->filter(function ($employee) use ($bestUnitCount) {
return $employee->getUnits()->count() >= $bestUnitCount;
});
}
Of course, we haven't actually solved the problem yet: the parameter still needs to be fetched from the container somewhere. If this method gets invoked mostly in controller actions, I wouldn't bother doing any extra work to make things cleaner and would pass the container parameter straight in the controller action.
However, should there be a need to get the best employees in a Twig template, for example, it would be nice if it wouldn't be necessary to pass the parameter. One possibility would be using a setter method and passing the parameter down beforehand to each and every entity that gets retrieved from the database. You could do this either in repositories or entitiy managers. The most advanced solution would be to listen to the postLoad event and pass the parameter in an event listener.

Resources