3 Useful services for Symfony applications

Useful services for Symfony are like tools

A lot of the time you find yourself writing a lot of the same code over and over. Not only is this annoying, but it’s also prone to copy / paste errors. That is why you should stop repeating yourself and leverage service classes in your applications. Today I’ll share some of my services for Symfony which I find very useful in my day to day life as a developer. Feel free to use them in your applications.

Logging service

Logging is essential to see what happened in your application when and error occurred. Because I didn’t want to scatter my application with log statements enclosed in null checks to see if the logger is actually available, I created this LogService.

namespace App\Application\Service;

use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Wrapper service around the logger
 */
class LogService
{
    /**
     * @param RequestStack $requestStack
     * @param LoggerInterface|null $logger
     */
    public function __construct(private RequestStack $requestStack, private LoggerInterface|null $logger = null) {}

    /**
     * System is unusable.
     */
    public function emergency(string $message, array $context = []): void
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     */
    public function alert(string $message, array $context = []): void
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     */
    public function critical(string $message, array $context = []): void
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     */
    public function error(string $message, array $context = []): void
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     */
    public function warning(string $message, array $context = []): void
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }

    /**
     * Normal but significant events.
     */
    public function notice(string $message, array $context = []): void
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     */
    public function info(string $message, array $context = []): void
    {
        $this->log(LogLevel::INFO, $message, $context);
    }

    /**
     * Detailed debug information.
     */
    public function debug(string $message, array $context = []): void
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }

    public function log(string $level, string $message, array $context = []): void
    {
        $request = $this->requestStack->getCurrentRequest();
        $context['ip'] = $request?->getClientIp();
        
        $this->logger?->log($level, $message, $context);
    }
}Code language: PHP (php)

It’s a wrapper around the Psr\Log\LoggerInterface which Symfony implements. The nice thing about this wrapper is that you can inject some default context variables as demonstrated in the log function of this service.

Email service

Almost every application needs to send some sort of email. To make sending emails easy in Symfony, I’ve created this EmailService.

namespace App\Application\Service;

use App\Entity\User;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;

/**
 * Service to send emails
 */
class EmailService
{
    /**
     * @param LogService $logService
     * @param MailerInterface $mailer
     * @param SettingService $settingService
     */
    public function __construct(
        private LogService $logService,
        private MailerInterface $mailer,
        private SettingService $settingService
    ) {}

    /**
     * @param string $template
     * @param array|string $to
     * @param string $subject
     * @param array $templateVariables
     */
    public function send(string $template, array|string $to, string $subject, array $templateVariables = []): void
    {
        $toAddress = is_array($to) ? new Address($to['address'], $to['name']) : new Address($to);

        $email = (new TemplatedEmail())
            ->to($toAddress)
            ->from(new Address(
                $this->settingService->get('application.from-email', 'noreply@woutercarabain.com'),
                $this->settingService->get('application.from-name', 'Wouters Website')
            ))
            ->subject($subject)
            ->context($templateVariables)
            ->htmlTemplate(sprintf('emails/%s.html.twig', $template))
            ->textTemplate(sprintf('emails/%s.text.twig', $template));

        try {
            $this->mailer->send($email);
        } catch (TransportExceptionInterface $e) {
            $this->logService->warning(
                sprintf('Cannot send email message: %s', $e->getMessage()),
                [
                    'exception' => $e,
                    'template' => $template,
                    'to' => $to,
                    'subject' => $subject,
                    'templateVariables' => $templateVariables,
                ]
            );
        }
    }

    /**
     * @param string $template
     * @param User $user
     * @param string $subject
     * @param array $templateVariables
     */
    public function sendToUser(string $template, User $user, string $subject, array $templateVariables = []): void
    {
        $this->send(
            $template,
            ['address' => $user->getEmail(), 'name' => $user->getName()],
            $subject,
            $templateVariables
        );
    }
}Code language: PHP (php)

This service handles all emails sent from my applications. It uses Twig templates for the email markup. If you want you can change the send function to load the templates from the database and create a nice management page for them. It’s also pretty easy to make the emails multilanguage. Just add another parameter for the language code and separate the templates by language.

