SOLID en Laravel (5/5): Principio de Inversión de Dependencias con Stripe
16 Dec 2025
10 min read
Introducción
Bienvenido al último artículo de nuestra serie SOLID en Laravel. Hoy exploraremos el Principio de Inversión de Dependencias (DIP), que establece:
“Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.”
“Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones.”
En términos simples: Depende de interfaces, no de clases concretas.
El Problema: Acoplamiento Directo
Veamos cómo muchos desarrolladores implementan la lógica de suscripciones acoplada directamente a Stripe:
<?php
namespace App\Services;
use Stripe\Stripe;
use Stripe\Customer;
use Stripe\Subscription;
use App\Models\User;
/**
* ❌ Servicio de alto nivel acoplado a implementación concreta (Stripe)
*/
class SubscriptionService
{
public function subscribe(User $user, string $planId, string $paymentMethod): bool
{
// Dependencia DIRECTA de la SDK de Stripe
Stripe::setApiKey(config('services.stripe.secret'));
try {
// Código fuertemente acoplado a Stripe
if (!$user->stripe_customer_id) {
$customer = Customer::create([
'email' => $user->email,
'name' => $user->name,
'payment_method' => $paymentMethod,
]);
$user->stripe_customer_id = $customer->id;
$user->save();
}
$subscription = Subscription::create([
'customer' => $user->stripe_customer_id,
'items' => [['price' => $planId]],
]);
$user->subscriptions()->create([
'stripe_subscription_id' => $subscription->id,
'status' => $subscription->status,
]);
return true;
} catch (\Stripe\Exception\ApiException $e) {
// Manejo de excepciones específico de Stripe
\Log::error('Stripe error: ' . $e->getMessage());
return false;
}
}
public function cancel(User $user): bool
{
// Más acoplamiento directo
Stripe::setApiKey(config('services.stripe.secret'));
$subscription = Subscription::retrieve($user->stripe_subscription_id);
$subscription->cancel();
return true;
}
}
Más Ejemplos de Acoplamiento
<?php
namespace App\Http\Controllers;
use App\Services\SubscriptionService;
use Stripe\Webhook;
class WebhookController extends Controller
{
public function handleStripe(Request $request)
{
// Controlador acoplado a Stripe
$signature = $request->header('Stripe-Signature');
try {
// Dependencia directa de SDK
$event = Webhook::constructEvent(
$request->getContent(),
$signature,
config('services.stripe.webhook_secret')
);
// Lógica de negocio mezclada con detalles de Stripe
if ($event->type === 'customer.subscription.deleted') {
$subscription = $event->data->object;
$user = User::where('stripe_customer_id', $subscription->customer)->first();
$user->subscription->update(['status' => 'cancelled']);
}
return response()->json(['success' => true]);
} catch (\Stripe\Exception\SignatureVerificationException $e) {
// Excepciones específicas de Stripe
return response()->json(['error' => 'Invalid signature'], 400);
}
}
}
¿Por qué esto viola DIP?
Problemas
- ❌ Imposible de testear: No puedes testear sin llamar a Stripe real
- ❌ Imposible cambiar de proveedor: Cambiar a PayPal requiere reescribir todo
- ❌ Lógica de negocio acoplada a infraestructura: Mezcla reglas de negocio con detalles técnicos
- ❌ Código frágil: Un cambio en Stripe rompe tu aplicación
- ❌ Violación de DIP: Módulo de alto nivel (
SubscriptionService) depende de bajo nivel (Stripe SDK)
La Solución: Inversión de Dependencias
Arquitectura con DIP
┌─────────────────────────────────────────┐
│ Capa de Aplicación (Alto Nivel) │
│ SubscriptionService, Controllers │
│ ↓ depende de ↓ │
│ (Interfaces/Contratos) │
│ PaymentGatewayInterface │
│ WebhookHandlerInterface │
│ ↑ implementan ↑ │
│ Capa de Infraestructura (Bajo Nivel) │
│ StripeGateway, PayPalGateway │
└─────────────────────────────────────────┘
1. Definir Abstracciones (Contratos)
<?php
namespace App\Contracts;
use App\ValueObjects\{PaymentResult, SubscriptionResult, Customer};
/**
* ✅ Abstracción de alto nivel
* Define QUÉ necesitamos, no CÓMO se implementa
*/
interface PaymentGatewayInterface
{
public function createCustomer(string $email, string $name): Customer;
public function createSubscription(
string $customerId,
string $planId,
string $paymentMethodId
): SubscriptionResult;
public function cancelSubscription(string $subscriptionId): bool;
}
interface WebhookHandlerInterface
{
public function verify(string $payload, string $signature): bool;
public function handle(string $payload): WebhookEvent;
}
interface NotificationServiceInterface
{
public function sendSubscriptionConfirmation(User $user): void;
public function sendCancellationNotice(User $user): void;
}
2. Implementar Detalles (Infraestructura)
<?php
namespace App\Infrastructure\Payment;
use App\Contracts\PaymentGatewayInterface;
use Stripe\Stripe;
use Stripe\Customer as StripeCustomer;
use Stripe\Subscription as StripeSubscription;
/**
* ✅ Implementación concreta (bajo nivel)
* Depende de la abstracción, no al revés
*/
class StripePaymentGateway implements PaymentGatewayInterface
{
public function __construct()
{
// Detalles de Stripe encapsulados aquí
Stripe::setApiKey(config('services.stripe.secret'));
}
public function createCustomer(string $email, string $name): Customer
{
try {
$stripeCustomer = StripeCustomer::create([
'email' => $email,
'name' => $name,
]);
// Convertir objeto de Stripe a nuestro dominio
return new Customer(
id: $stripeCustomer->id,
email: $stripeCustomer->email,
name: $stripeCustomer->name,
);
} catch (\Stripe\Exception\ApiException $e) {
// Convertir excepciones de Stripe a excepciones de dominio
throw new PaymentGatewayException(
"Failed to create customer: {$e->getMessage()}",
previous: $e
);
}
}
public function createSubscription(
string $customerId,
string $planId,
string $paymentMethodId
): SubscriptionResult {
try {
$subscription = StripeSubscription::create([
'customer' => $customerId,
'items' => [['price' => $planId]],
'default_payment_method' => $paymentMethodId,
]);
// Mapear a objetos de dominio
return new SubscriptionResult(
id: $subscription->id,
status: $subscription->status,
currentPeriodEnd: $subscription->current_period_end,
);
} catch (\Stripe\Exception\ApiException $e) {
throw new PaymentGatewayException(
"Failed to create subscription: {$e->getMessage()}",
previous: $e
);
}
}
public function cancelSubscription(string $subscriptionId): bool
{
try {
$subscription = StripeSubscription::retrieve($subscriptionId);
$subscription->cancel();
return true;
} catch (\Stripe\Exception\ApiException $e) {
throw new PaymentGatewayException(
"Failed to cancel subscription: {$e->getMessage()}",
previous: $e
);
}
}
}
3. Implementación Alternativa (PayPal)
<?php
namespace App\Infrastructure\Payment;
use App\Contracts\PaymentGatewayInterface;
/**
* ✅ Otra implementación de la misma abstracción
* Lógica de negocio NO cambia
*/
class PayPalPaymentGateway implements PaymentGatewayInterface
{
public function __construct(
private PayPalApiContext $apiContext
) {}
public function createCustomer(string $email, string $name): Customer
{
// Implementación específica de PayPal
// Pero retorna el MISMO tipo (Customer)
$paypalCustomer = $this->apiContext->createCustomer([
'email' => $email,
'name' => $name,
]);
return new Customer(
id: $paypalCustomer->id,
email: $email,
name: $name,
);
}
public function createSubscription(
string $customerId,
string $planId,
string $paymentMethodId
): SubscriptionResult {
// Lógica de PayPal completamente diferente
// Pero la INTERFAZ es la misma
}
// ... resto de métodos
}
4. Servicio de Aplicación (Alto Nivel)
<?php
namespace App\Services;
use App\Contracts\{PaymentGatewayInterface, NotificationServiceInterface};
use App\Models\User;
use App\Exceptions\SubscriptionException;
/**
* ✅ Servicio de alto nivel
* Depende solo de ABSTRACCIONES
*/
class SubscriptionService
{
public function __construct(
private PaymentGatewayInterface $paymentGateway,
private NotificationServiceInterface $notifier
) {
// Recibe INTERFACES, no implementaciones concretas
// No sabe si es Stripe, PayPal o cualquier otro
}
public function subscribe(User $user, string $planId, string $paymentMethodId): void
{
// Lógica de negocio pura, sin detalles técnicos
// 1. Crear cliente si no existe
if (!$user->payment_customer_id) {
$customer = $this->paymentGateway->createCustomer(
$user->email,
$user->name
);
$user->update(['payment_customer_id' => $customer->id]);
}
// 2. Crear suscripción
$subscription = $this->paymentGateway->createSubscription(
customerId: $user->payment_customer_id,
planId: $planId,
paymentMethodId: $paymentMethodId
);
// 3. Guardar en base de datos
$user->subscriptions()->create([
'external_id' => $subscription->id,
'status' => $subscription->status,
'current_period_end' => $subscription->currentPeriodEnd,
]);
// 4. Notificar usuario
$this->notifier->sendSubscriptionConfirmation($user);
}
public function cancel(User $user): void
{
$subscription = $user->activeSubscription;
if (!$subscription) {
throw new SubscriptionException('No active subscription found');
}
// Lógica de negocio independiente del gateway
$this->paymentGateway->cancelSubscription($subscription->external_id);
$subscription->update(['status' => 'cancelled']);
$this->notifier->sendCancellationNotice($user);
}
}
5. Configuración de Dependencias (Service Provider)
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\{PaymentGatewayInterface, NotificationServiceInterface};
use App\Infrastructure\Payment\StripePaymentGateway;
use App\Infrastructure\Notifications\EmailNotificationService;
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
// Configurar qué implementación usar
$this->app->bind(
PaymentGatewayInterface::class,
function ($app) {
// Cambiar implementación desde config
$gateway = config('payments.default_gateway');
return match($gateway) {
'stripe' => $app->make(StripePaymentGateway::class),
'paypal' => $app->make(PayPalPaymentGateway::class),
default => throw new \Exception("Unsupported gateway: {$gateway}")
};
}
);
$this->app->bind(
NotificationServiceInterface::class,
EmailNotificationService::class
);
}
}
6. Testing con Mocks
<?php
namespace Tests\Unit\Services;
use Tests\TestCase;
use App\Services\SubscriptionService;
use App\Contracts\{PaymentGatewayInterface, NotificationServiceInterface};
use App\Models\User;
use Mockery;
class SubscriptionServiceTest extends TestCase
{
public function test_subscribe_creates_customer_and_subscription()
{
// Mockear la INTERFAZ, no la implementación
$gateway = Mockery::mock(PaymentGatewayInterface::class);
$notifier = Mockery::mock(NotificationServiceInterface::class);
$gateway->shouldReceive('createCustomer')
->once()
->with('test@test.com', 'Test User')
->andReturn(new Customer('cus_123', 'test@test.com', 'Test User'));
$gateway->shouldReceive('createSubscription')
->once()
->andReturn(new SubscriptionResult('sub_123', 'active', now()->addMonth()));
$notifier->shouldReceive('sendSubscriptionConfirmation')
->once();
// Test sin tocar Stripe ni PayPal
$service = new SubscriptionService($gateway, $notifier);
$user = User::factory()->create();
$service->subscribe($user, 'plan_123', 'pm_123');
$this->assertDatabaseHas('subscriptions', [
'user_id' => $user->id,
'external_id' => 'sub_123',
]);
}
}
7. Uso en Controladores
<?php
namespace App\Http\Controllers;
use App\Services\SubscriptionService;
use Illuminate\Http\Request;
class SubscriptionController extends Controller
{
public function __construct(
private SubscriptionService $subscriptionService
// Recibe servicio de alto nivel
// No sabe nada de Stripe/PayPal
) {}
public function store(Request $request)
{
$validated = $request->validate([
'plan_id' => 'required|string',
'payment_method' => 'required|string',
]);
try {
// Código limpio y enfocado
$this->subscriptionService->subscribe(
auth()->user(),
$validated['plan_id'],
$validated['payment_method']
);
return response()->json(['success' => true]);
} catch (SubscriptionException $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}
Comparación
Antes (Violando DIP)
// Alto nivel depende de bajo nivel
class SubscriptionService
{
public function subscribe(...)
{
Stripe::setApiKey(...); // Stripe concreto
$customer = Customer::create(...); // Clase de Stripe
$subscription = Subscription::create(...); // Clase de Stripe
}
}
// Imposible de testear
public function test_subscribe()
{
// No puedes mockear Stripe sin paquetes adicionales
$service = new SubscriptionService();
$service->subscribe(...); // Llama a Stripe REAL
}
// Cambiar a PayPal = reescribir TODO
Después (Aplicando DIP)
// Alto nivel depende de abstracción
class SubscriptionService
{
public function __construct(
private PaymentGatewayInterface $gateway // Interfaz
) {}
public function subscribe(...)
{
$customer = $this->gateway->createCustomer(...);
$subscription = $this->gateway->createSubscription(...);
}
}
// Testing trivial
public function test_subscribe()
{
$mock = Mockery::mock(PaymentGatewayInterface::class);
$service = new SubscriptionService($mock);
// ...
}
// Cambiar a PayPal = cambiar 1 línea en config
Beneficios Reales
1. Cambiar de Proveedor
// En config/payments.php
'default_gateway' => env('PAYMENT_GATEWAY', 'stripe'),
// En .env
PAYMENT_GATEWAY=paypal // Cambiado en 1 segundo
2. Testing con Implementación Fake
<?php
namespace App\Testing\Fakes;
class FakePaymentGateway implements PaymentGatewayInterface
{
public array $createdCustomers = [];
public array $createdSubscriptions = [];
public function createCustomer(string $email, string $name): Customer
{
$customer = new Customer("fake_{$email}", $email, $name);
$this->createdCustomers[] = $customer;
return $customer;
}
public function assertCustomerCreated(string $email): void
{
$found = collect($this->createdCustomers)
->contains(fn($c) => $c->email === $email);
PHPUnit::assertTrue($found, "Customer {$email} was not created");
}
}
3. Múltiples Gateways Simultáneos
class MultiGatewayService implements PaymentGatewayInterface
{
public function __construct(
private PaymentGatewayInterface $primary,
private PaymentGatewayInterface $fallback
) {}
public function createSubscription(...): SubscriptionResult
{
try {
return $this->primary->createSubscription(...);
} catch (PaymentGatewayException $e) {
Log::warning('Primary gateway failed, using fallback');
return $this->fallback->createSubscription(...);
}
}
}
4. Logging Transparente
class LoggingPaymentGateway implements PaymentGatewayInterface
{
public function __construct(
private PaymentGatewayInterface $gateway,
private LoggerInterface $logger
) {}
public function createSubscription(...): SubscriptionResult
{
$this->logger->info('Creating subscription', [...]);
$result = $this->gateway->createSubscription(...);
$this->logger->info('Subscription created', ['id' => $result->id]);
return $result;
}
}
Casos de Uso Avanzados
A/B Testing de Gateways
class ABTestPaymentGateway implements PaymentGatewayInterface
{
public function __construct(
private PaymentGatewayInterface $gatewayA,
private PaymentGatewayInterface $gatewayB,
private float $ratioA = 0.5
) {}
public function createSubscription(...): SubscriptionResult
{
$gateway = (rand(0, 100) / 100) < $this->ratioA
? $this->gatewayA
: $this->gatewayB;
return $gateway->createSubscription(...);
}
}
Rate Limiting
class RateLimitedGateway implements PaymentGatewayInterface
{
public function __construct(
private PaymentGatewayInterface $gateway,
private RateLimiter $limiter
) {}
public function createSubscription(...): SubscriptionResult
{
if (!$this->limiter->attempt('payment', 10, 60)) {
throw new RateLimitException('Too many payment attempts');
}
return $this->gateway->createSubscription(...);
}
}
Conclusión
El Principio de Inversión de Dependencias es la culminación de SOLID:
- ✅ Código testeable: Mock interfaces, no implementaciones
- ✅ Código flexible: Cambiar implementaciones sin tocar lógica
- ✅ Código mantenible: Lógica de negocio separada de infraestructura
- ✅ Código escalable: Agregar funcionalidad sin modificar existente
Los 5 Principios SOLID Trabajando Juntos
- SRP: Cada clase tiene una responsabilidad
- OCP: Extiende sin modificar
- LSP: Implementaciones son intercambiables
- ISP: Interfaces pequeñas y específicas
- DIP: Depende de abstracciones
Resultado: Código profesional, mantenible y escalable.
Recursos Finales
Artículo anterior: SOLID en Laravel (4/5): Principio de Segregación de Interfaces
¡Gracias por seguir esta serie completa sobre SOLID en Laravel!
Serie Completa
☕ ¿Te ha sido útil este artículo?
Apóyame con un café mientras sigo creando contenido técnico
☕ Invítame un café