Hello and thank you for your concern.
i'am student and it's my first time creating an eventSubriber.
I would like to change all my attribute "display" (Boolean) in my Entity "Menu" when i try to update one of my entity "Menu" for have only one Attribute "display" true in all my entities.
I use EasyAdmin 3 in my project Symfony if you need to know.
My probleme start with $this->entityManager->flush(); in UniqueBoolTureEvent.php i think.
thank you in advance.
Service.yaml
App\EventListener\UniqueBoolTrueEvent:
tags:
- { name: doctrine.event_listener, event: preUpdate, Lazy: true }
UniqueBoolTrueEvent.php
<?php
namespace App\EventListener;
use App\Entity\Menu;
use Doctrine\Common\EventSubscriber;
// Entity to listen
use Doctrine\Persistence\ObjectManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
class UniqueBoolTrueEvent implements EventSubscriber {
private $entityManager;
/**
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function getSubscribedEvents() {
return array('preUpdate');//Event to listen
}
public function preUpdate( PreUpdateEventArgs $eventArgs ) {
if ($eventArgs->getEntity() instanceof Menu) {
if ($eventArgs->hasChangedField('display') && $eventArgs->getNewValue('display') == 'true') {
//get the id of entity change for true
$entityId = $eventArgs->getEntity()->getId();
// search the entity already on true exept entity change now
$displayTrue = $this->entityManager->getRepository(Menu::class)->findByDisplay($entityId);
//Change the value for entity already on true exept entity change now
foreach ($displayTrue as $display) {
$display->setDisplay(false);
$this->entityManager->persist($display);
}
$this->entityManager->flush();
}
}
}
}
MenuRepository.php
<?php
namespace App\Repository;
use App\Entity\Menu;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* #method Menu|null find($id, $lockMode = null, $lockVersion = null)
* #method Menu|null findOneBy(array $criteria, array $orderBy = null)
* #method Menu[] findAll()
* #method Menu[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class MenuRepository extends ServiceEntityRepository
{
/**
* #param ManagerRegistry $registry
*/
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Menu::class);
}
/**
* #return Menu[] Returns an array of Menu objects
*/
public function findByDisplay($entityId)
{
return $this->createQueryBuilder('m')
->andWhere('m.display = true')
->andWhere('m.id != :entityId')
->setParameter('entityId', $entityId)
->orderBy('m.id', 'ASC')
->getQuery()
->getResult()
;
}
}
after that i have only one error in my ajax request and cannot see where is the probleme.
Thank you in advance.
My Solution No 2 Solutions lol
First solution with EventSubscriberInterface and BeforeEntityUpdatedEvent. With this solution you don't need to add config in service.yaml
class UniqueBoolTrueEvent implements EventSubscriberInterface
{
private $entityManager;
/**
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public static function getSubscribedEvents()
{
return [
BeforeEntityUpdatedEvent::class => ['setDisplayTrue'],
];
}
public function setDisplayTrue(BeforeEntityUpdatedEvent $event)
{
$entity = $event->getEntityInstance();
$entityId = $entity->getId();
if ($entity instanceof Menu) {
$displayTrue = $this->entityManager->getRepository(Menu::class)->findByDisplay($entityId);
foreach ($displayTrue as $display) {
$display->setDisplay(false);
$this->entityManager->persist($display);
}
$this->entityManager->flush();
}
}
}
And my second solution with onFlush()
service.yaml
App\EventListener\UniqueBoolTrueEvent:
tags:
- { name: doctrine.event_listener, event: onFlush}
UniqueBoolTrueEvent.php
class UniqueBoolTrueEvent
{
private $entityManager;
/**
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$entities = $uow->getScheduledEntityUpdates();
foreach ($entities as $entity) {
//continue only if the object to be updated is a Ticket
if ($entity instanceof Menu) {
$entityId = $entity->getId();
$entityDisplay = $entity->getDisplay();
// //get all the changed properties of the Menu object
// $changes_set = $uow->getEntityChangeSet($entity);
//Check if the changed value of "display" is True
if ($entityDisplay == true) {
//find the entity with "display" value true exept the entity update
$displayTrue = $this->entityManager->getRepository(Menu::class)->findByDisplay($entityId);
// Change the value of "display" to false for each entity on true exept the entity update
foreach ($displayTrue as $display) {
$display->setDisplay(false);
$em->persist($display);
// for flush
$classMetadata = $em->getClassMetadata(get_class($entity));
// dd($classMetadata);
$uow->computeChangeSet($classMetadata, $display);
}
}
}
}
}
}
Related
I'm new on Symfony 6 and i've got some questions about this framework.
Recently i got this error but i don't understand why. I created all databases from the command line.
My error is : Case mismatch between loaded and declared class names: "App\Entity\tag" vs "App\Entity\Tag".
In my database all tables are written with lowercase syntaxe.
everything worked between I insert relation with all table.
i tried to insert this code in my entity to use the correct syntaxe but it doesn't work.
Entity\Tag.php
#[ORM\Table(name: 'tag')]
Some people speak about this parameter inside my database (MYSQL) but i don't know if this got an impact :
lower_case_table_names = (0 1 2)
Do you have an idea ?
This is my entity tag
<?php
namespace App\Entity;
use App\Repository\TagRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TagRepository::class)]
class Tag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $nomtag = null;
#[ORM\ManyToOne(inversedBy: 'idcatetag')]
#[ORM\JoinColumn(nullable: false)]
private ?catetag $idcatetag = null;
#[ORM\ManyToMany(targetEntity: Mission::class, mappedBy: 'idtagmissionassign')]
private Collection $missions;
#[ORM\ManyToMany(targetEntity: Service::class, mappedBy: 'idtagserviceassign')]
private Collection $services;
public function __construct()
{
$this->missions = new ArrayCollection();
$this->services = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNomtag(): ?string
{
return $this->nomtag;
}
public function setNomtag(string $nomtag): self
{
$this->nomtag = $nomtag;
return $this;
}
public function getIdcatetag(): ?catetag
{
return $this->idcatetag;
}
public function setIdcatetag(?catetag $idcatetag): self
{
$this->idcatetag = $idcatetag;
return $this;
}
/**
* #return Collection<int, Mission>
*/
public function getMissions(): Collection
{
return $this->missions;
}
public function addMission(Mission $mission): self
{
if (!$this->missions->contains($mission)) {
$this->missions->add($mission);
$mission->addIdtagmissionassign($this);
}
return $this;
}
public function removeMission(Mission $mission): self
{
if ($this->missions->removeElement($mission)) {
$mission->removeIdtagmissionassign($this);
}
return $this;
}
/**
* #return Collection<int, Service>
*/
public function getServices(): Collection
{
return $this->services;
}
public function addService(Service $service): self
{
if (!$this->services->contains($service)) {
$this->services->add($service);
$service->addIdtagserviceassign($this);
}
return $this;
}
public function removeService(Service $service): self
{
if ($this->services->removeElement($service)) {
$service->removeIdtagserviceassign($this);
}
return $this;
}
}
This is my repo file :
<?php
namespace App\Repository;
use App\Entity\Tag;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* #extends ServiceEntityRepository<Tag>
*
* #method Tag|null find($id, $lockMode = null, $lockVersion = null)
* #method Tag|null findOneBy(array $criteria, array $orderBy = null)
* #method Tag[] findAll()
* #method Tag[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TagRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Tag::class);
}
public function save(Tag $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Tag $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * #return Tag[] Returns an array of Tag objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('t')
// ->andWhere('t.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('t.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Tag
// {
// return $this->createQueryBuilder('t')
// ->andWhere('t.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
check file name , it should be Tag.php with T uppercase, symfony create table automatically in lowercase. If everything is ok try to delete you're file and rewrite it properly with make:entity or php bin/console doctrine:schema:update --force after entity's modification
I create service for add formType then persist object and in controller I invoke data but I have error shown on below image:
in controller i extend class abstractController content getHandler and I have view newskill.html.twig
Code SkillController.php:
<?php
namespace AppBundle\Controller\Condidate;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Skill;
use AppBundle\Controller\AbstractController;
use AppBundle\Form\SkillType;
/**
*Class SkillController.
*/
class SkillController extends AbstractController
{
/**
*function handler.
*/
protected function getHandler(){
//var_dump('test');
}
/**
*function addSkill
* #param Request $request
* #return \Symfony\Component\Form\Form The form
*/
public function addSkillAction(Request $request) {
$skill = $this->getHandler()->post();
if ($skill instanceof \AppBundle\Entity\Skill) {
return $this->redirectToRoute('ajouter_info');
}
return $this->render('skills/newskill.html.twig', array(
'form' => $form->createView(),));
}
}
Code SkillHandler.php:
<?php
namespace AppBundle\Handler;
use AppBundle\Handler\HandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Skill;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Form\formFactory;
/**
* SkillHandler.
*/
class SkillHandler implements HandlerInterface {
/**
*
* #var EntityManager
*/
protected $em;
/**
*
* #var formFactory
*/
private $formFactory;
/**
*function construct.
*/
public function __construct(EntityManager $entityManager, formFactory $formFactory)
{
$this->em = $entityManager;
$this->formFactory = $formFactory;
}
/**
*function post
*/
public function post(array $parameters, array $options = []) {
$form = $this->formFactory->create(\AppBundle\Form\SkillType::class, $object, $options);
$form->submit($parameters);
if ($form->isValid()) {
$skill = $form->getData();
$this->persistAndFlush($skill);
return $skill;
}
return $form->getData();
}
/**
*function persisteAndFlush
*/
protected function persistAndFlush($object) {
$this->em->persist($object);
$this->em->flush();
}
/**
*function get
*/
public function get($id){
throw new \DomainException('Method SkillHandler::get not implemented');
}
/**
*function all
*/
public function all($limit = 10, $offset = 0){
throw new \DomainException('Method SkillHandler::all not implemented');
}
/**
*function put
*/
public function put($resource, array $parameters, array $options){
throw new \DomainException('Method SkillHandler::put not implemented');
}
/**
*function patch
*/
public function patch($resource, array $parameters, array $options){
throw new \DomainException('Method SkillHandler::patch not implemented');
}
/**
*function delete
*/
public function delete($resource){
throw new \DomainException('Method SkillHandler::delete not implemented');
}
}
code services.yml:
skill_add:
class: AppBundle\Handler\SkillHandler
arguments:
- "#doctrine.orm.entity_manager"
- "#form.factory"
public: true
Any help would be appreciated.
Your $this->getHandler() retruns null.
Solution can be checking if $this->getHandler() doesn't return null in first place.
if (!$this->getHandler()) {
throw new \Exception(sprintf('Handler cannot be null')
} else {
$skill = $this->getHandler()->post();
}
Try this, firstly you should take your handler into getHandler() method at your Controller.
protected function getHandler(){
return $this->get('skill_add');
}
Hi I have an onFlush listener:
<?php
namespace FM\AppBundle\EventListener;
use FM\AdminBundle\Entity\Address\DeliveryAddress;
use Doctrine\ORM\Event\OnFlushEventArgs;
class DeliveryAddressListener
{
/**
* #param OnFlushEventArgs $args
*/
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof DeliveryAddress) {
$this->addPostalToUser($entity, $args);
}
}
}
/**
* #param DeliveryAddress $deliveryAddress
* #param OnFlushEventArgs $args
*/
public function addPostalToUser(DeliveryAddress $deliveryAddress, OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$user = $deliveryAddress->getOwner();
$user->setPostalCode($deliveryAddress->getZipCode());
}
}
service.yml:
delivery_address.listener:
class: FM\AppBundle\EventListener\DeliveryAddressListener
tags:
- { name: doctrine.event_listener, event: onFlush }
I'm trying to set the new zipCode to the User. But it does not seem to work.
Even when I'm adding a $em->persist($user).
I'm looking throught this doc: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#onflush
But I don't understand how I can make it works with this explanation:
If you create and persist a new entity in onFlush, then calling EntityManager#persist() is not enough. You have to execute an additional call to $unitOfWork->computeChangeSet($classMetadata, $entity).
When manipulating fields, they should be done in the preUpdaet/prePersist.
AppBundle/EventSubscriber/EntitySubscriber.php
namespace AppBundle\EventSubscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
class EntitySubscriber implements EventSubscriber
{
private $now;
public function __construct()
{
$this->now = \DateTime::createFromFormat('Y-m-d h:i:s', date('Y-m-d h:i:s'));
}
public function getSubscribedEvents()
{
return [
'prePersist',
'preUpdate'
];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if (method_exists($entity, 'setCreatedAt')) {
$entity->setUpdatedAt($this->now);
}
if (method_exists($entity, 'setUpdatedAt')) {
$entity->setUpdatedAt($this->now);
}
}
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if (method_exists($entity, 'setUpdatedAt')) {
$entity->setUpdatedAt($this->now);
}
}
}
services.yml
app.entity_subscriber:
class: AppBundle\EventSubscriber\EntitySubscriber
tags:
- { name: doctrine.event_subscriber, connection: default }
If you need to create an object, persist it and flush it in your listener, then tlorens' answer won't work, as Doctrine docs mention that this must be done with an onFlush Event.
Initial question was how to make it work following docs advice:
If you create and persist a new entity in onFlush, then calling EntityManager#persist() is not enough. You have to execute an additional call to $unitOfWork->computeChangeSet($classMetadata, $entity).
And this is a way to achieve this:
/**
* #param OnFlushEventArgs $eventArgs
*/
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof User) {
$uow->computeChangeSets();
$changeSet = $uow->getEntityChangeSet($entity);
// In this exemple, User has a boolean property 'enabled' and a log will be created if it is passed to 'false'
if ($changeSet && isset($changeSet['enabled']) && $changeSet['enabled'][1] === false) {
$log = new Log();
$em->persist($log);
$uow->computeChangeSet($em->getClassMetadata(get_class($log)), $log);
}
}
}
Well it works when I use that:
// Remove event, if we call $this->em->flush() now there is no infinite recursion loop!
$eventManager->removeEventListener('onFlush', $this);
My Listener
namespace FM\AppBundle\EventListener;
use FM\AdminBundle\Entity\Address\DeliveryAddress;
use Doctrine\ORM\Event\OnFlushEventArgs;
class DeliveryAddressListener
{
/**
* #param OnFlushEventArgs $args
*/
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$eventManager = $em->getEventManager();
// Remove event, if we call $this->em->flush() now there is no infinite recursion loop!
$eventManager->removeEventListener('onFlush', $this);
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof DeliveryAddress) {
$this->addPostalToUser($entity, $args);
}
}
}
/**
* #param DeliveryAddress $deliveryAddress
* #param OnFlushEventArgs $args
*/
public function addPostalToUser(DeliveryAddress $deliveryAddress, OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$user = $deliveryAddress->getOwner();
$user->setPostalCode($deliveryAddress->getZipCode());
$em->flush();
}
}
I'm on symfony 2.6.3 with stof Doctrine extension.
TimeStampable and SoftDeletable work well.
Also Blameable "on create" and "on update" are working well too:
/**
* #var User $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="createdBy", referencedColumnName="id")
*/
protected $createdBy;
/**
* #var User $updatedBy
*
* #Gedmo\Blameable(on="update")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="updatedBy", referencedColumnName="id")
*/
protected $updatedBy;
But "on change" seems not to be working.
/**
* #var User $deletedBy
*
* #Gedmo\Blameable(on="change", field="deletedAt")
* #ORM\ManyToOne(targetEntity="my\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deletedBy", referencedColumnName="id")
*/
protected $deletedBy;
I've got SoftDeletable configured on "deletedAt" field. SoftDeletable works fine, but deletedBy is never filled.
How can I manage to make it work? I just want to set user id who deleted the entity.
Here my solution :
mybundle.soft_delete:
class: Listener\SoftDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine_mongodb.odm.event_listener, event: preSoftDelete }
class SoftDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* Method called before "soft delete" system happened.
*
* #param LifecycleEventArgs $lifeCycleEvent Event details.
*/
public function preSoftDelete(LifecycleEventArgs $lifeCycleEvent)
{
$document = $lifeCycleEvent->getDocument();
if ($document instanceof SoftDeletedByInterface) {
$token = $this->tokenStorage->getToken();
if (is_object($token)) {
$oldValue = $document->getDeletedBy();
$user = $token->getUser();
$document->setDeletedBy($user);
$uow = $lifeCycleEvent->getObjectManager()->getUnitOfWork();
$uow->propertyChanged($document, 'deletedBy', $oldValue, $user);
$uow->scheduleExtraUpdate($document, array('deletedBy' => array($oldValue, $user)));
}
}
}
}
The problem is you want to update entity (set user) when you call remove method on it.
Currently there may not be a perfect solution for registering user who soft-deleted an object using Softdeleteable + Blameable extensions.
Some idea might be to overwrite SoftDeleteableListener (https://github.com/Atlantic18/DoctrineExtensions/blob/master/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php) but I had a problem doing it.
My current working solution is to use Entity Listener Resolver.
MyEntity.php
/**
* #ORM\EntityListeners({„Acme\MyBundle\Entity\Listener\MyEntityListener" })
*/
class MyEntity {
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deleted_by", referencedColumnName="id")
*/
private $deletedBy;
public function getDeletedBy()
{
return $this->deletedBy;
}
public function setDeletedBy($deletedBy)
{
$this->deletedBy = $deletedBy;
}
MyEntityListener.php
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Acme\MyBundle\Entity\MyEntity;
class MyEntityListener
{
/**
* #var TokenStorageInterface
*/
private $token_storage;
public function __construct(TokenStorageInterface $token_storage)
{
$this->token_storage = $token_storage;
}
public function preRemove(MyEntity $myentity, LifecycleEventArgs $event)
{
$token = $this->token_storage->getToken();
if (null !== $token) {
$entityManager = $event->getObjectManager();
$myentity->setDeletedBy($token->getUser());
$entityManager->persist($myentity);
$entityManager->flush();
}
}
}
An imperfection here is calling flush method.
Register service:
services:
myentity.listener.resolver:
class: Acme\MyBundle\Entity\Listener\MyEntityListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.orm.entity_listener, event: preRemove }
Update doctrine/doctrine-bundle in composer.json:
"doctrine/doctrine-bundle": "1.3.x-dev"
If you have any other solutions, especially if it is about SoftDeleteableListener, please post it here.
This is my solution, I use preSoftDelete event:
app.event.entity_delete:
class: AppBundle\EventListener\EntityDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.event_listener, event: preSoftDelete, connection: default }
and service:
<?php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class EntityDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function preSoftDelete(LifecycleEventArgs $args)
{
$token = $this->tokenStorage->getToken();
$object = $args->getEntity();
$om = $args->getEntityManager();
$uow = $om->getUnitOfWork();
if (!method_exists($object, 'setDeletedBy')) {
return;
}
if (null == $token) {
throw new AccessDeniedException('Only authorized users can delete entities');
}
$meta = $om->getClassMetadata(get_class($object));
$reflProp = $meta->getReflectionProperty('deletedBy');
$oldValue = $reflProp->getValue($object);
$reflProp->setValue($object, $token->getUser()->getUsername());
$om->persist($object);
$uow->propertyChanged($object, 'deletedBy', $oldValue, $token->getUser()->getUsername());
$uow->scheduleExtraUpdate($object, array(
'deletedBy' => array($oldValue, $token->getUser()->getUsername()),
));
}
}
It's not consistence because I check setDeletedBy method exists and set deletedBy property, but it work for me, and you can upgrade this code for your needs
Here is another solution I found :
Register a service:
softdeleteable.listener:
class: AppBundle\EventListener\SoftDeleteableListener
arguments:
- '#security.token_storage'
tags:
- { name: doctrine.event_listener, event: preFlush, method: preFlush }
SoftDeleteableListener:
/**
* #var TokenStorageInterface|null
*/
private $tokenStorage;
/**
* DoctrineListener constructor.
*
* #param TokenStorageInterface|null $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param PreFlushEventArgs $event
*/
public function preFlush(PreFlushEventArgs $event)
{
$user = $this->getUser();
$em = $event->getEntityManager();
foreach ($em->getUnitOfWork()->getScheduledEntityDeletions() as $object) {
/** #var SoftDeleteableEntity|BlameableEntity $object */
if (method_exists($object, 'getDeletedBy') && $user instanceof User) {
$object->setDeletedBy($user);
$em->merge($object);
// Persist and Flush allready managed by other doctrine extensions.
}
}
}
/**
* #return User|void
*/
public function getUser()
{
if (!$this->tokenStorage || !$this->tokenStorage instanceof TokenStorageInterface) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
$token = $this->tokenStorage->getToken();
if (!$token) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
$user = $token->getUser();
if (!$user instanceof User) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
return $user;
}
Context:
Let there be two entities (correctly mapped for Doctrine).
Post with properties {$id (integer, autoinc), $name (string), $tags (collection of Tag)}
Tag with properties {$id (integer, autoinc), $name (string), $posts (collection of Post)}
Relationship between these two is Many-To-Many.
Problem:
When creating a new Post, I want to immediately add tags to it.
If I wanted to add Tags that already are peristed, I would create entity field type, no problem with that.
But what would I do, if I wanted to add completely new Tags too? (Check some of already existing tags, fill name for new tag, maybe add some another new tag, then after submit assign everyting properly to Post entity)
Create new Post:
Name: [__________]
Add tags
|
|[x] alpha
|[ ] beta
|[x] gamma
|
|My tag doesnt exist, create new:
|
|Name: [__________]
|
|+Add another new tag
Is there any way to do this? I know the basics of Symfony 2, but have no idea how to deal with this. Also surprised me I havent found my answer anywhere, seems like a common problem to me. What am I missing?
My Tag entity has a unique field for the tag name. For add Tags I use a new form type and a transformer.
The Form Type:
namespace Sg\RecipeBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Sg\RecipeBundle\Form\DataTransformer\TagsDataTransformer;
class TagType extends AbstractType
{
/**
* #var RegistryInterface
*/
private $registry;
/**
* #var SecurityContextInterface
*/
private $securityContext;
/**
* Ctor.
*
* #param RegistryInterface $registry A RegistryInterface instance
* #param SecurityContextInterface $securityContext A SecurityContextInterface instance
*/
public function __construct(RegistryInterface $registry, SecurityContextInterface $securityContext)
{
$this->registry = $registry;
$this->securityContext = $securityContext;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(
new TagsDataTransformer(
$this->registry,
$this->securityContext
),
true
);
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return 'text';
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'tag';
}
}
The Transformer:
<?php
/*
* Stepan Tanasiychuk is the author of the original implementation
* see: https://github.com/stfalcon/BlogBundle/blob/master/Bridge/Doctrine/Form/DataTransformer/EntitiesToStringTransformer.php
*/
namespace Sg\RecipeBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Doctrine\ORM\EntityManager;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Sg\RecipeBundle\Entity\Tag;
/**
* Tags DataTransformer.
*/
class TagsDataTransformer implements DataTransformerInterface
{
/**
* #var EntityManager
*/
private $em;
/**
* #var SecurityContextInterface
*/
private $securityContext;
/**
* Ctor.
*
* #param RegistryInterface $registry A RegistryInterface instance
* #param SecurityContextInterface $securityContext A SecurityContextInterface instance
*/
public function __construct(RegistryInterface $registry, SecurityContextInterface $securityContext)
{
$this->em = $registry->getEntityManager();
$this->securityContext = $securityContext;
}
/**
* Convert string of tags to array.
*
* #param string $string
*
* #return array
*/
private function stringToArray($string)
{
$tags = explode(',', $string);
// strip whitespaces from beginning and end of a tag text
foreach ($tags as &$text) {
$text = trim($text);
}
// removes duplicates
return array_unique($tags);
}
/**
* Transforms tags entities into string (separated by comma).
*
* #param Collection | null $tagCollection A collection of entities or NULL
*
* #return string | null An string of tags or NULL
* #throws UnexpectedTypeException
*/
public function transform($tagCollection)
{
if (null === $tagCollection) {
return null;
}
if (!($tagCollection instanceof Collection)) {
throw new UnexpectedTypeException($tagCollection, 'Doctrine\Common\Collections\Collection');
}
$tags = array();
/**
* #var \Sg\RecipeBundle\Entity\Tag $tag
*/
foreach ($tagCollection as $tag) {
array_push($tags, $tag->getName());
}
return implode(', ', $tags);
}
/**
* Transforms string into tags entities.
*
* #param string | null $data Input string data
*
* #return Collection | null
* #throws UnexpectedTypeException
* #throws AccessDeniedException
*/
public function reverseTransform($data)
{
if (!$this->securityContext->isGranted('ROLE_AUTHOR')) {
throw new AccessDeniedException('Für das Speichern von Tags ist die Autorenrolle notwendig.');
}
$tagCollection = new ArrayCollection();
if ('' === $data || null === $data) {
return $tagCollection;
}
if (!is_string($data)) {
throw new UnexpectedTypeException($data, 'string');
}
foreach ($this->stringToArray($data) as $name) {
$tag = $this->em->getRepository('SgRecipeBundle:Tag')
->findOneBy(array('name' => $name));
if (null === $tag) {
$tag = new Tag();
$tag->setName($name);
$this->em->persist($tag);
}
$tagCollection->add($tag);
}
return $tagCollection;
}
}
The config.yml
recipe.tags.type:
class: Sg\RecipeBundle\Form\Type\TagType
arguments: [#doctrine, #security.context]
tags:
- { name: form.type, alias: tag }
use the new Type:
->add('tags', 'tag', array(
'label' => 'Tags',
'required' => false
))
Similarities, like "symfony" and "smfony" can be prevented with an autocomplete function:
TagController:
<?php
namespace Sg\RecipeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
/**
* Tag controller.
*
* #Route("/tag")
*/
class TagController extends Controller
{
/**
* Get all Tag entities.
*
* #Route("/tags", name="tag_tags")
* #Method("GET")
*
* #return \Symfony\Component\HttpFoundation\Response
*/
public function getTagsAction()
{
$request = $this->getRequest();
$isAjax = $request->isXmlHttpRequest();
if ($isAjax) {
$em = $this->getDoctrine()->getManager();
$search = $request->query->get('term');
/**
* #var \Sg\RecipeBundle\Entity\Repositories\TagRepository $repository
*/
$repository = $em->getRepository('SgRecipeBundle:Tag');
$qb = $repository->createQueryBuilder('t');
$qb->select('t.name');
$qb->add('where', $qb->expr()->like('t.name', ':search'));
$qb->setMaxResults(5);
$qb->orderBy('t.name', 'ASC');
$qb->setParameter('search', '%' . $search . '%');
$results = $qb->getQuery()->getScalarResult();
$json = array();
foreach ($results as $member) {
$json[] = $member['name'];
};
return new Response(json_encode($json));
}
return new Response('This is not ajax.', 400);
}
}
form.html.twig:
<script type="text/javascript">
$(document).ready(function() {
function split(val) {
return val.split( /,\s*/ );
}
function extractLast(term) {
return split(term).pop();
}
$("#sg_recipebundle_recipetype_tags").autocomplete({
source: function( request, response ) {
$.getJSON( "{{ path('tag_tags') }}", {
term: extractLast( request.term )
}, response );
},
search: function() {
// custom minLength
var term = extractLast( this.value );
if ( term.length < 2 ) {
return false;
}
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function( event, ui ) {
var terms = split( this.value );
// remove the current input
terms.pop();
// add the selected item
terms.push( ui.item.value );
// add placeholder to get the comma-and-space at the end
terms.push( "" );
this.value = terms.join( ", " );
return false;
}
});
});
</script>
I took a slightly different approach using Select2's tag input:
It has the advantage that it prevents duplicates on the client side and looks pretty.
To create the newly added entities, I am using a EventSubscriber rather than a DataTransformer.
For a few more details, see my gist. Below are the TagType and the AddEntityChoiceSubscriber.
AppBundle/Form/Type/TagType:
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\Form\EventListener\AddEntityChoiceSubscriber;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class TagType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$subscriber = new AddEntityChoiceSubscriber($options['em'], $options['class']);
$builder->addEventSubscriber($subscriber);
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return EntityType::class;
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'tag';
}
}
AppBundle/Form/EventListener/AddEntityChoiceSubscriber:
<?php
namespace TriprHqBundle\Form\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
class AddEntityChoiceSubscriber implements EventSubscriberInterface
{
/**
* #var EntityManager
*/
protected $em;
/**
* The name of the entity
*
* #var string
*/
protected $entityName;
public function __construct(EntityManager $em, string $entityName)
{
$this->em = $em;
$this->entityName = $entityName;
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'preSubmit',
];
}
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
$data = [];
}
// loop through all values
$repository = $this->em->getRepository($this->entityName);
$choices = array_map('strval', $repository->findAll());
$className = $repository->getClassName();
$newChoices = [];
foreach($data as $key => $choice) {
// if it's numeric we consider it the primary key of an existing choice
if(is_numeric($choice) || in_array($choice, $choices)) {
continue;
}
$entity = new $className($choice);
$newChoices[] = $entity;
$this->em->persist($entity);
}
$this->em->flush();
// now we need to replace the text values with their new primary key
// otherwise, the newly added choice won't be marked as selected
foreach($newChoices as $newChoice) {
$key = array_search($newChoice->__toString(), $data);
$data[$key] = $newChoice->getId();
}
$event->setData($data);
}
}