Patrón Factory Avanzado en Laravel: Container, Service Locator y Abstract Factory
29 Dec 2025
14 min read
Introducción
En el artículo anterior sobre el Patrón Factory, aprendimos los fundamentos de este patrón de diseño y cómo implementarlo en Laravel de forma básica. Vimos cómo crear Factories simples usando match y new para instanciar objetos.
Pero Laravel ofrece herramientas mucho más poderosas que podemos aprovechar para llevar nuestras Factories al siguiente nivel. En este artículo exploraremos técnicas avanzadas que te permitirán crear Factories más flexibles, escalables y mantenibles.
¿Qué veremos en este artículo?
- Factory con el Container de Laravel: Inyección automática de dependencias
- Factory vs Service Locator: Evitando anti-patrones
- Abstract Factory: Familias de objetos relacionados
- Factory con caché: Optimización de instancias
- Factory dinámica: Registro de implementaciones en runtime
- Mejores prácticas avanzadas
Si aún no has leído el artículo básico, te recomiendo empezar por ahí: Patrón Factory en Laravel.
Factory Avanzada: Usando el Container de Laravel
Una de las características más poderosas de Laravel es su Container de Inyección de Dependencias. Veamos cómo aprovecharlo en nuestras Factories.
El Problema con la Instanciación Manual
En el artículo anterior, nuestras Factories instanciaban objetos directamente:
class NotifierFactory
{
public function make(string $type): NotifierInterface
{
return match ($type) {
'email' => new EmailNotifier(),
'sms' => new SmsNotifier(),
'push' => new PushNotifier(),
default => throw new InvalidArgumentException("Tipo no soportado: {$type}")
};
}
}
Problemas de este enfoque:
✗ Si EmailNotifier necesita dependencias, debemos pasarlas manualmente
✗ No aprovechamos el Container de Laravel
✗ Dificulta el testing y la configuración
Solución: Factory con Container
<?php
namespace App\Factories;
use App\Contracts\NotifierInterface;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
class NotifierFactory
{
private array $notifiers = [
'email' => \App\Services\Notifications\EmailNotifier::class,
'sms' => \App\Services\Notifications\SmsNotifier::class,
'push' => \App\Services\Notifications\PushNotifier::class,
'slack' => \App\Services\Notifications\SlackNotifier::class,
];
public function __construct(
private Container $container
) {}
public function make(string $type): NotifierInterface
{
if (!isset($this->notifiers[$type])) {
throw new InvalidArgumentException("Tipo de notificador no soportado: {$type}");
}
// Laravel resuelve automáticamente las dependencias
return $this->container->make($this->notifiers[$type]);
}
/**
* Registrar un nuevo tipo de notificador dinámicamente.
*/
public function extend(string $type, string $class): self
{
$this->notifiers[$type] = $class;
return $this;
}
/**
* Verificar si un tipo está registrado.
*/
public function hasType(string $type): bool
{
return isset($this->notifiers[$type]);
}
/**
* Obtener todos los tipos disponibles.
*/
public function getAvailableTypes(): array
{
return array_keys($this->notifiers);
}
}
Ventajas de este enfoque:
✓ Inyección automática: Laravel resuelve todas las dependencias
✓ Extensibilidad: Método extend() para agregar tipos dinámicamente
✓ Introspección: Métodos para verificar y listar tipos disponibles
✓ Testing: Fácil de mockear y testear
Ejemplo: Notifier con Dependencias
Ahora EmailNotifier puede tener todas las dependencias que necesite:
<?php
namespace App\Services\Notifications;
use App\Contracts\NotifierInterface;
use Illuminate\Mail\Mailer;
use Illuminate\Contracts\Queue\Queue;
use Psr\Log\LoggerInterface;
class EmailNotifier implements NotifierInterface
{
public function __construct(
private Mailer $mailer,
private Queue $queue,
private LoggerInterface $logger
) {}
public function send(string $recipient, string $message): bool
{
try {
$this->queue->push(function() use ($recipient, $message) {
$this->mailer->raw($message, function($msg) use ($recipient) {
$msg->to($recipient);
});
});
$this->logger->info("Email queued", ['recipient' => $recipient]);
return true;
} catch (\Exception $e) {
$this->logger->error("Email failed", ['error' => $e->getMessage()]);
return false;
}
}
}
Cuando usamos $container->make(EmailNotifier::class), Laravel inyecta automáticamente Mailer, Queue y LoggerInterface.
Registrar la Factory en un Service Provider
Para que nuestra Factory esté disponible en toda la aplicación, la registramos en un Service Provider:
<?php
namespace App\Providers;
use App\Factories\NotifierFactory;
use Illuminate\Support\ServiceProvider;
class NotificationServiceProvider extends ServiceProvider
{
/**
* Registrar servicios en el container.
*/
public function register(): void
{
// Registrar la Factory como singleton
$this->app->singleton(NotifierFactory::class, function ($app) {
return new NotifierFactory($app);
});
}
/**
* Bootstrap de servicios (opcional).
*/
public function boot(): void
{
// Extender la factory con tipos personalizados si es necesario
$factory = $this->app->make(NotifierFactory::class);
if (config('notifications.enable_telegram')) {
$factory->extend('telegram', \App\Services\Notifications\TelegramNotifier::class);
}
if (config('notifications.enable_whatsapp')) {
$factory->extend('whatsapp', \App\Services\Notifications\WhatsAppNotifier::class);
}
}
}
No olvides registrar el provider en config/app.php:
'providers' => [
// ...
App\Providers\NotificationServiceProvider::class,
],
Factory Pattern vs Service Locator
Es crucial entender la diferencia entre el patrón Factory y el anti-patrón Service Locator.
Factory Pattern ✓ (Correcto)
<?php
namespace App\Services;
use App\Factories\NotifierFactory;
class NotificationService
{
public function __construct(
private NotifierFactory $factory // ✓ Dependencia explícita
) {}
public function sendWelcomeEmail(User $user): void
{
$notifier = $this->factory->make('email');
$notifier->send($user->email, "Bienvenido {$user->name}!");
}
public function sendSmsVerification(User $user, string $code): void
{
$notifier = $this->factory->make('sms');
$notifier->send($user->phone, "Tu código es: {$code}");
}
}
Por qué es correcto:
✓ La dependencia está declarada explícitamente en el constructor
✓ Es fácil de testear (puedes inyectar un mock)
✓ Es obvio qué dependencias tiene la clase
✓ Sigue el principio de inversión de dependencias (DIP)
Service Locator ✗ (Anti-patrón)
<?php
namespace App\Services;
class NotificationService
{
public function sendWelcomeEmail(User $user): void
{
// ✗ Dependencia oculta
$notifier = app()->make('notifier.email');
$notifier->send($user->email, "Bienvenido {$user->name}!");
}
public function sendSmsVerification(User $user, string $code): void
{
// ✗ Dependencia oculta
$notifier = resolve('notifier.sms');
$notifier->send($user->phone, "Tu código es: {$code}");
}
}
Por qué es un anti-patrón:
✗ Las dependencias están ocultas
✗ Difícil de testear (no puedes inyectar mocks fácilmente)
✗ Acoplamiento al framework (usa helpers específicos de Laravel)
✗ No es obvio qué dependencias necesita
La Regla de Oro
Siempre inyecta la Factory en el constructor. Nunca uses
app(),resolve()u otros Service Locators dentro de tus clases.
Excepción: En archivos de configuración, migrations, seeders o Service Providers, usar el container directamente está bien, ya que son puntos de entrada de la aplicación.
Abstract Factory: Familias de Objetos Relacionados
El Abstract Factory es un patrón más avanzado que permite crear familias de objetos relacionados sin especificar sus clases concretas.
Caso de Uso: Sistema Multi-Tenant con Diferentes Proveedores
Imagina que tienes una aplicación multi-tenant donde cada tenant puede usar diferentes proveedores para email, SMS y almacenamiento.
<?php
namespace App\Contracts;
/**
* Abstract Factory para crear familias de servicios.
*/
interface ServiceFactoryInterface
{
public function createEmailService(): EmailServiceInterface;
public function createSmsService(): SmsServiceInterface;
public function createStorageService(): StorageServiceInterface;
}
Implementaciones Concretas
Factory para AWS:
<?php
namespace App\Factories\ServiceProviders;
use App\Contracts\ServiceFactoryInterface;
use App\Services\Aws\SesEmailService;
use App\Services\Aws\SnsSmsService;
use App\Services\Aws\S3StorageService;
class AwsServiceFactory implements ServiceFactoryInterface
{
public function createEmailService(): EmailServiceInterface
{
return new SesEmailService(
config('services.aws.ses_key'),
config('services.aws.ses_secret')
);
}
public function createSmsService(): SmsServiceInterface
{
return new SnsSmsService(
config('services.aws.sns_key'),
config('services.aws.sns_secret')
);
}
public function createStorageService(): StorageServiceInterface
{
return new S3StorageService(
config('services.aws.s3_bucket')
);
}
}
Factory para Google Cloud:
<?php
namespace App\Factories\ServiceProviders;
use App\Contracts\ServiceFactoryInterface;
use App\Services\Google\GmailEmailService;
use App\Services\Google\FirebaseSmsService;
use App\Services\Google\CloudStorageService;
class GoogleServiceFactory implements ServiceFactoryInterface
{
public function createEmailService(): EmailServiceInterface
{
return new GmailEmailService(
config('services.google.gmail_credentials')
);
}
public function createSmsService(): SmsServiceInterface
{
return new FirebaseSmsService(
config('services.google.firebase_credentials')
);
}
public function createStorageService(): StorageServiceInterface
{
return new CloudStorageService(
config('services.google.storage_bucket')
);
}
}
Factory de Factories
Ahora necesitamos una Factory que decida qué Abstract Factory usar:
<?php
namespace App\Factories;
use App\Contracts\ServiceFactoryInterface;
use App\Factories\ServiceProviders\AwsServiceFactory;
use App\Factories\ServiceProviders\GoogleServiceFactory;
use App\Factories\ServiceProviders\AzureServiceFactory;
use InvalidArgumentException;
class ServiceProviderFactory
{
public function make(string $provider): ServiceFactoryInterface
{
return match ($provider) {
'aws' => new AwsServiceFactory(),
'google' => new GoogleServiceFactory(),
'azure' => new AzureServiceFactory(),
default => throw new InvalidArgumentException("Proveedor no soportado: {$provider}")
};
}
public function makeForTenant(int $tenantId): ServiceFactoryInterface
{
$provider = DB::table('tenants')
->where('id', $tenantId)
->value('service_provider');
return $this->make($provider);
}
}
Uso en un Servicio
<?php
namespace App\Services;
use App\Factories\ServiceProviderFactory;
class TenantService
{
public function __construct(
private ServiceProviderFactory $providerFactory
) {}
public function sendWelcomePackage(int $tenantId, User $user): void
{
// Obtener la factory específica del tenant
$factory = $this->providerFactory->makeForTenant($tenantId);
// Crear servicios de la familia correspondiente
$emailService = $factory->createEmailService();
$smsService = $factory->createSmsService();
// Usar los servicios
$emailService->send($user->email, 'Bienvenido!');
$smsService->send($user->phone, 'Gracias por registrarte');
}
}
Ventaja: Cambiar de proveedor es transparente. Todos los servicios (email, SMS, storage) cambian juntos de forma consistente.
Factory con Caché de Instancias
A veces quieres que una Factory reutilice instancias en lugar de crear una nueva cada vez (patrón Flyweight).
<?php
namespace App\Factories;
use App\Contracts\PaymentGatewayInterface;
use Illuminate\Contracts\Container\Container;
class CachedPaymentGatewayFactory
{
private array $gateways = [
'stripe' => \App\Services\Payments\StripeGateway::class,
'paypal' => \App\Services\Payments\PayPalGateway::class,
'mercadopago' => \App\Services\Payments\MercadoPagoGateway::class,
];
private array $instances = [];
public function __construct(
private Container $container
) {}
/**
* Obtener una instancia (cacheada).
*/
public function make(string $gateway): PaymentGatewayInterface
{
// Si ya existe, reutilizarla
if (isset($this->instances[$gateway])) {
return $this->instances[$gateway];
}
if (!isset($this->gateways[$gateway])) {
throw new InvalidArgumentException("Gateway no soportado: {$gateway}");
}
// Crear y cachear la instancia
$this->instances[$gateway] = $this->container->make($this->gateways[$gateway]);
return $this->instances[$gateway];
}
/**
* Crear una instancia fresca sin cachear.
*/
public function makeFresh(string $gateway): PaymentGatewayInterface
{
if (!isset($this->gateways[$gateway])) {
throw new InvalidArgumentException("Gateway no soportado: {$gateway}");
}
return $this->container->make($this->gateways[$gateway]);
}
/**
* Limpiar el caché.
*/
public function clearCache(): void
{
$this->instances = [];
}
}
Cuándo usar caché:
✓ Los objetos son stateless (sin estado)
✓ La creación es costosa (conexiones a APIs, inicializaciones pesadas)
✓ Quieres reutilizar conexiones (pools de conexiones)
Cuándo NO usar caché:
✗ Los objetos tienen estado mutable
✗ Necesitas instancias independientes por request
✗ Los objetos almacenan datos específicos del contexto
Factory con Configuración Externa
Puedes hacer que las Factories sean completamente configurables desde archivos de configuración:
Archivo de Configuración
<?php
// config/notifications.php
return [
'default' => env('NOTIFICATION_CHANNEL', 'email'),
'channels' => [
'email' => [
'driver' => \App\Services\Notifications\EmailNotifier::class,
'enabled' => true,
'queue' => true,
],
'sms' => [
'driver' => \App\Services\Notifications\SmsNotifier::class,
'enabled' => env('SMS_ENABLED', false),
'queue' => true,
],
'push' => [
'driver' => \App\Services\Notifications\PushNotifier::class,
'enabled' => env('PUSH_ENABLED', false),
'queue' => false,
],
'slack' => [
'driver' => \App\Services\Notifications\SlackNotifier::class,
'enabled' => env('SLACK_ENABLED', false),
'queue' => false,
],
],
];
Factory que Lee la Configuración
<?php
namespace App\Factories;
use App\Contracts\NotifierInterface;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
class ConfigurableNotifierFactory
{
private array $channels;
public function __construct(
private Container $container
) {
$this->channels = config('notifications.channels', []);
}
public function make(string $channel): NotifierInterface
{
if (!isset($this->channels[$channel])) {
throw new InvalidArgumentException("Canal no configurado: {$channel}");
}
$config = $this->channels[$channel];
if (!$config['enabled']) {
throw new InvalidArgumentException("Canal deshabilitado: {$channel}");
}
return $this->container->make($config['driver']);
}
public function makeDefault(): NotifierInterface
{
return $this->make(config('notifications.default'));
}
public function getEnabledChannels(): array
{
return array_keys(array_filter($this->channels, fn($c) => $c['enabled']));
}
public function isChannelEnabled(string $channel): bool
{
return $this->channels[$channel]['enabled'] ?? false;
}
}
Factory con Resolución por Convención
Puedes hacer que una Factory resuelva clases automáticamente siguiendo una convención de nombres:
<?php
namespace App\Factories;
use App\Contracts\ExporterInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Str;
class ExporterFactory
{
private string $namespace = 'App\\Services\\Exporters\\';
public function __construct(
private Container $container
) {}
/**
* Crear un exportador siguiendo la convención de nombres.
*
* Ejemplos:
* - 'pdf' -> App\Services\Exporters\PdfExporter
* - 'excel' -> App\Services\Exporters\ExcelExporter
* - 'csv' -> App\Services\Exporters\CsvExporter
*/
public function make(string $format): ExporterInterface
{
$className = $this->namespace . Str::studly($format) . 'Exporter';
if (!class_exists($className)) {
throw new InvalidArgumentException("Exportador no encontrado para formato: {$format}");
}
$instance = $this->container->make($className);
if (!$instance instanceof ExporterInterface) {
throw new InvalidArgumentException("{$className} debe implementar ExporterInterface");
}
return $instance;
}
}
Ventajas:
✓ No necesitas registrar cada tipo manualmente
✓ Agregar nuevos exportadores es automático
✓ Código más DRY
Desventajas:
✗ Menos explícito
✗ Depende de convenciones de nombres
✗ Errores en tiempo de ejecución si no se sigue la convención
Testing Avanzado con Factories
Mockear una Factory Completa
<?php
namespace Tests\Feature;
use App\Factories\NotifierFactory;
use App\Contracts\NotifierInterface;
use Tests\TestCase;
use Mockery;
class NotificationTest extends TestCase
{
public function test_sends_notification_via_factory()
{
// Crear un mock del notificador
$mockNotifier = Mockery::mock(NotifierInterface::class);
$mockNotifier->shouldReceive('send')
->once()
->with('[email protected]', 'Test message')
->andReturn(true);
// Crear un mock de la factory
$mockFactory = Mockery::mock(NotifierFactory::class);
$mockFactory->shouldReceive('make')
->with('email')
->once()
->andReturn($mockNotifier);
// Reemplazar la factory en el container
$this->app->instance(NotifierFactory::class, $mockFactory);
// Ejecutar el test
$response = $this->postJson('/notifications', [
'type' => 'email',
'recipient' => '[email protected]',
'message' => 'Test message'
]);
$response->assertOk();
}
}
Testing con Fakes de Laravel
Si tu Factory crea servicios de Laravel (Mail, Queue, etc.), puedes usar los fakes:
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use App\Factories\NotifierFactory;
use Tests\TestCase;
class EmailNotificationTest extends TestCase
{
public function test_email_notifier_queues_email()
{
Mail::fake();
Queue::fake();
$factory = app(NotifierFactory::class);
$notifier = $factory->make('email');
$notifier->send('[email protected]', 'Hello!');
Queue::assertPushed(/* ... */);
}
}
Mejores Prácticas Avanzadas
1. Usa Type Hints Estrictos
public function make(string $type): NotifierInterface
{
// El tipo de retorno garantiza que siempre devuelves la interfaz correcta
}
2. Valida Configuraciones en el Constructor
public function __construct(Container $container)
{
$this->container = $container;
$this->channels = config('notifications.channels');
// Validar que la configuración sea correcta
if (empty($this->channels)) {
throw new RuntimeException("No hay canales de notificación configurados");
}
}
3. Proporciona Métodos Helper Útiles
public function makeDefault(): NotifierInterface
{
return $this->make(config('notifications.default'));
}
public function makeMultiple(array $types): array
{
return array_map(fn($type) => $this->make($type), $types);
}
public function canMake(string $type): bool
{
return isset($this->notifiers[$type]);
}
4. Documenta Extensamente
/**
* Factory para crear instancias de notificadores.
*
* Esta factory soporta múltiples canales de notificación configurables
* mediante el archivo config/notifications.php. Utiliza el Container de
* Laravel para resolver automáticamente las dependencias.
*
* @example
* $notifier = $factory->make('email');
* $notifier->send('user@example.com', 'Hello!');
*
* @see \App\Contracts\NotifierInterface
*/
class NotifierFactory
{
// ...
}
5. Considera el Performance
// ✓ Bueno: Cachear configuración pesada
private array $notifiers;
public function __construct(Container $container)
{
$this->notifiers = config('notifications.channels');
}
// ✗ Malo: Leer configuración en cada llamada
public function make(string $type): NotifierInterface
{
$config = config('notifications.channels'); // ← Se ejecuta cada vez
// ...
}
6. Maneja Errores Apropiadamente
public function make(string $type): NotifierInterface
{
if (!isset($this->notifiers[$type])) {
throw new InvalidArgumentException(
"Tipo de notificador no soportado: {$type}. " .
"Tipos disponibles: " . implode(', ', array_keys($this->notifiers))
);
}
try {
return $this->container->make($this->notifiers[$type]);
} catch (BindingResolutionException $e) {
throw new RuntimeException(
"No se pudo crear el notificador {$type}: {$e->getMessage()}",
previous: $e
);
}
}
Comparación: Cuándo Usar Cada Técnica
| Técnica | Cuándo Usar | Complejidad |
|---|---|---|
| Factory Simple (new) | Objetos sin dependencias | Baja |
| Factory con Container | Objetos con dependencias | Media |
| Factory con Caché | Objetos costosos de crear | Media |
| Abstract Factory | Familias de objetos relacionados | Alta |
| Factory Configurable | Comportamiento variable por entorno | Media |
| Factory por Convención | Muchas implementaciones similares | Media-Alta |
Conclusión
El patrón Factory en Laravel puede ser tan simple o tan sofisticado como lo necesites. Hemos visto técnicas avanzadas que incluyen:
✓ Container de Laravel para inyección automática de dependencias
✓ Diferencia entre Factory y Service Locator (evita el anti-patrón)
✓ Abstract Factory para familias de objetos
✓ Caché de instancias para optimización
✓ Configuración externa para flexibilidad
✓ Resolución por convención para DRY
✓ Testing avanzado con mocks y fakes
Recuerda:
- Inyecta la Factory, no uses Service Locators
- Aprovecha el Container de Laravel para resolver dependencias
- Documenta bien tus factories y sus capacidades
- Elige la técnica apropiada según la complejidad de tu caso
- No sobre-ingenierices: usa la técnica más simple que resuelva tu problema
La maestría del patrón Factory viene de saber cuándo aplicar cada técnica y cuándo mantener las cosas simples.
Artículos Relacionados
- Patrón Factory en Laravel (Básico) - Fundamentos del patrón Factory
- Guía Completa de Principios SOLID - Los 5 principios fundamentales
- Principio de Inversión de Dependencias - DIP con Stripe
Happy coding! 🚀
C O M E N T A R I O S
Deja un comentario
Tu email no será publicado. Los campos marcados con * son obligatorios.
Cargando comentarios...
☕ ¿Te ha sido útil este artículo?
Apóyame con un café mientras sigo creando contenido técnico
☕ Invítame un café