The sendToUser method is added to easily send emails to users of our application without the need to specify the $to parameter.

When you want to send an email, simply inject the EmailService and call the send or the sendToUser method with the required parameters.

Application settings service

If you want to change simple application settings like 3rd party API URLs, mail servers or other simple settings without changing your configuration variables and re-deploying your application, then this is de service for you! For this service to work we need to create a simple entity to store configuration values in the database:

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use App\Repository\SettingRepository;

#[ORM\Entity(repositoryClass: SettingRepository::class)]
class Setting
{
    #[ORM\Id()]
    #[ORM\Column(type: "string", length: 50, nullable: false)]
    private string $identifier;

    #[ORM\Column(type: "string", length: 1024, nullable: false)]
    private string $value;

    /**
     * Setting constructor.
     *
     * @param string $identifier
     * @param mixed  $value
     */
    public function __construct(string $identifier, $value)
    {
        $this->setIdentifier($identifier);
        $this->setValue($value);
    }

    /**
     * @return string
     */
    public function getIdentifier(): string
    {
        return $this->identifier;
    }

    /**
     * @param string $identifier
     *
     * @return Setting
     */
    public function setIdentifier(string $identifier): Setting
    {
        $this->identifier = $identifier;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getValue(): mixed
    {
        return unserialize($this->value);
    }

    /**
     * @param mixed $value
     *
     * @return Setting
     */
    public function setValue(mixed $value): Setting
    {
        $this->value = serialize($value);

        return $this;
    }
}
Code language: PHP (php)

As you can see this is a very simple entity for storing settings. Because the values get serialized in the database you can use it to store basically anything. From simple strings to complex configurations. Now we need a companion service to go with this Setting entity.

namespace App\Application\Service;

use App\Entity\Setting;
use App\Repository\SettingRepository;
use Doctrine\ORM\EntityManagerInterface;

class SettingService
{
    /**
     * @var array
     */
    private array $loadedSettings = [];

    /**
     * @var bool
     */
    private bool $settingsLoaded = false;

    /**
     * @param EntityManagerInterface $entityManager
     * @param SettingRepository $settingRepository
     */
    public function __construct(
        private EntityManagerInterface $entityManager,
        private SettingRepository $settingRepository
    ) {}

    /**
     * @param string $identifier

     * @param mixed|null $default
     *
     * @return mixed
     */
    public function get(string $identifier, mixed $default = null): mixed
    {
        if (false === $this->has($identifier)) {
            return $default;
        }

        return $this->loadedSettings[$identifier];
    }

    /**
     * @param string $identifier
     * @param mixed $value
     */
    public function set(string $identifier, mixed $value): void
    {
        $setting = $this->settingRepository->findOneBy(['identifier' => $identifier]);
        if (null === $setting) {
            $setting = new Setting($identifier, $value);
        }

        $this->entityManager->persist($setting);
        $this->entityManager->flush();

        $this->loadedSettings[$identifier] = $value;
    }

    /**
     * @param string $identifier
     *
     * @return bool
     */
    public function has(string $identifier): bool
    {
        $this->loadSettings();

        return isset($this->loadedSettings[$identifier]);
    }

    /**
     * Loads all settings into memory
     */
    private function loadSettings(): void
    {
        if (true === $this->settingsLoaded) {
            return;
        }

        $settings = $this->settingRepository->findAll();
        foreach($settings as $setting) {
            $this->loadedSettings[$setting->getIdentifier()] = $setting->getValue();
        }

        $this->settingsLoaded = true;
    }
}Code language: PHP (php)

This is the service we will inject when we want to get or set a setting value. What this service does is load all the settings into the memory when you first need to access a setting. The subsequent calls to the get method will retrieve the value from memory for fast access. The set method can be used to add or update a setting in the database.

I use this service quite often to store the from name and from email address as you can see in the send method of the EmailService above.

These are the 3 services for Symfony I find very useful. If you have useful services you use in basically every application, let us know! Also if you use these services in your application I’d love to hear from you.

2 comments

Leave a Reply

Your email address will not be published. Required fields are marked *