Inconsistent behavior of Doctrine's entity listener - symfony

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.

Related

Symfony2 - Share Entity Between Bundles with different relationships

How do you share an entity between multiple bundles with different relationships?
For example both the ZooAnimalBundle and FarmAnimalBundle need a User Entity. A third Bundle AccountUserBundle has the User Entity.
In both the Zoo and Farm AnimalBundles I create a User Entity like so:
use Account\UserBundle\Entity\User as BaseUser;
class User extends BaseUser
{
}
I then have a Hospital entity in Zoo:
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_zoo_animals")
*/
protected $users;
And a Room entity in Farm:
class Room {
/**
* #ORM\ManyToMany(targetEntity="Farm\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_farm_animals")
*/
protected $users;
Everything works so far in that I can call Zoo.Room->getUsers() or Farm.Hospital->getUsers()
However the problem is I'm not sure on how to set up the inverse relationship in their respective User entities.
If for example I update the FarmAnimal User Entity and run doctrine:generate:entities
/**
* #ORM\Entity
*/
class User extends BaseUser
{
/**
* #ORM\ManyToMany(targetEntity="Room", mappedBy="users", cascade={"persist"})
*/
protected $rooms;
}
It will copy the protected $properties from BaseUser and create all the set and get methods which is not what I want. What is the correct way of setting up these relationships?
Update
If you don't setup the inverse relationship, how would you select all users where hospital.id = 1
$qb = $this->getEntityManager()->createQueryBuilder()
->select(
'u'
)
->from('Account\UserBundle\Entity\User','u')
->leftJoin('u.hospitals', 'h')
->andWhere('h.id = :hospital_id')
->setParameter('hospital_id',$hospital_id);
This gives the error:
Class Account\UserBundle\Entity\User has no association named hospitals
I know I could select from hospital and join user because that relationship does exist but I need to select users because I am using them with Doctrine\ORM\Tools\Pagination\Paginator
The query would be
$qb = $this->createQueryBuilder('a')
->select(
'h', 'u'
)
->leftJoin('h.users', 'u')
The problem with this is Paginator only sees one result Hospital because the Users are attached to it.
You can define abstract entity dependencies and implement them with other bundles.
First, each of the bundles depending on a user entity should define a User interface. For example:
namespace Foo\BarBundle\Entity;
interface UserInterface
{
public function getId();
public function getEmail();
// other getters
}
Then, in each entity depending on the user, define the relationship, e.g.:
namespace Foo\BarBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
*/
class Something
{
/**
* #ORM\ManyToOne(targetEntity="UserInterface")
* #Assert\NotNull
*/
protected $User;
// add other fields as required
}
Now you need to register the User entity as an implementation of the UserInterfaces:
namespace Foo\UserBundle\Entity;
use Foo\BarBundle\Entity\UserInterface as BarUserInterface;
use Foo\FoobarBundle\Entity\UserInterface as FoobarUserInterface;
/**
* #ORM\Entity
*/
class User implements BarUserInterface, FoobarUserInterface
{
// implement the demanded methods
}
Then add the following to app/config/config.yml:
doctrine:
orm:
resolve_target_entities:
Foo\BarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
Foo\FooarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
(Heads up: there will usually already be a doctrine.orm node which you'll have to extend.)
This is not a perfect solution, because you cannot really say which fields the user entity should have. On the other hand, it's strictly OOP, as you don't have to know about internals of the User implementation – you just need it to return the right values.
Creating multiple definitions of the account is the wrong way to do it, unless you want to create 3 seperate user tables (even then it's better not to do it this way).
Really you want your other entities to map to the your user entity in the account bundle.
I.e.,
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AccountBundle\Entity\User")
*/
protected $users;
Now, there is no need to create the inverse relationship. In fact, this is a bad practice since you have a bi-directional dependency. Users don't know about hospitals, but hospital knows about it's users. Now, any bundle can map to the user entity and reuse it.

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 :)

Composite key and form

I have the following associations in my database (simplified version):
This is a Many-To-Many association but with an attribute on the joining table, so I have to use One-To-Many/Many-To-One associations.
I have a form where I can add as many relations as I want to one order item and create it at the same time (mainly inspired by the How to Embed a Collection of Forms tutorial from the documentation.
When I post the form, I get the following error:
Entity of type TEST\MyBundle\Entity\Relation has identity through
a foreign entity TEST\MyBundle\Entity\Order, however this entity
has no identity itself. You have to call EntityManager#persist() on
the related entity and make sure that an identifier was generated
before trying to persist 'TEST\MyBundle\Entity\Relation'. In case
of Post Insert ID Generation (such as MySQL Auto-Increment or
PostgreSQL SERIAL) this means you have to call EntityManager#flush()
between both persist operations.
I understand this error because Doctrine tries to persist the Relation object(s) related to the order since I have the cascade={"persist"} option on the OneToMany relation. But how can I avoid this behavior?
I have tried to remove cascade={"persist"} and manually persist the entity, but I get the same error (because I need to flush() order to get the ID and when I do so, I have the same error message).
I also tried to detach() the Relation objects before the flush() but with no luck.
This problem seems unique if 1) you are using a join table with composite keys, 2) forms component, and 3) the join table is an entity that is being built by the form component's 'collection' field. I saw a lot of people having problems but not a lot of solutions, so I thought I'd share mine.
I wanted to keep my composite primary key, as I wanted to ensure that only one instance of the two foreign keys would persist in the database. Using
this entity setup as an example
/** #Entity */
class Order
{
/** #OneToMany(targetEntity="OrderItem", mappedBy="order") */
private $items;
public function __construct(Customer $customer)
{
$this->items = new Doctrine\Common\Collections\ArrayCollection();
}
}
/** #Entity */
class Product
{
/** #OneToMany(targetEntity="OrderItem", mappedBy="product") */
private $orders;
.....
public function __construct(Customer $customer)
{
$this->orders = new Doctrine\Common\Collections\ArrayCollection();
}
}
/** #Entity */
class OrderItem
{
/** #Id #ManyToOne(targetEntity="Order") */
private $order;
/** #Id #ManyToOne(targetEntity="Product") */
private $product;
/** #Column(type="integer") */
private $amount = 1;
}
The problem I was facing, if I were building an Order object in a form, that had a collection field of OrderItems, I wouldn't be able to save OrderItem entity without having saved the Order Entity first (as doctrine/SQL needs the order id for the composite key), but the Doctrine EntityManager wasn't allowing me to save the Order object that has OrderItem attributes (because it insists on saving them en mass together). You can't turn off cascade as it will complain that you haven't saved the associated entities first, and you cant save the associated entities before saving Order. What a conundrum. My solution was to remove the associated entities, save Order and then reintroduce the associated entities to the Order object and save it again. So first I created a mass assignment function of the ArrayCollection attribute $items
class Order
{
.....
public function setItemsArray(Doctrine\Common\Collections\ArrayCollection $itemsArray = null){
if(null){
$this->items->clear();
}else{
$this->items = $itemsArray;
}
....
}
And then in my Controller where I process the form for Order.
//get entity manager
$em = $this->getDoctrine()->getManager();
//get order information (with items)
$order = $form->getData();
//pull out items array from order
$items = $order->getItems();
//clear the items from the order
$order->setItemsArray(null);
//persist and flush the Order object
$em->persist($order);
$em->flush();
//reintroduce the order items to the order object
$order->setItemsArray($items);
//persist and flush the Order object again ):
$em->persist($order);
$em->flush();
It sucks that you have to persist and flush twice (see more here Persist object with two foreign identities in doctrine). But that is doctrine for you, with all of it's power, it sure can put you in a bind. But thankfully you will only have to do this when creating a new object, not editing, because the object is already in the database.
You need to persist and flush the original before you can persist and flush the relationship records. You are 100% correct in the reason for the error.
I assume from the diagram that you are trying to add and order and the relation to the contact at the same time? If so you need to persist and flush the order before you can persist and flush the relationship. Or you can add a primary key to the Relation table.
I ended up creating a separated primary key on my Relation table (instead of having the composite one).
It looks like it is a dirty fix, and I am sure there is a better way to handle this situation but it works for now.
Here is my Relations entity:
/**
* Relation
*
* #ORM\Entity
*/
class Relation
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Contact", inversedBy="relation")
*/
protected $contact;
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="relation")
*/
protected $order;
/**
* #var integer
*
* #ORM\Column(name="invoice", type="integer", nullable=true)
*/
private $invoice;
//Rest of the entity...
I then added the cascade={"persist"} option on the OneToMany relation with Order:
/**
* Orders
*
* #ORM\Entity
*/
class Order
{
/**
* #ORM\OneToMany(targetEntity="Relation", mappedBy="order", cascade={"persist"})
*/
protected $relation;
//Rest of the entity...
Et voilà!

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

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();
}
...

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