Mastering Activity Logging in Symfony PHP: Enhancing Security, Debugging, and User Experience

Serghei Pogor on 2024-01-05

In the realm of web application development, the ability to track and log user activities stands as a cornerstone for maintaining security, ensuring accountability, and enhancing the overall user experience.

Particularly in Symfony applications, where robustness and scalability are key, implementing an activities log system transcends basic utility and emerges as an essential component for developers.

Importance of an Activity Log System

How to Implement a Professional Symfony Activity Log System

Let’s start with how NOT to do… because I see this too often in a lot of applications

Dont implement a Logging System directly in Entity

Consider an entity Product where logging is incorrectly implemented directly within its methods:

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Psr\Log\LoggerInterface;

/**
 * @ORM\Entity()
 */
class Product
{
    private LoggerInterface $logger;

    // ... other properties and methods ...

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Set the name of the product.
     *
     * @param string $name
     */
    public function setName(string $name)
    {
        $this->name = $name;

        // Bad practice: Logging directly in the entity method
        $this->logger->info('Product name changed to ' . $name);
    }

    /**
     * Set the price of the product.
     *
     * @param float $price
     */
    public function setPrice(float $price)
    {
        $this->price = $price;

        // Bad practice: Logging directly in the entity method
        $this->logger->info('Product price changed to ' . $price);
    }

    // ... other setters ...
}

Why This is a Bad Practice

Dont implement a Logging System directly in the Controller

Consider a controller action in Symfony where logging is incorrectly implemented directly within the action method:

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Psr\Log\LoggerInterface;

class ProductController extends AbstractController
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Update a product.
     *
     * @param Request $request
     * @return Response
     */
    public function updateProduct(Request $request): Response
    {
        // ... some code to update the product ...

        // Bad practice: Directly writing log to the logger
        $this->logger->info('Product updated with new details', [
            'product_id' => $productId,
            'user' => $this->getUser()->getUsername(),
            'changes' => $changes
        ]);

        // ... return a response ...
    }

    // ... other actions ...
}

Why This is a Bad Practice

Dont implement Static Logging Methods

Consider a scenario where a static logger class is used throughout a Symfony application:

class StaticLogger
{
    public static function log($message, $context = [])
    {
        // Implementation of the logging, possibly using a library like Monolog
        // This is a static method directly called from various places in the application.
    }
}

// Usage in different parts of the application:

// In a controller
StaticLogger::log('User logged in', ['user_id' => $userId]);

// In an entity
StaticLogger::log('Product updated', ['product_id' => $this->id]);

// In a service
StaticLogger::log('Payment processed', ['payment_id' => $paymentId]);

Why This is a Bad Practice

Example of Good Practice: Doctrine Event Listeners/Subscribers for Logging

Implementing a logging system for Doctrine entity lifecycle events (like pre/post update, delete, persist) in a Symfony application is a good practice that aligns with the principles of clean architecture. By utilizing Doctrine’s event listeners or subscribers, you can create a centralized and efficient logging system. Let’s look at how to do this.

Create a Doctrine Event Subscriber: This subscriber will listen to the lifecycle events of Doctrine entities (such as preUpdate, prePersist, and preRemove) and trigger logging actions accordingly.

namespace App\EventSubscriber;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Psr\Log\LoggerInterface;

class EntityLoggingSubscriber implements EventSubscriber
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function getSubscribedEvents(): array
    {
        return [
            Events::preUpdate,
            Events::prePersist,
            Events::preRemove,
        ];
    }

    public function preUpdate(LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();
        // Implement logic to log entity update
        $this->logger->info('Entity updated', ['entity' => get_class($entity)]);
    }

    public function prePersist(LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();
        // Implement logic to log new entity creation
        $this->logger->info('Entity created', ['entity' => get_class($entity)]);
    }

    public function preRemove(LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();
        // Implement logic to log entity removal
        $this->logger->info('Entity removed', ['entity' => get_class($entity)]);
    }
}

Register the Subscriber as a Service: Add your subscriber to the service container. If you’re using Symfony’s default service configuration, it should be auto-registered.

# config/services.yaml
services:
    App\EventSubscriber\EntityLoggingSubscriber:
        arguments:
            $logger: '@logger'
        tags:
            - { name: doctrine.event_subscriber }

Why This is a Good Practice

As a Senior Symfony developer, my final thoughts on implementing activity log systems revolve around best practices, scalability, and the evolving nature of web applications.

Final thoughts

As we wrap up our deep dive into the world of professional Symfony Activity Log Systems, I can’t help but think about the diverse experiences each of us has had in the coding trenches. I’ve shared insights, tips, and some dos and don’ts from my own adventures (and misadventures) in the realm of Symfony development.

But now, I’m super eager to hear from you!

Have you ever encountered a log system that was more tangled than a bowl of spaghetti at a toddler’s dinner party?

Maybe you’ve stumbled upon a logging mechanism that was so convoluted, it felt like trying to solve a Rubik’s Cube in the dark. Or perhaps you’ve seen a brilliantly executed log system that was as satisfying as that perfect cup of coffee on a rainy day.

Your stories, code examples, and experiences are what truly bring this discussion to life. They’re the secret sauce that can turn this from a monologue into a rich, vibrant conversation. So, if you’ve come across any particularly notable examples of log systems — the good, the bad, and the downright quirky — I’m all ears!

Drop a comment below and share your tales from the front lines of Symfony development.

Let’s swap stories and learn from each other’s journeys. After all, every line of code tells a story, and I’m sure you’ve got some page-turners to share!