Showing posts with label symfony. Show all posts
Showing posts with label symfony. Show all posts

2017-02-20

Adding information about the logged in user to the error log in Symfony 2.8

Providing support for a Symfony application in a production environment, I am often faced with error messages in log files. And often it is unclear which user created which error message. Sometimes, users will call in and complain about a problem and it would come in handy to know which error messages were actually created by the calling user and which error messages are unrelated to the current problem. If only there was some way to store that information in the log file, next to each error message.

Fortunately, this is a task which others have tried to complete before me. I can only assume that this is the reason why the Symfony docs already contain a section on How to Add extra Data to Log Messages via a Processor. Basically, you have to create a processor class that modifies the log entry, e.g. by adding some information. Afterwards, you will need to tell Symfony that it should call your processor upon logging by adding your newly created class to the dependency injection container.

So, let's dive right in and get started with creating the processor class. We want to modify our log entry so that it always contains the user's display name and ID. Note that the display name here is a feature of my custom user class, so you might want to call a different method to get whatever value you want to display as the user's name. In Symfony, we can get the currently logged in user via the token storage. Of course, it is also possible that we are not logged in, so in that case we will get the string "anon." as a username.

<?php
namespace MyName\MyBundle;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use MyName\MyBundle\Entity\User;

class LoggedInUserProcessor
{
    private $tokenStorage;

    public function __construct(TokenStorageInterface $tokenStorage) {
        $this->tokenStorage = $tokenStorage;
    }

    public function processRecord(array $record) {
        if ($this->tokenStorage->getToken() && $this->tokenStorage->getToken()->getUser()) {
            $user = $this->tokenStorage->getToken()->getUser();
            if ($user instanceof User) {
                /** @var User $user */
                $record['extra']['userName'] = $user->getDisplayName();
                $record['extra']['userId'] = $user->getId();
            } else {
                $record['extra']['userName'] = $user;
            }
        return $record;
    }
}

That already concludes all the work which we need to do in our processor class. Now all we have to do is tell Symfony that it should call our processor whenever a log entry is written. We can achieve this by registering our processor class as a service and decorating it with the monolog.processor tag. Note that we also need to inject the token storage and define the method which should be called. As I am using YAML configuration files for my services, the respective entry looks like this:

    app.logged_in_user_processor:
        class: MyName\MyBundle\LoggedInUserProcessor
        arguments: ['@security.token_storage']
        tags:
            - { name: monolog.processor, method: processRecord }

And this is already it. Now, provided you haven't changed the default formatter, your added information will always be output at the end of each log entry. Here are some sample log entries from my application with the new processor enabled:

 [2017-02-17 08:09:58] event.DEBUG: Notified event "kernel.finish_request" to listener "Symfony\Component\Security\Http\Firewall::onKernelFinishRequest". []
{"userName":"Administrator","userId":1}

[2017-02-17 08:09:58] event.DEBUG: Notified event "kernel.terminate" to listener "Symfony\Bundle\SwiftmailerBundle\EventListener\EmailSenderListener::onTerminate". []
{"userName":"Administrator","userId":1}

2015-11-03

How to prevent choice field from rendering attr on each option element in Symfony 2.7

In my Symfony 2.6 application, I use some forms with the choice field type. As I wanted those choice fields to match the rest of my UI which uses jQuery UI, I added the relevant classes to my fields when rendering them.
<?php echo $view['form']->widget($form['myField'], ['attr' => ['class' => "text ui-widget-content ui-corner-all"]]); ?>
This worked perfectly and generated the correct attribute on my <select> tag, but not on my <option> tags. Just as I wanted it.
<select class="text ui-widget-content ui-corner-all" name="myField">
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
</select>
At a later point in time, I upgraded my application from Symfony 2.6 to 2.7 and ever since then the class attribute was rendered on the <select> tag as well as on the <option> tags.
<select class="text ui-widget-content ui-corner-all" name="myField">
    <option value="1" class="text ui-widget-content ui-corner-all">Option 1</option>
    <option value="2" class="text ui-widget-content ui-corner-all">Option 2</option>
</select>
Unfortunately, this made borders appear around the single elements in my select lists which looks quite ugly.

After a bit of searching the internet, I found a blog post about this new behavior on the Symfony blog. In New in Symfony 2.7: Choice form type refactorization, the new choice_attr options is explained. When rendering the <option> tags, Symfony now merges the entries in choice_attr with the ones in attr.
So the easiest solution seems to be to overwrite our class entry in the choice_attr option and we should be done. I did this directly in my form class.
namespace MyBundle\Form;

public function buildForm(FormBuilderInterface $builder, array $options) {
    $builder
        ->add('roles',
            'entity',
            [
                'class' => 'MyBundle:Role',
                'choice_label' => 'name',
                'multiple' => true,
                'choice_attr' => function () { return ["class" => ""]; }
            ]);
}
While this solution works, you will need to do it for each choice field that you use in your entire application. As I was using quite a few of those, I decided I wanted something more flexible which would allow me to change the behavior of the choice field without touching every single form. So I reverted my previous change and searched for a better solution.

The first step was to create a custom field type which is based on the choice field type but exhibits the same behavior I was used to from Symfony 2.6. So I created a new field type that would always override the class attribute in the choice_attr options.
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ChoiceNoOptAttrType extends ChoiceType {
    public function configureOptions(OptionsResolver $resolver) {
        parent::configureOptions($resolver);

        $resolver->setDefault("choice_attr", function () { return ["class" => ""]; });
    }
}
This at least means that I don't need to touch the choice_attr option for all my form types. However, I would still need to change each of my existing form types to use my newly created choice form type. Also not a nice solution.
I checked the Symfony documentation to find out how Symfony actually creates a choice field. Symfony uses dependency injetion to locate its built-in field types. So what we need to do is change the dependency injection container to return our newly created custom choice field type replacement.
Symfony provides what they call “Compiler Passes” to make changes to the DI container after it has been initialized. The basic idea is that you create a custom class that is given the fully instantiated DI container on which you can then perform changes, e.g. replace an existing service with a different one. (Further reading in the Symfony docs: Creating a Compiler Pass)
This seems like the perfect entry point for what we want to do: replace the built-in choice field type. Thanks to the verbose Symfony documentation, it is not hard to figure out what code we need to accomplish this task.
namespace MyBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class MyCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition("form.type.choice");
        $definition->setClass('MyBundle\Form\ChoiceNoOptAttrType');
    }
}
Now that we have created a compiler pass, we still need to make sure that it is actually executed. Compiler passes need to be registered for Symfony to find and execute them when building the container. As my compiler pass was in my bundle, I added the required code to my bundle class. (Further reading in the Symfony docs: How to Work with Compiler Passes in Bundles)
namespace MyBundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use MyBundle\DependencyInjection\Compiler\MyCompilerPass;

class MyBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new MyCompilerPass());
    }
}
And this is enough to let Symfony work its magic. Now all choice fields in my application use my custom created form type. Now I don't have to make any changes to my existing form types and they still exhibit the same behavior as in version 2.6.