I have a service who intercepts the events of Sentry. I'm using a function called beforeSend.
I would to load a json file who contains the data to scrub or to keep. It's a service and I build my constructor with a similar way than others, but the "$this" context doesn't exist when I'm in the debugger in this function.
The kernel is in the Global variables, but I think it's not a good idea... I only would to get the root dir and it's all, but I don't find how to do this in this class... The constructor seems useless.
Someone could help me with a similar experience ?
EDIT :
Service :
namespace App\Services;
use Sentry\Event;
use Symfony\Component\HttpKernel\KernelInterface;
class SentryBeforeSendService
{
private static $rootDir;
public function __construct(KernelInterface $kernel)
{
self::$rootDir = $kernel->getRootDir();
}
/**
* Scrubs the value of all TARGET_PARAMETERS
* in the event's request.
*
* #param Event $event
*
* #return Event
*/
public function beforeSend(Event $event)
{
$rootDir = self::$rootDir;
$event->setRequest(self::scrubRequest($event->getRequest(), $rootDir));
try {
$composerData = json_decode(file_get_contents($rootDir.'/../composer.json'), true);
$version = $composerData['version'];
$event->setRelease($version);
} catch (\Exception $e) {
//do nothing
}
return $event;
}
/**
* Scrubs GET and POST parameters
*
* #param array $request
*
* #return array
*/
private static function scrubRequest(array $request, $rootDir)
{
// DO SOMETHING WITH $rootDir to scrub data with external file
}}
services.yml :
app.service.sentry_before_send:
class: 'App\Services\SentryBeforeSendService'
arguments: ['#kernel']
config_prod.yml :
sentry:
dsn: "%sentry_dsn%"
options:
environment: "%sentry_environment%"
# release: '%env(VERSION)%' #overridden from composer.json version in SentryBeforeSendService::beforeSend
before_send: 'App\Services\SentryBeforeSendService::beforeSend'
But it seems the construct never happened.
Thank you very much.
I was unable to inject a parameter, but I found a way to get the project_root from my method. Half victory ...
config_prod.yml:
sentry:
dsn: "%sentry_dsn%"
options:
environment: "%sentry_environment%"
# release: '%env(VERSION)%' #overridden from composer.json version in SentryBeforeSendService::beforeSend
before_send: 'App\Services\SentryBeforeSendService::beforeSend'
project_root: '%kernel.project_dir%'
Service :
<?php
namespace App\Services;
use Sentry\Event;
use Sentry\State\Hub;
class SentryBeforeSendService
{
private static $projectRoot;
/**
* Scrubs the value of all TARGET_PARAMETERS
* in the event's request.
*
* #param Event $event
*
* #return Event
*/
public function beforeSend(Event $event)
{
$sentryClient = Hub::getCurrent()->getClient();
self::$projectRoot = $sentryClient->getOptions()->getProjectRoot();
$event->setRequest(self::scrubRequest($event->getRequest()));
try {
$composerData = json_decode(file_get_contents(self::$projectRoot.'/composer.json'), true);
$version = $composerData['version'];
$event->setRelease($version);
} catch (\Exception $e) {
//do nothing
}
return $event;
}}
Hope it'll help someone else.
Thank you for answers.
You can inject the kernel.project_dir parameter in your service constructor with a named parameter:
In your services.yml file:
services:
_defaults:
bind:
string $kernelProjectDir: '%kernel.project_dir%'
Then in your service:
public function __construct(string $kernelProjectDir)
{
Related
i am building an Api with symfony 4.2 and want to use jms-serializer to serialize my data in Json format, after installing it with
composer require jms/serializer-bundle
and when i try to use it this way :
``` demands = $demandRepo->findAll();
return $this->container->get('serializer')->serialize($demands,'json');```
it gives me this errur :
Service "serializer" not found, the container inside "App\Controller\DemandController" is a smaller service locator that only knows about the "doctrine", "http_kernel", "parameter_bag", "request_stack", "router" and "session" services. Try using dependency injection instead.
Finally i found the answer using the Symfony serializer
it's very easy:
first : istall symfony serialzer using the command:
composer require symfony/serializer
second : using the serializerInterface:
.....//
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
// .....
.... //
/**
* #Route("/demand", name="demand")
*/
public function index(SerializerInterface $serializer)
{
$demands = $this->getDoctrine()
->getRepository(Demand::class)
->findAll();
if($demands){
return new JsonResponse(
$serializer->serialize($demands, 'json'),
200,
[],
true
);
}else{
return '["message":"ooooops"]';
}
}
//......
and with it, i don't find any problems with dependencies or DateTime or other problems ;)
As I said in my comment, you could use the default serializer of Symfony and use it injecting it by the constructor.
//...
use Symfony\Component\Serializer\SerializerInterface;
//...
class whatever
{
private $serializer;
public function __constructor(SerializerInterface $serialzer)
{
$this->serializer = $serializer;
}
public function exampleFunction()
{
//...
$data = $this->serializer->serialize($demands, "json");
//...
}
}
Let's say that you have an entity called Foo.php that has id, name and description
And you would like to return only id, and name when consuming a particular API such as foo/summary/ in another situation need to return description as well foo/details
here's serializer is really helpful.
use JMS\Serializer\Annotation as Serializer;
/*
* #Serializer\ExclusionPolicy("all")
*/
class Foo {
/**
* #Serializer\Groups({"summary", "details"})
* #Serializer\Expose()
*/
private $id;
/**
* #Serializer\Groups({"summary"})
* #Serializer\Expose()
*/
private $title;
/**
* #Serializer\Groups({"details"})
* #Serializer\Expose()
*/
private $description;
}
let's use serializer to get data depends on the group
class FooController {
public function summary(Foo $foo, SerializerInterface $serialzer)
{
$context = SerializationContext::create()->setGroups('summary');
$data = $serialzer->serialize($foo, json, $context);
return new JsonResponse($data);
}
public function details(Foo $foo, SerializerInterface $serialzer)
{
$context = SerializationContext::create()->setGroups('details');
$data = $serialzer->serialize($foo, json, $context);
return new JsonResponse($data);
}
}
It's possibile with security file config to redirect user already logged in to specific route (e.g homepage) if they try to access on login/register pages? One solution that I already found is to attach a listener to EventRequest, but I prefer to use security config if it's possible.
After some googling I noticed that another solution is to override the fosuserbundle controllers. But because I need that this behavior should works also for /register and /resetting pages, instead to override also those controller, I preferred to use EventListener. Maybe this's the best solution in this case. I'm using Symfony 4, so for the other versions could be different.
My code:
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class LoggedInUserListener
{
private $router;
private $authChecker;
public function __construct(RouterInterface $router, AuthorizationCheckerInterface $authChecker)
{
$this->router = $router;
$this->authChecker = $authChecker;
}
/**
* Redirect user to homepage if tryes to access in anonymously path
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if ($this->authChecker->isGranted('IS_AUTHENTICATED_REMEMBERED') && $this->isAnonymouslyPath($path)) {
$response = new RedirectResponse($this->router->generate('homepage'));
$event->setResponse($response);
}
}
/**
* Check if $path is an anonymously path
* #param string $path
* #return bool
*/
private function isAnonymouslyPath(string $path): bool
{
return preg_match('/\/login|\/register|\/resetting/', $path) ? true : false;
}
}
add this to services.yaml:
App\EventListener\LoggedInUserListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
#Mintendo, I have errors using your code:
request.CRITICAL: Exception thrown when handling an exception (Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException: The token storage contains no authentication token.
php.CRITICAL: Uncaught Exception: The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.
Besides that debug bar also showed error and was broken.
But you pushed me in the right direction, so I have modified your code a little.
And it works without errors now:
<?php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security;
class LoggedInUserListener
{
private $router;
private $security;
public function __construct(RouterInterface $router, Security $security)
{
$this->router = $router;
$this->security = $security;
}
/**
* Redirect user to homepage if tries to access in anonymously path
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if ($this->security->getUser() && $this->isAnonymouslyPath($path)) {
$response = new RedirectResponse($this->router->generate('dashboard'));
$event->setResponse($response);
}
}
/**
* Check if $path is an anonymously path
* #param string $path
* #return bool
*/
private function isAnonymouslyPath(string $path): bool
{
return preg_match('/\/login|\/register|\/resetting/', $path) ? true : false;
}
}
I have a problem with Events in Symfony. I do not understand how way it works. This is my Listener:
class ClientVisitedListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return
[
KernelEvents::REQUEST => 'sprawdz',
];
}
My service.yml
anderos_invoice.invoice_club_listener:
class: Anderos\AcpPriceBundle\EventListener\InvoiceClubListener
arguments: [#service_container]
tags:
- { name: kernel.event_subscriber }
In all system, I have not any dispatcher. How does it work?
Where is the start of this procedure? Maybe in Kernel?
Could you help me to understand that procedure?
This is the key to understand what's happening here:
tags:
- { name: kernel.event_subscriber }
When the container is being compiled, it uses compiler passes. Compiler pass is an object which, at the time of compilation, gets ContainerBuilder as an argument and can do something with it. For example iterate over all services, check if they have a tag (kernel.event_subscriber in this case) and if so, do something with it.
In this case there is such compiler pass which takes all services having kernel.event_subscriber tag and adds them into EventDispatcher, which already exists in Symfony core (so yes, you have an event dispatcher, although you may not know about it).
That's how it knows which services need to be called when an event occurs - when it happens, the EventDispatcher instance already has registered all listeners/subscribers and simply call them.
When an event happens, then listener that is subscribed to this event will execute some code. Here is how I implemented it.
my service.yml:
app.listener.bot.logger:
class: AppBundle\Listener\BotLoggerListener
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: bot }
- { name: kernel.event_listener, event: bot.log.message, method: 'onBotMessage' }
in my controller:
$event = new BotLogMessage('Request finish ');
$this->get('event_dispatcher')->dispatch($event::NAME, $event);
the listener:
namespace AppBundle\Listener;
use AppBundle\Event\BotLogRequestEvent;
use AppBundle\Event\BotLogResponseEvent;
use AppBundle\Event\BotLogMessage;
use Psr\Log\LoggerInterface;
class BotLoggerListener
{
private $logger;
/**
* BotLoggerListener constructor.
* #param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* #param BotLogMessage $event
*/
public function onBotMessage(BotLogMessage $event)
{
$this->logger->info('[Log Message] : ' . $event->getMessage());
}
}
the event class:
namespace AppBundle\Event;
use AppBundle\Model\BotRequest\BotRequestInterface;
use Symfony\Component\EventDispatcher\Event;
class BotLogMessage extends Event
{
const NAME = 'bot.log.message';
/**
* #var string
*/
private $message;
/**
* #param string $message
*/
public function __construct($message)
{
$this->message = $message;
}
/**
* #return string
*/
public function getMessage() : string
{
return $this->message;
}
}
I separated mobile and web requests with the help of kernel.view Event Listener.
The logic works like this:
if request is coming from mobile, then load xxx.mobile.twig
if request is coming from web, then load xxx.html.twig
This is working with my CustomBundle without any problem. In addition to it I'm using FOSUserBundle and HWIOAuthBundle with some of their routes. I checked var/logs/dev.log and I can't see kernel.view events regarding these bundles routes and eventually my listener cannot work with these bundles.
Could you give me an idea how could I bind to the kernel.view event for those bundles?
/**
* #param GetResponseForControllerResultEvent $event
* #return bool
*/
public function onKernelView(GetResponseForControllerResultEvent $event)
{
if (!$this->isMobileRequest($event->getRequest()->headers->get('user-agent'))) {
return false;
}
$template = $event->getRequest()->attributes->get('_template');
if (!$template) {
return false;
}
$templateReference = $this->templateNameParser->parse($template);
if ($templateReference->get('format') == 'html' && $templateReference->get('bundle') == 'CustomBundle') {
$mobileTemplate = sprintf(
'%s:%s:%s.mobile.twig',
$templateReference->get('bundle'),
$templateReference->get('controller'),
$templateReference->get('name')
);
if ($this->templating->exists($mobileTemplate)) {
$templateReference->set('format', 'mobile');
$event->getRequest()->attributes->set('_template', $templateReference);
}
}
}
There are a few things you should consider when debugging event related issues on Symfony2.
Events propagation can be stopped
it could be that another listener is listening for the very same event and is stopping the event propagation by calling $event->stopPropagation(). In that case make sure your listener is executed first (see point 2 below).
Event listeners have priorities
When defining a listener you can set its priority like shown below:
view_response_listener:
class: AppBundle\EventListener\ViewResponseListener
tags:
# The highest the priority, the earlier a listener is executed
# #see http://symfony.com/doc/2.7/cookbook/event_dispatcher/event_listener.html#creating-an-event-listener
- { name: kernel.event_listener, event: kernel.view, method: onKernelView, priority: 101 }
The other optional tag attribute is called priority, which defaults to 0 and it controls the order in which listeners are executed (the highest the priority, the earlier a listener is executed). This is useful when you need to guarantee that one listener is executed before another. The priorities of the internal Symfony listeners usually range from -255 to 255 but your own listeners can use any positive or negative integer.
Source: http://symfony.com/doc/2.7/cookbook/event_dispatcher/event_listener.html#creating-an-event-listener
Usually the dispatch of those events is done in the bootstrap file
Make sure regenerate your bootstrap file (and clear the cache now that you're at it!), especially if you're playing with priorities and/or your configuration.
composer run-script post-update-cmd
php app/console cache:clear --env=dev
The composer post-update-cmd will regenerate your bootstrap file but it will also do other things like reinstalling your assets which is probably something that you don't need. To just regenerate the bootstrap file check my answer here.
I find the solution, it is however a bit workaround, working properly now.
I put following function to my MobileTemplateListener.php file.
More details are here -> http://www.99bugs.com/handling-mobile-template-switching-in-symfony2/
/**
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($this->isMobileRequest($request->headers->get('user-agent')))
{
//ONLY AFFECT HTML REQUESTS
//THIS ENSURES THAT YOUR JSON REQUESTS TO E.G. REST API, DO NOT GET SERVED TEXT/HTML CONTENT-TYPE
if ($request->getRequestFormat() == "html")
{
$request->setRequestFormat('mobile');
}
}
}
/**
* Returns true if request is from mobile device, otherwise false
* #return boolean mobileUA
*/
private function isMobileRequest($userAgent)
{
if (preg_match('/(android|blackberry|iphone|ipad|phone|playbook|mobile)/i', $userAgent)) {
return true;
}
return false;
}
as a result when kernel.request event listener starts to handling, it is setting the format with value mobile
I was using FOSUserBundle through a child bundle to manipulate for my needs. You may find more details here -> https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/overriding_controllers.md
for instance : we assume SecurityController.php
I created a file named SecurityController.php under my UserBundle It looks like following.
<?php
namespace Acme\UserBundle\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
use FOS\UserBundle\Controller\SecurityController as BaseController;
use Symfony\Component\HttpFoundation\Request;
use Acme\UserBundle\Overrides\ControllerOverrideRenderTrait;
class SecurityController extends BaseController
{
use ControllerOverrideRenderTrait;
public function loginAction(Request $request)
{
$securityContext = $this->container->get('security.context');
$router = $this->container->get('router');
if ($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
return new RedirectResponse($router->generate('my_profile_dashboard'), 307);
}
return parent::loginAction($request);
}
}
for all other FOS controllers I have to override render function. Which is provided by smyfony Symfony\Bundle\FrameworkBundle\Controller
But already my child bundle extends the FOSUserBundle's controllers, the only way to override this without duplicates of code is to use traits.
and I created one trait as following.
<?php
namespace Acme\UserBundle\Overrides;
use Symfony\Component\HttpFoundation\Response;
trait ControllerOverrideRenderTrait {
/**
* This overrides vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
* Renders a view.
*
* #param string $view The view name
* #param array $parameters An array of parameters to pass to the view
* #param Response $response A response instance
*
* #return Response A Response instance
*/
public function render($view, array $parameters = array(), Response $response = null)
{
$format = $this->getRequest()->getRequestFormat();
$view = str_replace('.html.', '.' . $format . '.', $view);
return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}
}
The Original function provided by symfony is the following.
/**
* Renders a view.
*
* #param string $view The view name
* #param array $parameters An array of parameters to pass to the view
* #param Response $response A response instance
*
* #return Response A Response instance
*/
public function render($view, array $parameters = array(), Response $response = null)
{
return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}
Basically my change replaces '.html.' part in template name by providing $format which setted via onKernelRequest EventListener.
don't forget to add your service definition in services.yml
services:
acme.frontend.listener.mobile_template_listener:
class: Acme\FrontendBundle\EventListener\MobileTemplateListener
arguments: ['#templating', '#templating.name_parser']
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
I'm working in a project using Symfony 2,
I'm using Assetic with rewrite and less filter, and it work fine,
Now I'm planing to let administrator (connected user) to controle some features in css like font and main color.
The problem that I'm facing is :
- how can I proceed to integrate these css changes from entity to the css management
I can't let assetic use routing rule to include custom css
Eaven if I success to get this work, every time I have a changes to the custom css I have to install assets to web folder and make the assetic:dump and clearing cache from a controller.
If you (or someone else) still need this:
I solved this by putting all generic CSS in a asset handled by Assetic like usual and putting the dynamic CSS generation in a Controller action and rendering the CSS with Twig.
As suggested by Steffen you should put the dynamic CSS in a Twig template.
But now you might suffer from that part of the css being a full request to a symfony application instead of a css (HTTP 302 and such) which increases server load.
Thats why I would advise you to do 3 things (you can skip step 2 if your css doesn't change without interaction, e.g. date based):
Implement a service which caches the current output to e.g. web/additional.css.
Write and register a RequestListener to update the css regularly
Extend all controller actions that could introduce changes to the css with the service call
Example (assumes you use Doctrine and have an entity with some color information):
Service
<?php
//Acme\DemoBundle\Service\CSSDeployer.php
namespace Acme\DemoBundle\Service;
use Doctrine\ORM\EntityManager;
class CSSDeployer
{
/**
* #var EntityManager
*/
protected $em;
/**
* Twig Templating Service
*/
protected $templating;
public function __construct(EntityManager $em, $templating)
{
$this->em = $em;
$this->templating = $templating;
}
public function deployStyle($filepath)
{
$entity = $this->em->getRepository('AcmeDemoBundle:Color')->findBy(/* your own logic here */);
if(!$entity) {
// your error handling
}
if(!file_exists($filepath)) {
// your error handling, be aware of the case where this service is run the first time though
}
$content = $this->templating->render('AcmeDemoBundle:CSS:additional.css.twig', array(
'data' => $entity
));
//Maybe you need to wrap below in a try-catch block
file_put_contents($filepath, $content);
}
}
Service Registration
#Acme\DemoBundle\Resources\config\services.yml
services:
#...
css_deployer:
class: Acme\DemoBundle\Service\CSSDeployer
arguments: [ #doctrine.orm.entity_manager, #templating ]
RequestListener
<?php
//Acme\DemoBundle\EventListener\RequestListener.php
namespace Acme\DemoBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Debug\Exception\ContextErrorException;
use \DateTime;
use Doctrine\ORM\EntityManager;
class RequestListener
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var EntityManager
*/
protected $em;
public function __construct(ContainerInterface $container, $em)
{
$this->container = $container;
$this->em = $em;
}
/**
* Checks filemtime (File modification time) of web/additional.css
* If it is not from today it will be redeployed.
*/
public function onKernelRequest(GetResponseEvent $event)
{
$kernel = $event->getKernel();
$container = $this->container;
$path = $container->get('kernel')->getRootDir().'/../web'.'/additional.css';
$time = 1300000000;
try {
$time = #filemtime($path);
} catch(ContextErrorException $ex) {
//Ignore
} catch(\Exception $ex) {
//will never get here
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))) {
throw $ex;
}
}
if($time === FALSE || $time == 1300000000) {
file_put_contents($path, "/*Leer*/");
$time = 1300000000;
}
$modified = new \DateTime();
$modified->setTimestamp($time);
$today = new \DateTime();
if($modified->format("Y-m-d")!= $today->format("Y-m-d")) {
//UPDATE CSS
try {
$container->get('css_deployer')->deployStyle($path);
} catch(\Exception $ex) {
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))){
throw $ex;
}
}
} else {
//DO NOTHING
}
}
}
RequestListener registration
#Acme\DemoBundle\Resources\config\services.yml
acme_style_update_listener.request:
class: Acme\DemoBundle\EventListener\RequestListener
arguments: [ #service_container, #doctrine.orm.entity_manager ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Controller actions
public function updateAction()
{
// Stuff
$path = '....';
$this->get('css_deployer')->deployStyle($path);
}
Hope this helps someone in the future.