how to pass username to rollbar via monolog, symfony2 - symfony

I am using rollbar.com to collect all details about exceptions in symfony2 app. However I don't understand how can I configure monolog so it would pass username and user id to rollbar.
I see that I can pass rollbar config as shown here and I am thinking person_fn is what I need. Still I don't know where to put this function (this should be in service because I need to check security token) and how to pass it to rollbar.
# config_prod.yml
rollbar:
type: rollbar
level: error
token: %rollbar_token%
config:
person_fn: getUserForRollbarRightAboutNowOrSomething

Found solution:
update monolog/monolog bundle to at least 1.17.0 version.
create ContextProcessor and update user information
#src/AppBundle/Monolog/RollbarContextProcessor
namespace AppBundle\Monolog;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class RollbarContextProcessor
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function processRecord($record)
{
if ($this->tokenStorage->getToken()) {
$user = $this->tokenStorage->getToken()->getUser();
if ($user instanceof User) {
$record['context']['payload']['person'] = [
'id' => $user->getId(),
'username' => $user->getUsername(),
'email' => $user->getEmail(),
];
}
}
return $record;
}
}
configure ContextProcessor as service with monolog.processor tag.
# app/config/config_prod.yml
services:
monolog.processor.rollbar_context:
class: AppBundle\Monolog\RollbarContextProcessor
arguments: [#security.token_storage]
tags:
- { name: monolog.processor, method: processRecord, handler: rollbar }
monolog:
handlers:
rollbar:
type: rollbar
level: error
token: %rollbar_token%

Your question has two parts:
Rollbar
person_fn is exactly what you need. You should be able to add a reference to the function by using a string (e.g.: "MyClass::static_function_reference" or "my_function_name").
Symfony
Disclaimer: I don't use or know much about Symfony.
This question has some excellent examples of how to get the current user in Symfony. (Punch line: in a controller you can call $this.getUser())
This question has a good example of how to inject the current user in a service. (Make a Twig Extension that depends on the SecurityContext or TokenStorage, use those dependencies to get a user objet).
Finally, there's the classic PHP move: as soon as you have a user add it to $_REQUEST. I'm not sure if Symfony co-opts this, but it'd be a valid way in a non-framework PHP application.

Related

Issue programmatically authenticating Users for PhpUnit functional test - Unmanaged by Doctrine - Symfony 4.3

I'm trying to get a simple "200 Response" test to work for a part of a website requiring an authenticated user. I think I've got the creation of the Session working, as during debugging the Controller function is called and a User is retrieved (using $this->getUser()).
However, afterwards the function fails with the following message:
1) App\Tests\Controller\SecretControllerTest::testIndex200Response
expected other status code for 'http://localhost/secret_url/':
error:
Multiple non-persisted new entities were found through the given association graph:
* A new entity was found through the relationship 'App\Entity\User#role' that was not configured to cascade persist operations for entity: ROLE_FOR_USER. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
* A new entity was found through the relationship 'App\Entity\User#secret_property' that was not configured to cascade persist operations for entity: test123. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade pe
rsist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). (500 Internal Server Error)
Failed asserting that 500 matches expected 200.
This would make sense if this was not already stored in the (MySQL) database and retrieved with Doctrine. The records are created using Fixtures on each run/for each test. This is why in the Controller $this->getUser() functions as expected.
The test I'm wanting to work:
public function testIndex200Response(): void
{
$client = $this->getAuthenticatedSecretUserClient();
$this->checkPageLoadResponse($client, 'http://localhost/secret_url/');
}
Get a user:
protected function getAuthenticatedSecretUserClient(): HttpKernelBrowser
{
$this->loadFixtures(
[
RoleFixture::class,
SecretUserFixture::class,
]
);
/** #var User $user */
$user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => 'secret_user']);
$client = self::createClient(
[],
[
'PHP_AUTH_USER' => $user->getUsername(),
'PHP_AUTH_PW' => $user->getPlainPassword(),
]
);
$this->createClientSession($user, $client);
return $client;
}
Create a session:
// Based on https://symfony.com/doc/current/testing/http_authentication.html#using-a-faster-authentication-mechanism-only-for-tests
protected function createClientSession(User $user, HttpKernelBrowser $client): void
{
$authenticatedGuardToken = new PostAuthenticationGuardToken($user, 'chain_provider', $user->getRoles());
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($authenticatedGuardToken);
$session = self::$container->get('session');
$session->set('_security_<security_context>', serialize($authenticatedGuardToken));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$client->getCookieJar()->set($cookie);
self::$container->set('security.token_storage', $tokenStorage);
}
This works for the creating of the client, session and cookie.
When the Request is executed to the $url in the first function, it gets into the endpoint, confirming the User is indeed authenticated.
According to the documentation here a User should be "refreshed" from via the configured provider (using Doctrine in this case) to check if a given object matches a stored object.
[..] At the beginning of the next request, it's deserialized and then passed to your user provider to "refresh" it (e.g. Doctrine queries for a fresh user).
I would expect this would also ensure that the session User is replaced with a Doctrine managed User object to prevent the error above.
How can I go about solving that the User in the session becomes a managed User during PhpUnit testing?
(Note: the production code works without any issue, this problem only arises during testing (legacy code now starting to get tests))
Ok, had multiple issues, but got it working doing the following:
First, was creating a Client using incorrect password, I was creating (in Fixtures) User entities with username and password being identical. The function getPlainPassword, though present in an interface, was not something stored, so was a blank value.
Corrected code:
$client = self::createClient(
[],
[
'PHP_AUTH_USER' => $user->getUsername(),
'PHP_AUTH_PW' => $user->getUsername(),
]
);
Next, a User not being refreshed took some more.
In config/packages/security.yaml, add the following:
security:
firewalls:
test:
security: ~
This is to create the "test" key, as creating that immediately in the next file will cause a permission denied error. In config/packages/test/security.yaml, create the following:
security:
providers:
test_user_provider:
id: App\Tests\Functional\Security\UserProvider
firewalls:
test:
http_basic:
provider: test_user_provider
This adds a custom UserProvider specifically for testing purposes (hence usage App\Tests\ namespace). You must register this service in your config/services_test.yaml:
services:
App\Tests\Functional\Security\:
resource: '../tests/Functional/Security'
Not sure you'll need it, but I added in config/packages/test/routing.yaml the following:
parameters:
protocol: http
As PhpUnit is testing via CLI, there by default is no secure connection, can vary by environment so see if you need it.
Lastly, config for test framework in config/packages/test/framework.yaml:
framework:
test: true
session:
storage_id: session.storage.mock_file
All of the above config (apart from the http bit) is to ensure that a custom UserProvider will be used to provider User objects during testing.
This might excessive for others, but our setup (legacy) has some custom work for providing Users for authentication (which seems very related but far out of my current issue scope).
Back on to the UserProvider, it's setup like so:
namespace App\Tests\Functional\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserProvider implements UserProviderInterface
{
/** #var UserRepository */
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function loadUserByUsername($username)
{
try {
return $this->userRepository->getByUsername($username);
} catch (UserNotFoundException $e) {
throw new UsernameNotFoundException("Username: $username unknown");
}
}
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return User::class === $class;
}
}
Note: should you use this, you need to have a getByUsername function in your UserRepository.
Please note, this might not be the solution for you. Maybe you need to change it up, maybe it's completely off. Either way, thought to leave a solution for any future souls.

