I've created an event subscriber that sends an email with Swiftmailer every time certain entity is updated but the event is triggered twice so i'm receiving email two times.
I've followed this guide and this one. The event is: easy_admin.pre_update
EasyAdminBundle version: 1.17.9
services.yml
app.easy_admin.aprobar_publicacion:
class: AppBundle\EventListener\AprobarPublicacionSubscriber
arguments: ['#mailer','#service_container','#twig']
tags:
- { name: kernel.event_subscriber, event: easy_admin.pre_update, method: preUpdate }
AprobarPublicacionSubscriber.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use AppBundle\Entity\VestidoDeNovia;
use AppBundle\Entity\PlantillaEmail;
class AprobarPublicacionSubscriber implements EventSubscriberInterface {
protected $mailer;
protected $container;
protected $twig;
public static function getSubscribedEvents() {
return array(
'easy_admin.pre_update' => array('preUpdate'),
);
}
public function __construct(\Swift_Mailer $mailer, \Symfony\Component\DependencyInjection\ContainerInterface $container, \Twig_Environment $twig) {
$this->mailer = $mailer;
$this->container = $container;
$this->twig = $twig;
}
public function preUpdate(GenericEvent $event) {
$entity = $event->getSubject();
if (!($entity instanceof VestidoDeNovia)) {
return;
}
$uow = $event->getArgument('em')->getUnitOfWork();
$uow->computeChangeSets();
$changeset = $uow->getEntityChangeSet($entity);
if (array_key_exists('estado', $changeset)) {
$estadoActual = $changeset['estado'][0]->getId();
if ($estadoActual != 2 and $entity->getEstado()->getId() == 2) {
$plantillaEmail = new PlantillaEmail();
$plantillaEmail = $event->getArgument('em')->getRepository(PlantillaEmail::class)->find(6);
$message = (new \Swift_Message($plantillaEmail->getAsunto()))
->setFrom($this->container->getParameter('direccion_mail_sistema'))
->setTo($entity->getUsuario()->getEmail())
->setBody(
$this->twig->render(
'AppBundle::Emails/publicacion_exitosa.html.twig', array(
'titulo' => $plantillaEmail->getAsunto(),
'mensaje' => $plantillaEmail->getMensaje(),
)
), 'text/html'
)
;
$this->mailer->send($message);
}
}
}
}
What am i missing?
Related
as soon as I call up the form builder into my unit test I get the following exception:
class EventFormBuilderTest extends TypeTestCase
{
public function testSuccessfullyCreatesForm(): void
{
$formBuilder = new EventFormBuilder($this->factory);
$user = new User();
$actualEvent = new Event();
$form = $formBuilder->buildFormBasedOnUserRole($user, $actualEvent);
I injected a repository into my form to have default options:
class EventType extends AbstractType
{
/** #var LegalServiceRepository */
private $legalServiceRepository;
public function __construct(LegalServiceRepository $legalServiceRepository)
{
$this->legalServiceRepository = $legalServiceRepository;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['services']);
$resolver->setAllowedTypes('services', 'App\Entity\LegalService[]');
$resolver->setDefaults([
'data_class' => Event::class,
'services' => $this->legalServiceRepository->findAllOfTypeEvent(),
'error_mapping' => [
'.' => 'court',
],
]);
}
Then I wrote a custom form builder that allows me to create a different form based on the user's role. The GlobalAdminEventType form extends EventType. I simply needed an extra field.
This works perfectly like expected and i get my options without any error.
class EventFormBuilder
{
/** #var FormFactoryInterface */
private $formFactory;
public function __construct(
FormFactoryInterface $formFactory
) {
$this->formFactory = $formFactory;
}
public function buildFormBasedOnUserRole(User $user, Event $event): FormInterface
{
if (\in_array(RolesEnum::ROLE_SUPER_ADMIN, $user->getRoles(), true)) {
$form = $this->buildForm(GlobalAdminEventType::class, $event);
} else {
$event->setCourt($user->getCourt());
$form = $this->buildForm(EventType::class, $event);
}
return $form;
}
public function buildForm(string $type, Event $event): FormInterface
{
return $this->formFactory->create($type, $event);
}
}
Admin generate user then user first time login i want to redirect user to change password.
I have google but there are no any post found that make this fix.
Below i have attached service.yml and loginsuccessListener file code.
service.yml
services:
fos_user.security.interactive_login_listener:
class: OnlineCare\MarketingWebBundle\EventListener\LastLoginListener
tags:
- { name: kernel.event_subscriber, event: kernel.request, method: onKernelRequest }
arguments: ['#fos_user.user_manager','#router','#service_container']
LastLoginListiner.php
namespace OnlineCare\MarketingWebBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\UserEvent;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class LastLoginListener implements EventSubscriberInterface
{
protected $userManager;
private $router;
private $container;
public function __construct(UserManagerInterface $userManager, UrlGeneratorInterface $router, ContainerInterface $container)
{
$this->userManager = $userManager;
$this->router = $router;
$this->container = $container;
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::SECURITY_IMPLICIT_LOGIN => 'onImplicitLogin',
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
);
}
public function onImplicitLogin(UserEvent $event)
{
$user = $event->getUser();
$user->setLastLogin(new \DateTime());
$this->userManager->updateUser($user);
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if(empty($user->getLastLogin())){
$url = $this->router->generate('fos_user_change_password');
**return $event->setResponse(new RedirectResponse($url));**
} elseif ($user instanceof UserInterface) {
$user->setLastLogin(new \DateTime());
$this->userManager->updateUser($user);
}
}
}
I make a listener for exception handling. Below is my code
services.yml
kernel.listener.prod_exception_listener:
class: MyBundle\Listener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
ExceptionListener.php
<?php
namespace MyBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// no fatal exception goes here others are coming in this function
// like 403,404,500 are coming in this block
}
}
What additional work I need to do for fatal exceptions in production mode? Because in dev mode fatal errors are coming in listener.
I solved it the following way,
in my services.yml
api_exception_subscriber:
class: AppBundle\EventListener\ApiExceptionSubscriber
arguments: ['%kernel.debug%', '#api.response_factory', '#logger']
tags:
- { name: kernel.event_subscriber }
api.response_factory:
class: AppBundle\Api\ResponseFactory
my response factory look like:
<?php
namespace AppBundle\Api;
use Symfony\Component\HttpFoundation\JsonResponse;
class ResponseFactory
{
public function createResponse(ApiProblem $apiProblem)
{
$data = $apiProblem->toArray();
$response = new JsonResponse(
$data,
$apiProblem->getStatusCode()
);
$response->headers->set('Content-Type', 'application/json');
return $response;
}
}
and the Api subscriper class
<?php
namespace AppBundle\EventListener;
use AppBundle\Api\ApiProblem;
use AppBundle\Api\ApiProblemException;
use AppBundle\Api\ResponseFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class ApiExceptionSubscriber implements EventSubscriberInterface
{
private $debug;
private $responseFactory;
private $logger;
public function __construct($debug, ResponseFactory $responseFactory, LoggerInterface $logger)
{
$this->debug = $debug;
$this->responseFactory = $responseFactory;
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
// only reply to /api URLs
if (strpos($event->getRequest()->getPathInfo(), '/api') !== 0) {
return;
}
$e = $event->getException();
$statusCode = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
// allow 500 errors to be thrown
if ($this->debug && $statusCode >= 500) {
return;
}
$this->logException($e);
if ($e instanceof ApiProblemException) {
$apiProblem = $e->getApiProblem();
} else {
$apiProblem = new ApiProblem(
$statusCode
);
/*
* If it's an HttpException message (e.g. for 404, 403),
* we'll say as a rule that the exception message is safe
* for the client. Otherwise, it could be some sensitive
* low-level exception, which should *not* be exposed
*/
if ($e instanceof HttpExceptionInterface) {
$apiProblem->set('detail', $e->getMessage());
}
}
$response = $this->responseFactory->createResponse($apiProblem);
$event->setResponse($response);
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::EXCEPTION => 'onKernelException'
);
}
/**
* Adapted from the core Symfony exception handling in ExceptionListener
*
* #param \Exception $exception
*/
private function logException(\Exception $exception)
{
$message = sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine());
$isCritical = !$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500;
$context = array('exception' => $exception);
if ($isCritical) {
$this->logger->critical($message, $context);
} else {
$this->logger->error($message, $context);
}
}
}
Edit 2020: as of Symfony 5 this is no longer necessary
I have handled this by overriding Kernel::handle to call the ExceptionListener manually
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true): Response
{
try {
return parent::handle($request, $type, $catch);
} catch (\Exception $exception) {
throw new \Exception("There was an issue booting the framework");
} catch (\Throwable $throwable) {
$exception = new FatalThrowableError($throwable);
$event = new ExceptionEvent($this, $request, $type, $exception);
/** #var ExceptionListener $exceptionListener */
$exceptionListener = $this->container->get(ExceptionListener::class);
$exceptionListener->onKernelException($event);
return $event->getResponse();
}
}
How to render a template outside a controller or in a service?
I've been following the documentation of Symfony2. Doc
namespace Acme\HelloBundle\Newsletter;
use Symfony\Component\Templating\EngineInterface;
class NewsletterManager
{
protected $mailer;
protected $templating;
public function __construct(
\Swift_Mailer $mailer,
EngineInterface $templating
) {
$this->mailer = $mailer;
$this->templating = $templating;
}
// ...
}
This is where i call my helper :
$transport = \Swift_MailTransport::newInstance();
$mailer = \Swift_Mailer::newInstance($transport);
$helper = new MailHelper($mailer);
$helper->sendEmail($from, $to, $subject, $path_to_twig, $arr_to_twig);
So the first thing here missing is the second parameter of the construct method in :
$helper = new MailHelper($mailer);
But how would i instantiate the EngineInterface?
Of course it can't be :
new EngineInterface();
I'm totally lost here.
All what i need to do is render a template for the email that is being sent.
Inject only #twig and pass the rendered template to the mailer body:
<?php
namespace Acme\Bundle\ContractBundle\Event;
use Acme\Bundle\ContractBundle\Event\ContractEvent;
class ContractListener
{
protected $twig;
protected $mailer;
public function __construct(\Twig_Environment $twig, \Swift_Mailer $mailer)
{
$this->twig = $twig;
$this->mailer = $mailer;
}
public function onContractCreated(ContractEvent $event)
{
$contract = $event->getContract();
$body = $this->renderTemplate($contract);
$projectManager = $contract->getProjectManager();
$message = \Swift_Message::newInstance()
->setSubject('Contract ' . $contract->getId() . ' created')
->setFrom('noreply#example.com')
->setTo('dev#example.com')
->setBody($body)
;
$this->mailer->send($message);
}
public function renderTemplate($contract)
{
return $this->twig->render(
'AcmeContractBundle:Contract:mailer.html.twig',
array(
'contract' => $contract
)
);
}
}
I have a method in 'DynamicList' service that should return a select filled with dynamic data, but i'm getting "circular reference":
YML:
parameters:
my.dynamic_list.class: My\DynamicListBundle\Service\DynamicList
services:
my.dynamic_list:
class: %my.dynamic_list.class%
arguments: ['#doctrine.orm.default_entity_manager','#templating']
Class:
<?php
namespace My\DynamicListBundle\Service;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
class DynamicList
{
private $em;
private $templating;
public function __construct(
EntityManager $em,
EngineInterface $templating
) {
$this->em = $em;
$this->templating = $templating;
}
public function getSelect($slug)
{
$dynamic_list = $this->em
->getRepository('MyDynamicListBundle:DynamicList')
->findOneBy(array(
"slug" => $slug
));
return $this->templating->render('MyComponentsCoreBundle::Templates/DynamicList/combo.html.twig', array(
'dl' => $dynamic_list
));
}
}
I guess i don't need to put here the twig content: the problema occurs before.
Last, the error i'm getting:
Circular reference detected for service "my.dynamic_list", path: "my.dynamic_list -> templating -> twig". (500 Internal Server Error - ServiceCircularReferenceException)
What's the proper way to get templating component working in my service?
Well, I found a workaround but I don't if is the best way:
<?php
namespace My\DynamicListBundle\Service;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
class DynamicList
{
private $em;
private $templating;
public function __construct(
EntityManager $em
) {
$this->em = $em;
}
public function init(
EngineInterface $templating
) {
$this->templating = $templating;
}
public function getSelect($slug)
{
$dynamic_list = $this->em
->getRepository('MyDynamicListBundle:DynamicList')
->findOneBy(array(
"slug" => $slug
));
return $this->templating->render('MyComponentsCoreBundle::Templates/DynamicList/combo.html.twig', array(
'dl' => $dynamic_list
));
}
}
So, in controller, I call 'init()' to pass 'templating':
$dl_service = $this->get('my.dynamic_list');
$dl_service->init($this->container->get('templating'));