I would like to know if it is possible to disable the subscriber inside a command (only during this command) with Symfony.
I need to update a database but, each time an entity is updated, it will trigger the subscriber and take to much time.
The subscriber is useful for a small amount of modification, but not when the entire database is updated.
I use prePersist, preUpdate, postUpdate and postPersist. There is some example :
<?php
namespace App\EventSubscriber\ElasticSearch;
use App\Entity\Product;
use App\Entity\ProductTranslation;
use App\Service\ElasticSearch\ElasticSearchProductService;
use Cocur\Slugify\Slugify;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;
class ProductSubscriber implements EventSubscriber
{
private ElasticSearchProductService $elasticSearchProductService;
public function __construct(ElasticSearchProductService $elasticSearchProductService)
{
$this->elasticSearchProductService = $elasticSearchProductService;
}
/**
* #return array
*/
public function getSubscribedEvents(): array
{
return [
Events::prePersist,
Events::preUpdate,
Events::postPersist,
Events::postUpdate,
];
}
/**
* #param LifecycleEventArgs $args
*/
public function prePersist(LifecycleEventArgs $args): void
{
$product = $args->getObject();
if (!$product instanceof Product) {
return;
}
$this->slugProduct($product);
}
/**
* #param LifecycleEventArgs $args
*/
public function preUpdate(LifecycleEventArgs $args): void
{
$product = $args->getObject();
if (!$product instanceof Product) {
return;
}
$this->slugProduct($product);
}
/**
* #param LifecycleEventArgs $args
*/
public function postUpdate(LifecycleEventArgs $args): void
{
$this->initializeElasticSearch($args);
}
/**
* #param LifecycleEventArgs $args
*/
public function postPersist(LifecycleEventArgs $args): void
{
$this->initializeElasticSearch($args);
}
/**
* #param Product $product
*/
private function slugProduct(Product $product): void
{
$product->setSlug((new Slugify())->slugify($product->getTranslations()['fr']->getName() . '-' . $product->getEan() . '-' . $product->getPickRef()));
}
/**
* #param LifecycleEventArgs $args
*/
private function initializeElasticSearch(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!($entity instanceof Product || $entity instanceof ProductTranslation)) {
return;
}
if ($entity instanceof ProductTranslation) {
$entity = $args->getObject()->getTranslatable();
}
//$this->elasticSearchProductService->initializeDocumentElasticSearch($entity);
}
}
EDIT : I have updated the example. This is the full subscriber. I update a Brand with for example :
$this->em->persist($brand);
$this->em->flush();
It will activate the subscriber who will update ElasticSearch. But, in one Symfony Command that I made,I have to update 40 000 brands. I would like to update elasticSearch at the end of the command and not update one by one with the subscriber. So I would like to disable (for only this Command) the subscriber.
This is a Doctrine EventSubscriber and I tried
$listeners = $this->em->getEventManager()->getListeners("productSubscriber");
foreach ($listeners as $key => $listener) {
if ($listener instanceof ProductSubscriber) {
$this->em->getEventManager()->removeEventListener($listener->getSubscribedEvents(), $listener);
}
}
But the subscriber is not removed.
Related
I'm trying to create a listener to when a new Rating is created. I followed all the documentation but I keep getting the same error:
Argument 1 passed to "Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch()" must be an instance of "Symfony\Component\EventDispatcher\Event", "App\Event\AverageRatingEvent" given.
I'm trying to use Symfony\Component\EventDispatcher\Event in the event but it keeps saying that it is deprecated and according to documents to use Symfony\Contracts\EventDispatcher\Event instead.
I register my event in the services and the following is my event, eventlistener and class
Class Rating
class RatingApiController extends AbstractController
{
/**
* #Route("api/rating/create", name="CreateRating", methods={"POST"})
* #param DocumentManager $dm
* #param Request $request
* #param EventDispatcher $eventDispatcher
* #return RedirectResponse|Response
* #throws MongoDBException
*
*/
public function addRating(Request $request, EventDispatcherInterface $eventDispatcher)
{
$response = [];
$form = $this->
createForm(RatingType::class, new Rating() ,array('csrf_protection' => false));
$request = json_decode($request->getContent(), true);
$form->submit($request);
if($form->isSubmitted() && $form->isValid())
{
$rating = $form->getData();
$this->documentManager->persist($rating);
$this->documentManager->flush();
$averageRatingEvent = new AverageRatingEvent($rating);
$eventDispatcher->dispatch( AverageRatingEvent::NAME, $averageRatingEvent);
$status = 200;
$response = ["status" => $status, "success" => true, "data" => $rating->getId()];
// return $this->redirectToRoute('rating_list');
}
}
Event
<?php
namespace App\Event;
use App\Document\Rating;
use Symfony\Contracts\EventDispatcher\Event;
class AverageRatingEvent extends Event
{
/**
* #var Rating $rating
*/
protected $rating;
public const NAME = "average.rating";
public function __construct(Rating $rating)
{
$this->rating = $rating;
}
public function getRating()
{
return $this->rating;
}
}
Listener
<?php
namespace App\Event;
use App\Document\Rating;
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
class AverageRatingListener
{
public function postPersist(LifecycleEventArgs $args)
{
$document = $args->getObject();
if(!$document instanceof Rating)
return;
}
public function RatingCreated()
{
dump("Hello a rating was just added");
}
}
Inside AverageRatingEvent you extend Event.
The use needs to be changed from
use Symfony\Contracts\EventDispatcher\Event;
to
use Symfony\Component\EventDispatcher\Event;
I code a simple app (Symfony 4.1.7) with a user and product system
A user can edit his product, but not another user's
My problem, I go on the edit route, it return access denied, even when it's my product
My ProductController :
/**
* #Route("seller/myproduct/{id}/edit", name="seller_edit_product")
* #param Product $product
* #return Response
* #Security("product.isAuthor(user)")
*/
public function edit(Product $product, Request $request): Response
{
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()){
$this->em->flush();
$this->addFlash('success', 'Modify Successfully');
return $this->redirectToRoute('seller_index_product');
}
return $this->render('seller/product/edit.html.twig', [
'product' => $product,
'form' => $form->createView()
]);
}
Product.php
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="product_id")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
public function getUser(): User
{
return $this->user;
}
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
/**
* #return bool
*/
public function isAuthor(User $user = null)
{
return $user && $user->getProductId() === $this->getUser();
}
In my isAuhor function
== Access Denied
!== I can access the edition of product that Is not mine
User.php
/**
* #ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="user",orphanRemoval=true)
*/
private $product_id;
public function __construct()
{
$this->product_id = new ArrayCollection();
}
/**
* #return Collection|Product[]
*/
public function getProductId(): Collection
{
return $this->product_id;
}
public function addProductId(Product $productId): self
{
if (!$this->product_id->contains($productId)) {
$this->product_id[] = $productId;
$productId->setUser($this);
}
return $this;
}
}
Thank you
Your isAuthor function will always return false as you are comparing an ArrayCollection to a User
You could add a function in User Class definition that checks if a given user have a given product or no.
So in Product.php :
/**
* #return bool
*/
public function isAuthor(User $user = null)
{
return $user && $user->hasProduct($this);
}
And the hasProduction function could be something like this:
// this goes into User.php
/**
* #return bool
*/
public function hasProduct(Product $product)
{
return $this->product_id->contains($product)
}
I need to migrate a really old version of Knp Menu to a newest one. The real problem is here
$collapse = new CollapseItem($group,$router->generate('seguridad_group_list'),array('class'=>'submenu'),'Primicia\SeguridadBundle\Menu\CollapseItem');
$collapse->setIcon('sp sp-ico-menu-grupo sp-icon-display');
$this->addChild($collapse);
How can I make it following the menu-as-service-way in the version 2 of KnpMenu?
The rest of code is this
The menu service implementation:
namespace Primicia\SeguridadBundle\Menu;
use Knp\Bundle\MenuBundle\Menu;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Router;
use Knp\Bundle\MenuBundle\Renderer\RendererInterface;
class SeguridadMenu extends Menu {
/**
* #param Request $request
* #param Router $router
* #param Container $container
*/
public function __construct(Request $request, Router $router, $container)
{
parent::__construct();
$this->setCurrentUri($request->getRequestUri());
$this->setAttribute('class','nav nav-list menu_lateral');
$translator = $container->get('translator');
if($container->get('security.context')->isGranted('ROLE_ADMIN'))
{
$user = $translator->trans('menu.user.titles',array(),'SeguridadBundle');
$signal = new CollapseItem($user,$router->generate('seguridad_user_list'),array('class'=>'submenu'),'Primicia\SeguridadBundle\Menu\CollapseItem');
$signal->setIcon('sp sp-ico-menu-usuario sp-icon-display');
$this->addChild($signal);
$group = $translator->trans('menu.group.titles',array(),'SeguridadBundle');
$collapse = new CollapseItem($group,$router->generate('seguridad_group_list'),array('class'=>'submenu'),'Primicia\SeguridadBundle\Menu\CollapseItem');
$collapse->setIcon('sp sp-ico-menu-grupo sp-icon-display');
$this->addChild($collapse);
}
}
/**
* Gets renderer which is used to render menu items.
*
* #return RendererInterface $renderer Renderer.
*/
public function getRenderer()
{
if(null === $this->renderer) {
if($this->isRoot()) {
$this->setRenderer(new ApcRenderer());
}
else {
return $this->getParent()->getRenderer();
}
}
return $this->renderer;
}
}
The CollapseItem class, which is used in the $router->generate
namespace Primicia\SeguridadBundle\Menu;
use Knp\Menu\MenuItem;
class CollapseItem extends MenuItem
{
protected $hasIcon;
public function renderLink()
{
$label = $this->renderLabel();
$uri = $this->getUri();
if (!$uri) {
die;
return sprintf('<a class="dropdown-toggle" href="#">%s</a>', $label);
}
return sprintf('<a class="dropdown-toggle" href="%s">%s</a>', $uri, $label);
}
public function setIcon($icon)
{
$this->hasIcon=$icon;
return $this;
}
public function getIcon()
{
return $this->hasIcon;
}
}
I have a Doctrine entity and I use JMS serializer to render it in my API.
I'd like to add a boolean field like this :
/**
* #var bool
*
* #ORM\Column(name = "is_serialized", type = "boolean")
*/
protected $isSerialized = true;
I also use an EventSubscriber to add some data to my entity before serialization.
I'd like to dynamically include or not each entity, based on the $isSerialized value (I can't modify the Doctrine Query).
class SerializationEventSubscriber extends EventSubscriberInterface
{
/**
* #param ObjectEvent $event
*/
public function onPostSerialize(ObjectEvent $event)
{
if (!$this->isGroup('api', $event)) {
return;
}
$entity = $event->getObject();
$visitor = $event->getVisitor();
if (!$object->isSerialized()) {
// Skip the current object and remove it from serialization
}
}
}
I can't find any information about this, neither in the JMS annotation documentation.
Here is my EventListener, but instead of removing the object I just skip nulled field.
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\Metadata\ClassMetadata as JMSClassMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
class EntitySerializerListener
{
/**
* #var EntityManagerInterface
*/
protected $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
public function onPostSerialize(ObjectEvent $event)
{
/** #var JsonSerializationVisitor $visitor */
$object = $event->getObject();
$visitor = $event->getVisitor();
$context = $event->getContext();
$type = $event->getType();
/** #var JMSClassMetadata $metadata */
$metadata = $context->getMetadataFactory()->getMetadataForClass($type['name']);
$data = $visitor->endVisitingObject($metadata, $object, $type);
$visitor->startVisitingObject($metadata, $object, $type);
// Here I remove unnecessary fields
$this->prune($type['name'], $data);
// Reset fresh serialized data
foreach ($data as $field => $value) {
$visitor->visitProperty(new StaticPropertyMetadata($type['name'], $field, $value), $value);
}
}
/**
* Prune the empty field which was set to NULL by MaxDepth annotation but left in the data graph by JWT serializer.
*
* #param string $fqcn
* #param array $data
*/
protected function prune(string $fqcn, array & $data)
{
/** #var ClassMetadata $metadata */
$metadata = $this->em->getMetadataFactory()->getMetadataFor($fqcn);
// Handle association
$associations = $metadata->getAssociationMappings();
foreach ($associations as $field => $association) {
if (!array_key_exists($field, $data)) {
continue;
}
// Here remove entity or any other field which you want
if (empty($data[$field])) {
unset($data[$field]);
} else {
$this->prune($association['targetEntity'], $data[$field]);
}
}
}
}
I have a postUpdate listener and I'd like to know what the values were prior to the update and what the values for the DB entry were after the update. Is there a way to do this in Symfony 2.1? I've looked at what's stored in getUnitOfWork() but it's empty since the update has already taken place.
You can use this ansfer Symfony2 - Doctrine - no changeset in post update
/**
* #param LifecycleEventArgs $args
*/
public function postUpdate(LifecycleEventArgs $args)
{
$changeArray = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($args->getObject());
}
Found the solution here. What I needed was actually part of preUpdate(). I needed to call getEntityChangeSet() on the LifecycleEventArgs.
My code:
public function preUpdate(Event\LifecycleEventArgs $eventArgs)
{
$changeArray = $eventArgs->getEntityChangeSet();
//do stuff with the change array
}
Your Entitiy:
/**
* Order
*
* #ORM\Table(name="order")
* #ORM\Entity()
* #ORM\EntityListeners(
* {"\EventListeners\OrderListener"}
* )
*/
class Order
{
...
Your listener:
class OrderListener
{
protected $needsFlush = false;
protected $fields = false;
public function preUpdate($entity, LifecycleEventArgs $eventArgs)
{
if (!$this->isCorrectObject($entity)) {
return null;
}
return $this->setFields($entity, $eventArgs);
}
public function postUpdate($entity, LifecycleEventArgs $eventArgs)
{
if (!$this->isCorrectObject($entity)) {
return null;
}
foreach ($this->fields as $field => $detail) {
echo $field. ' was ' . $detail[0]
. ' and is now ' . $detail[1];
//this is where you would save something
}
$eventArgs->getEntityManager()->flush();
return true;
}
public function setFields($entity, LifecycleEventArgs $eventArgs)
{
$this->fields = array_diff_key(
$eventArgs->getEntityChangeSet(),
[ 'modified'=>0 ]
);
return true;
}
public function isCorrectObject($entity)
{
return $entity instanceof Order;
}
}
You can find example in doctrine documentation.
class NeverAliceOnlyBobListener
{
public function preUpdate(PreUpdateEventArgs $eventArgs)
{
if ($eventArgs->getEntity() instanceof User) {
if ($eventArgs->hasChangedField('name') && $eventArgs->getNewValue('name') == 'Alice') {
$eventArgs->setNewValue('name', 'Bob');
}
}
}
}