Inyección de Servicios en Symfony 7: Una Guía Avanzada
Symfony 7 representa una evolución significativa en la manera en que implementamos la inyección de dependencias, un patrón fundamental para la arquitectura de aplicaciones modernas. En esta guía avanzada, exploraremos cómo Symfony 7 ha refinado este concepto, proporcionando ejemplos prácticos y técnicas que demuestran el poder y la elegancia del sistema de inyección de servicios.
El Núcleo de la Inyección de Dependencias en Symfony 7
La inyección de dependencias (DI) es uno de los pilares arquitectónicos de Symfony. Con Symfony 7, este patrón se ha optimizado para ofrecer mayor rendimiento y una sintaxis más concisa gracias a las mejoras en PHP 8.x.
El Contenedor de Servicios
El contenedor de servicios de Symfony 7 implementa el patrón PSR-11 y gestiona la creación y entrega de servicios. Veamos su configuración básica:
// config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
Este archivo define el comportamiento predeterminado para todos los servicios: autowiring (resolución automática de dependencias) y autoconfiguración (configuración automática basada en interfaces).
Técnicas Avanzadas de Inyección de Servicios
1. Inyección de Servicios mediante Atributos (#[Autowire])
Symfony 7 aprovecha al máximo los atributos de PHP 8 para simplificar la inyección de servicios:
namespace App\Controller;
use App\Service\ProductService;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController
{
#[Route('/product/{id}', name: 'product_show', methods: ['GET'])]
public function show(
#[Autowire] private ProductService $productService,
int $id
): Response {
$product = $productService->find($id);
return new Response("Producto: {$product->getName()}");
}
}
Este enfoque permite inyectar servicios directamente en los métodos, reduciendo la necesidad de constructores extensos.
2. Inyección de Servicios con Configuración Avanzada
Para casos más complejos, podemos personalizar la configuración de los servicios:
// config/services.yaml
services:
App\Service\FileUploader:
arguments:
$targetDirectory: '%kernel.project_dir%/public/uploads'
$filesystem: '@filesystem'
Y su uso:
namespace App\Service;
use Symfony\Component\Filesystem\Filesystem;
class FileUploader
{
public function __construct(
private string $targetDirectory,
private Filesystem $filesystem
) {}
public function upload(UploadedFile $file): string
{
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $this->slugify($originalFilename);
$fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();
$file->move($this->targetDirectory, $fileName);
return $fileName;
}
// ...
}
3. Inyección de Servicios Etiquetados (Tagged Services)
Symfony 7 mejora el sistema de etiquetado para colecciones de servicios:
// config/services.yaml
services:
App\EventSubscriber\:
resource: '../src/EventSubscriber/'
tags: ['kernel.event_subscriber']
Podemos luego inyectar todos los servicios con una etiqueta específica:
namespace App\Service;
use App\EventSubscriber\EventSubscriberInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class EventDispatcher
{
/**
* @param iterable<EventSubscriberInterface> $eventSubscribers
*/
public function __construct(
#[TaggedIterator('kernel.event_subscriber')]
private iterable $eventSubscribers
) {}
public function dispatch(string $eventName, array $data): void
{
foreach ($this->eventSubscribers as $subscriber) {
if ($subscriber->supportsEvent($eventName)) {
$subscriber->handle($eventName, $data);
}
}
}
}
4. Inyección de Servicios Condicionales (Service Binding)
Symfony 7 proporciona un potente sistema de enlace de servicios:
// config/services.yaml
services:
_defaults:
bind:
$mailerSender: '%app.mailer_sender%'
$environment: '%kernel.environment%'
$debug: '%kernel.debug%'
App\Service\EnvironmentAwareService:
arguments:
$isProd: '%kernel.environment% == "prod"'
Esto se puede utilizar en cualquier servicio:
namespace App\Service;
class NotificationService
{
public function __construct(
private string $mailerSender,
private bool $debug
) {}
public function sendNotification(string $email, string $message): void
{
// Si estamos en modo debug, simulamos el envío
if ($this->debug) {
echo "DEBUG: Enviando correo a {$email} desde {$this->mailerSender}: {$message}";
return;
}
// Código real de envío de correo...
}
}
5. Servicios Lazy (Lazy Services)
Para optimizar el rendimiento, Symfony 7 proporciona soporte mejorado para servicios lazy:
// config/services.yaml
services:
App\Service\ExpensiveService:
lazy: true
O mediante el atributo:
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
class UserManager
{
public function __construct(
#[Autowire(lazy: true)]
private ExpensiveService $expensiveService
) {}
// El servicio ExpensiveService solo se instanciará
// cuando realmente se utilice este método
public function processComplexTask(): void
{
$this->expensiveService->process();
}
}
Inyección de Servicios en Controladores
Symfony 7 introduce un enfoque más limpio para la inyección de servicios en controladores mediante atributos:
namespace App\Controller;
use App\Repository\ProductRepository;
use App\Service\PricingService;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class ShopController
{
public function __construct(
private ProductRepository $productRepository
) {}
#[Route('/products', name: 'product_list')]
public function list(
#[Autowire] PricingService $pricingService,
#[MapQueryParameter] int $page = 1,
#[MapQueryParameter] int $limit = 10,
#[MapQueryParameter] string $sortBy = 'name'
): Response {
$products = $this->productRepository->findPaginated($page, $limit, $sortBy);
// Aplica descuentos usando el servicio inyectado
foreach ($products as $product) {
$product->setDiscountedPrice(
$pricingService->calculateDiscount($product)
);
}
return new Response('Lista de productos con descuentos aplicados');
}
}
Implementación de Factory Services
Symfony 7 simplifica la implementación de factories con autowiring:
namespace App\Factory;
use App\Entity\User;
use App\Service\PasswordEncoder;
class UserFactory
{
public function __construct(
private PasswordEncoder $passwordEncoder
) {}
public function createUser(string $email, string $plainPassword): User
{
$user = new User();
$user->setEmail($email);
$user->setPassword(
$this->passwordEncoder->encodePassword($plainPassword)
);
$user->setCreatedAt(new \DateTimeImmutable());
return $user;
}
}
Decorando Servicios
El patrón Decorator es potente para extender la funcionalidad de servicios existentes:
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
#[AsDecorator(decorates: 'App\Service\OriginalService')]
class LoggingServiceDecorator implements ServiceInterface
{
public function __construct(
private ServiceInterface $innerService,
private LoggerInterface $logger
) {}
public function process(string $data): string
{
$this->logger->info('Procesando datos: ' . $data);
$result = $this->innerService->process($data);
$this->logger->info('Resultado: ' . $result);
return $result;
}
}
Uso de Servicios Privados
En Symfony 7, todos los servicios son privados por defecto, lo que significa que solo pueden ser inyectados, no accedidos directamente:
// src/Controller/AdminController.php
namespace App\Controller;
use App\Service\AdminService;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class AdminController
{
public function __construct(
private AdminService $adminService
) {}
#[Route('/admin/dashboard', name: 'admin_dashboard')]
public function dashboard(): Response
{
// Usa el servicio privado inyectado
$stats = $this->adminService->generateStats();
return new Response('Dashboard Admin con estadísticas');
}
}
Configuración de Servicios con Argumentos Localizados
Para situaciones donde necesitamos servicios altamente personalizados:
// config/services.yaml
services:
App\Service\GeoService:
arguments:
$apiKey: '%env(GEO_API_KEY)%'
$cacheDir: '%kernel.cache_dir%/geo'
$debug: '%kernel.debug%'
Servicios Sintéticos
Los servicios sintéticos son placeholders que serán reemplazados en tiempo de ejecución:
// config/services.yaml
services:
App\Service\CurrentUserProvider:
synthetic: true
// src/Kernel.php
namespace App;
use App\Entity\User;
use App\Service\CurrentUserProvider;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
public function boot(): void
{
parent::boot();
// Configura un servicio sintético en tiempo de ejecución
if (!$this->container->has('App\Service\CurrentUserProvider')) {
$currentUser = new CurrentUserProvider(/* ... */);
$this->container->set('App\Service\CurrentUserProvider', $currentUser);
}
}
}
Optimización del Contenedor de Servicios
Symfony 7 incluye herramientas avanzadas para optimizar el contenedor en producción:
# Compila el contenedor para producción
php bin/console cache:clear --env=prod --no-debug
Este comando genera una versión optimizada del contenedor que mejora significativamente el rendimiento.