In Symfony2 how can I use a Service within a Factory class?

I am trying to setup a Symfony implementation of this PHP library for Chargify https://github.com/johannez/chargify
I'm getting a bit lost working out the best / proper way to set it all up.
I think I need to setup Guzzle as a service, then create a Chargify factory and have that added as a service.
My problem is that in the factory class, when I try and use the Guzzle service I get a fatal error
Fatal error: Using $this when not in object context in /symfony/src/Acme/ChargifyBundle/Factory/ChargifyFactory.php on line 8
This is my Factory class
<?php
namespace Acme\ChargifyBundle\Factory;
class ChargifyFactory implements ChargifyFactoryInterface
{
public static function build($type)
{
$client = $this->get('chargify.guzzle.client');
$className = 'Chargify\\Controller\\' . ucfirst($type);
if (class_exists($className)) {
return new $className($client);
}
else {
throw new Exception("Invalid controller type given.");
}
}
}
If it's useful to see some config, this is my services.yml for the bundle
services:
chargify.guzzle.client.curl_auth:
class: %guzzle.plugin.curl_auth.class%
arguments:
api_key: %chargify_api_key%
chargify.guzzle.client:
class: %guzzle.client.class%
tags:
- { name: guzzle.client }
calls:
- [setBaseUrl, [%chargify_domain%]]
- [addSubscriber, [#chargify.guzzle.client.curl_auth]]
argument: %chargify_domain%
chargify.factory:
class: Acme\ChargifyBundle\Factory\ChargifyFactory
arguments:
- ["type"]
chargify.customer:
class: Acme\ChargifyBundle\Controller\CustomerController
factory_class: Acme\ChargifyBundle\Factory\ChargifyFactory
factory_method: build
arguments:
type: "customer"
How can I use the guzzle client in the Factory with out using
$client = $this->get('chargify.guzzle.client');
EDIT:
I have changed the code as per #alex's answer, but I'm still getting an error. I think this is because the function is static. I've looked though the documents, but I can't see where I can setup a factory without a static function, and when I get rid of static I get a different error.
Runtime Notice: Non-static method Acme\ChargifyBundle\Factory\ChargifyFactory::build() should not be called statically, assuming $this from incompatible context
That is being thrown from some generated code
protected function getChargify_CustomerService()
{
return $this->services['chargify.customer'] = \Acme\ChargifyBundle\Factory\ChargifyFactory::build('customer');
}

Pre_deserialize callback not working in JMSSerializer

I am trying to just execute my Document's __constructor on pre_deserialization via jmsserializer but I don't have a clue why it is not working.
I am loading the serializer metadata from a yaml file looking like this:
AppBundle\Document\Campaign:
exclusion_policy: ALL
xml_root_name: campaign
properties:
id:
type: string
expose: true
slug:
type: string
expose: true
name:
type: string
expose: true
callback_methods:
pre_deserialize: [__construct]
When I try to deserialize executing:
$object = $serializer->deserialize($jsonString, 'AppBundle\\Document\\Campaign', 'json');
I am unable to reach the contructor function, however If I change the event to any of the others available (pre_serialize, post_serialize and post_deserialize) I do.
I think there are missing code about the handling of this specific event but trying to copy the same code affecting the other events it still not working.
It looks like it is never registered in the event dispatcher or something similar.
My environment is:
symfony 2.6.3
jms/serializer 0.16.0
jms/serializer-bundle 0.13.0
Thanks.
I can verify this appears to be a bug in JMS Serializer. For some reason, the service container is not reading the pre_deserialize events and registering it with JMS.
You can, however, work around this using an event subscriber.
First define the Subscriber class, similar to your listener:
<?php
namespace Acme\AcmeBundle\Listener;
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
class SerializationSubscriber implements EventSubscriberInterface
{
/**
* #inheritdoc
*/
static public function getSubscribedEvents()
{
return array(
array('event' => 'serializer.pre_deserialize', 'method' => 'onPreDeserialize'),
);
}
public function onPreDeserialize(PreDeserializeEvent $event)
{
echo "we're about to de-cerealizing";
}
}
Then register the Subscriber in your bundle's services configuration:
parameters:
acme.serializer_subscriber.class: Acme\AcmeBundle\Listener\SerializationSubscriber
services:
acme.serializer.subscriber:
class: %acme.serializer_subscriber.class%
tags:
- { name: jms_serializer.event_subscriber }
Rebuild your cache, and you should be good!
Official Documentation: http://jmsyst.com/libs/serializer/master/event_system

Best practice to implement Factory pattern using Symfony2

I am making a messenger which can send email messages or sms messages, and has the possibility to send them now or send them later (the information is saved in the DB). I've made 2 solutions, but neither is satisfying me.
I'm centralising the code in one Factory, and the code of the Factory pattern is very easy:
class MessageFactory
{
static public function get($type,$em)
{
$instance = null;
switch ($type) {
case 'email':
$instance = new EmailMessage($em);
break;
....
return $instance;
}
class EmailMessage implements MessangerInterface
{
...
public function send( $eMessage,array $receivers, $time=NULL)
{
interface MessangerInterface
{
public function send($message,array $receivers);
}
1st solution: Just call as an ordinary static method
$messanger = Factory\MessageFactory::get('email',$em);
$messanger->send($eMessage, array('tom'=>'tom#gmail.com'));
This is a bad solution, because I need to pass in a Doctrine Manager as a parameter to the method
2nd solution: To use it as a Symfony 2 Service
services:
my.messanger:
class: Bundle\Factory\MessangerInterface
factory_class: Bundle\Factory\MessageFactory
factory_method: get
arguments:
messanger_type: %messanger.type%
and also pass in Doctrine as an argument. But using such a solution I can't choose messanger.type in my code, it's defined using a configuration parameter as email or sms; I need to have the capability in code to choose the type.
Also I have a problem that inside the class I need to send email or sms, and that means that I need an external service, getting it like this:
class EmailMessage implements MessangerInterface
{
if ('AppCache' == get_class($kernel)) {
$kernel = $kernel->getKernel();
}
$kernel->getContainer()->get('mailer')->send($eMessage);
which seems like very bad practice.
Please, are you able to advise me on any better solutions?
I want to follow the "thin controller fat model" concept.
It seems like option 2, using Symfony 2 Services, would be best.
I considered suggesting that you let the Factory be the Service, and pass the type in to get the Messenger instance, rather than fixing it in config, but if what you want is to only have one of each type of Messenger then that's unhelpful (the Factory would keep creating more and more Messengers). So instead I think you need to define two Services, one for each Messenger.
And if you don't want to have to fetch another Service within your Messenger, you need to inject that in when you get the Messenger.
e.g.
services:
mailer:
class: Mailer
smser:
class: SMSer
email.messanger:
class: Bundle\Factory\MessangerInterface
factory_class: Bundle\Factory\MessageFactory
factory_method: get
arguments:
messanger_type: email
sender: #mailer
sms.messanger:
class: Bundle\Factory\MessangerInterface
factory_class: Bundle\Factory\MessageFactory
factory_method: get
arguments:
messanger_type: sms
sender: #smser
And your Factory needs to accept the new $sender argument:
class MessageFactory
{
static public function get($type,$em,$sender)
{
$instance = null;
switch ($type) {
case 'email':
$instance = new EmailMessage($em, $sender);
break;
....
return $instance;
}
interface MessangerInterface
{
public function send($message,$sender, array $receivers);
}
Then when you call it, you ask for either of the Messengers specifically:
$this->get('email.messenger')->send($emailMessage);
$this->get('sms.messenger')->send($smsMessage);

Get twilio instance in FOSUserBundle event

I am trying to send SMS to users who register in my website through Twilio, I got the vresh/twilio-bundle and it works fine.
I am trying to pass twilio instance to the event but I think I am missing something, here is what I am doing:
In config.yml i set the servide like this:
services:
registration.completed.listener:
class: Jaguar\AloBundle\EventListener\RegistrationEventListener
arguments:
entityManager: ["#doctrine.orm.voipswitch_entity_manager", "vresh_twilio"]
tags:
- { name: kernel.event_subscriber, event: performOnRegistrationCompleted }
I have declared the twilio config:
vresh_twilio:
sid: 'xxx'
authToken: 'xxx'
version: '2010-04-01'
retryAttempts: 3
Then, in my method I try to get the instance:
public function performOnRegistrationCompleted(UserEvent $event)
{
$twilio = $event->get('vresh_twilio');
}
But it fails...
Any help on this, please?
Thanks a lot!
There are a few issues with your service setup.
You are not actually passing the Twilio instance as you have no # sign preceding the service name. #vresh_twilio is a service, vresh_twilio is just a string.
You are passing in an associative array with a key of entityManager and a value that is also an array with the values of the service #doctrine.orm.voipswitch_entity_manager and the string vresh_twilio.
You're not passing the Twilio instance in your event you are building a listener with the Twilio instance in the constructor.
Your service should actually look like...
services:
registration.completed.listener:
class: Jaguar\AloBundle\EventListener\RegistrationEventListener
arguments:
entityManager: "#doctrine.orm.voipswitch_entity_manager"
twilio: "#vresh_twilio"
// Or
// - #doctrine.orm.voipswitch_entity_manager
// - #vresh_twilio
// Or
// [#doctrine.orm.voipswitch_entity_manager, #vresh_twilio]
//
// As they all mean the same thing and the keys aren't
// used in your actual service __construct
tags:
- { name: kernel.event_subscriber, event: performOnRegistrationCompleted }
This would mean your listener would then have a constructor to receive those services like..
protected $entityManager;
protected $twilio;
public function __conctruct(ObjectManager $entityManager, TwilioWrapper $twilio)
{
$this->entityManager = $entityManager;
$this->twilio = $twilio;
}
Meaning that you could then call it in your class using $this->twilio.
Also, from looking at the services that the Vresh\TwilioBundle creates it looks like the service that you would want to be injecting would be #twilio.api rather than #vresh_twilio as it doesn't seem to exist but I may be wrong there (I haven't used the bundle myself).

Resources