My project is a php project built with symfony 2.7
I have two entities: Lead and Customer.
Some user action create a Lead entity, and an admin can create a Customer entity based on a Lead.
Both the Lead and the Customer entity have a one to many relation to an entity Status (a Lead/Customer can have a status)
Up until now the Status of a Lead was independent from that of the Customer created from it (and vice versa), but now, I need to keep them synchronized.
They have to be the same.
I don't want to do that in controllers, because the status can be changed in many ways, and it's likely new ways will be added.
So I tried to do it with doctrine PreUpdate event listeners, but for some reason that's just not working (changing the other entity in the first one's PreUpdate event triggers a PreUpdate on the second one - and that probably kills it. but I'm not sure).
I'm going to keep trying fixing the PreUpdate event listeners, unless someone here has a better idea?
Thanks :-)
Customer PreUpdate listener
use CRM\CoreBundle\Entity\Customer;
use Doctrine\ORM\Event\PreUpdateEventArgs;
class CustomerPreUpdateListener
{
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getObject();
if (!($entity instanceof Customer)) {
return;
}
if($this->isStatusChanged($args)) {
$this->syncParentLeadStatus($args);
}
}
private function isStatusChanged(PreUpdateEventArgs $args)
{
return $args->hasChangedField('status');
}
private function syncParentLeadStatus(PreUpdateEventArgs $args)
{
/** #var Customer $entity */
$entity = $args->getObject();
if(!$entity->getLead()) {
return;
}
$newStatus = $args->getNewValue('status');
$entity->getLead()->setStatus($newStatus);
$args->getEntityManager()->persist($entity->getLead());
}
}
Lead PreUpdate listener
use CRM\BasicLeadsBundle\Entity\Lead;
use CRM\CoreBundle\Entity\Customer;
use Doctrine\ORM\Event\PreUpdateEventArgs;
class LeadPreUpdateListener
{
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getObject();
if (!($entity instanceof Lead)) {
return;
}
if($this->isStatusChanged($args)) {
$this->syncCustomerStatus($args);
}
}
private function isStatusChanged(PreUpdateEventArgs $args)
{
return $args->hasChangedField('status');
}
private function syncCustomerStatus(PreUpdateEventArgs $args)
{
/** #var Lead $entity */
$entity = $args->getObject();
if(0 === $entity->getCustomersCreated()->count()) {
return;
}
$newStatus = $args->getNewValue('status');
foreach ($entity->getCustomersCreated() as $customer) {
/** #var Customer $customer */
$customer->setStatus($newStatus);
$args->getEntityManager()->persist($customer);
}
}
}
Related
I have this business requirement where I need to create update entities of the same entity type with a single request(Tabular form).
Entity Class:
class Tag{
private $name;
}
I have setup this custom controller to accept multiple instances of the Tag class in json.
For example :
{
[
{"name":"tag 1","id":"T1"},
{"name":"tag 2","id":"T2"},
{"name":"tag 3","id":"T3"}
]
}
And my custom action is defined as below.
class TagMultipleAction extends AbstractController
{
public function __construct(
private EntityManagerInterface $entityManager,
private SerializerInterface $serializer,
private ValidatorInterface $validator
)
{
}
public function __invoke(TagData $data): JsonResponse
{
$tagPostErrors = [];
foreach ($data as $tagPst) {
$tag = new Tag();
$tag->setName($tagPst['name']);
try {
$this->validator->validate($tag, ['groups' => 'create']);
$this->entityManager->persist($tag);
} catch (ValidationException $ex) {
$tagPostErrors[$tagPst['id']] = [$ex->getMessage()];
}
}
if (count($tagPostErrors) > 0) {
return new JsonResponse($tagPostErrors, 400);
}
$this->entityManager->flush();
return new JsonResponse(null, 204);
}
This controller works fine and all the data persists without any issue.
What I want to know is that instead of me mapping the post data to object($tag->setName($tagPst['name']), is there a way to automate this using DTO's. Hoping to find a better way to deal with tabular data.
I am new to EventListeners (even though I can read here and there that it is best to use subscribers) and I got maybe a silly question:
Can I access data from another table (Table 2) in my EventListener plugged in my Entity 1 (Table1).
Here my example:
representation of my table
I have an EventListener on my table 1 and want to access the rows E "crit2" and "crit3" to check their value.
If crit 2 is "combo", I would concatenate on F the value of A and D (Table 1) otherwise, the other, A and D.
I tried a eventListener and get a file NewOrganised.php in the folder EventListener:
<?php
namespace App\EventListener;
use App\Entity\ProductsInfoOrganised;
use Doctrine\Persistence\Event\LifecycleEventArgs;
class NewOrganised
{
// the listener methods receive an argument which gives you access to
// both the entity object of the event and the entity manager itself
public function postPersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
// if this listener only applies to certain entity types,
// add some code to check the entity type as early as possible
if (!$entity instanceof ProductsInfoOrganised) {
return;
}
$entityManager = $args->getObjectManager();
// ... do something with the Product entity
}
}
In my entity ProductsInfoOrganised aka Table 1, I added to the end
/**
* #ORM\PrePersist
*/
public function setValuesLeftBlank(): void
{
$this->triabc = $this->packId . $this->adapteGaucher;
$this->triacb = $this->adapteGaucher . $this->packId;
// if( $crit2 == "localisation" ) {
// $this->triId = $this->shopId . $this->triabc;
// } else {
// $this->triId = $this->shopId . $this->triacb;
// }
}
I call it in my services.yaml:
App\EventListener\NewOrganised:
tags:
-
name: 'doctrine.event_listener'
priority: 500
connection: 'default'
I am not sure if I should do as I did in my Entity and call an entityManagerInterface there to get value of the rows wanted or in my EventListener:
public function postPersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
$entityRef = $args->getObject();
// if this listener only applies to certain entity types,
// add some code to check the entity type as early as possible
if (!$entity instanceof ProductsInfoOrganised) {
return;
}
if (!$entityRef instanceof ProductsInfoOrganisation) {
return;
}
$entityManager = $args->getObjectManager();
// ... do something with the Product entity
}
Thanks
I am developing my first Symfony 4 application and I migrating from Symfony 2+ and symfony 3+.
Right now I am developing a back-end and all of my entity classes have a addedBy() and updatedBy() methods where I need to record the current logged in administrator.
I would like to have something like an event listener where I do not have to set those methods in all of my controllers.
How to accomplish this?
First, to simplify matters and help down the road I would create an interface this user-tracking entities would need to comply with:
interface UserTracking
{
public function addedBy(UserInterface $user);
public function updatedby(UserInterface $user);
public function getAddedBy(): ?UserInterface;
public function getUpdatedBy(): ?UserInterface;
}
Then you can create a Doctrine event listener, and inject the Security component there:
class UserDataListener
{
protected $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function prePersist(LifecycleEventArgs $event): void
{
$entity = $event->getObject();
$user = $this->security->getUser();
// only do stuff if $entity cares about user data and we have a logged in user
if ( ! $entity instanceof UserTracking || null === $user ) {
return;
}
$this->setUserData($entity, $user);
}
private function preUpdate(LifecycleEventArgs $event) {
$this->prePersist($event);
}
private function setUserData(UserTracking $entity, UserInterface $user)
{
if (null === $entity->getAddedBy()) {
$entity->addedBy($user);
}
$entity->updatedBy($user);
}
}
You'd need to tag the listener appropriately, so it triggers on prePersist and preUpdate:
services:
user_data_listener:
class: App\Infrastructure\Doctrine\Listener\UserDataListener
tags:
- { name: doctrine.event_listener, event: prePersist }
- { name: doctrine.event_listener, event: preUpdate }
While the above should work, I believe it's generally not such a great idea to use Doctrine events this way, since you are coupling your domain logic with Doctrine, and are hiding changing under a layer of magic that may not be immediately evident for other developers working with your application.
I'd put the createdBy as a constructor parameter, and set updateBy explicitly when needed. It's just one line of code each time, but you gain clarity and expressiveness, and you have a simpler system with less moving parts.
class FooEntity
{
private $addedBy;
private $updatedBy;
public function __construct(UserInterface $user)
{
$this->addedBy = $user;
}
public function updatedBy(UserInterface $user)
{
$this->updatedBy = $user;
}
}
This expresses much better what's happening with the domain, and future coders in your application do no have to dig around for the possible extensions you may have installed and enabled, or what events are being triggered.
You probably do not want to reinvent the wheel. There is the Blameable Doctrine Extension that already does that. You can install it with :
composer require antishov/doctrine-extensions-bundle
As there is already a recipe to configure it. Then you can activate the extension and then use it with something like :
Specific documentation for the Blameable extension can be found here
use Gedmo\Mapping\Annotation as Gedmo;
class Post {
/**
* #var User $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="App\Entity\User")
*/
private $createdBy;
}
I've got an entity called Logs that has a ManyToOne relation to an HourlyRates entity. Both Logs and HourlyRates have date properties. When adding a log with a specific date, an hourlyRate is assigned to it if the log-date fits within the rate's time range. I'm using the Doctrine Extensions Bundle, so the data in each entity can be soft-deleted.
What needs to be done:
After soft-deleting an HourlyRate the related Log has to be updated, so that the nearest existing past HourlyRate takes the place of the deleted one.
I tried to use preSoftDelete, postSoftDelete, preRemove and postRemove methods inside an HourlyRate entity listener. The code was being executed and the setters were working properly, but the database hasn't been updated in any of said cases. An "EntityNotFoundException" was being thrown everytime.
My second approach was to use the preRemove event along with setting the cascade option to "all" by using annotations in the HourlyRate class. As a result, soft-deleting an hourlyRate caused soft-deleting of the related log.
The Log entity:
class Log
{
use SoftDeleteableEntity;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\HourlyRate", inversedBy="logs")
* #ORM\JoinColumn(nullable=false)
*/
private $hourlyRate;
public function setHourlyRate(?HourlyRate $hourlyRate): self
{
$this->hourlyRate = $hourlyRate;
return $this;
}
}
The HourlyRate entity:
class HourlyRate
{
use SoftDeleteableEntity;
//other code
/**
* #ORM\OneToMany(targetEntity="App\Entity\Log", mappedBy="hourlyRate", cascade={"all"})
*/
private $logs;
}
The HourlyRate entity listener:
class HourlyRateEntityListener
{
public function preRemove(HourlyRate $hourlyRate, LifecycleEventArgs $args)
{
$entityManager = $args->getObjectManager();
/** #var HourlyRateRepository $HRrepo */
$HRrepo = $entityManager->getRepository(HourlyRate::class);
foreach ($hourlyRate->getLogs() as $log)
{
$rate = $HRrepo->findHourlyRateByDate($log->getDate(), $log->getUser(), $hourlyRate);
$log->setHourlyRate($rate);
}
}
}
The repository method:
class HourlyRateRepository extends ServiceEntityRepository
{
public function findHourlyRateByDate(?\DateTimeInterface $datetime, User $user, ?HourlyRate $ignore = null): ?HourlyRate
{
$qb = $this->createQueryBuilder('hr')
->where('hr.date <= :hr_date')
->andWhere('hr.user = :user')
->orderBy('hr.date', 'DESC')
->setMaxResults(1)
->setParameters(array('hr_date' => $datetime, 'user' => $user));
//ignore the "deleted" hourlyRate
if($ignore){
$qb->andWhere('hr.id != :ignored')
->setParameter('ignored', $ignore->getId());
}
return $qb->getQuery()
->getOneOrNullResult()
;
}
}
Thank you in advance for any of your help.
EDIT:
Okay so after a whole week of trials and errors i finally managed to achieve the result I wanted.
I removed the One-To-Many relation between the hourlyRates and the logs from the entities, but left the $hourlyRate property inside the Log class. Then I got rid of the HourlyRateEntityListener and the preRemove() method from the LogEntityListener. Instead, I implemented the postLoad() method:
class LogEntityListener
{
public function postLoad(Log $log, LifeCycleEventArgs $args)
{
$entityManager = $args->getObjectManager();
$HRrepo = $entityManager->getRepository(HourlyRate::class);
/** #var HourlyRateRepository $HRrepo */
$rate = $HRrepo->findHourlyRateByDate($log->getDate(), $log->getUser());
$log->setHourlyRate($rate);
}
}
This approach allows me to set the proper hourlyRate for each log without involving the database. Idk if this solution is acceptable though.
I have a User entity, and I would like to archive it when banned. I have the following preUpdate listener:
/**
* #ORM\PreUpdate
*/
public function preUpdate(PreUpdateEventArgs $eventArgs) {
if ($eventArgs->hasChangedField('banned') {
$this->setIsArchived(true);
}
if ($eventArgs->hasChangedField('isArchived')) {
/* do Special work here */
}
}
How do I inform eventArgs about the field changed inside the handler itself?
if you edit an entity within the eventArgs, I think you need to persist it and then run the computeChangeSet or computeChangeSets in the UnitOfWork in order for you to use the hasChangedField:
$entity = $eventArgs->getObject();
$em = $eventArgs->getObjectManager();
$uow = $em->getUnitOfWork();
if ($eventArgs->hasChangedField('banned') {
$entity->setIsArchived(true);
$em->persist($entity);
}
$uow->computeChangeSets();
if ($eventArgs->hasChangedField('isArchived')) {
/* do Special work here */
}