Skip to content

StripeGateway

Viames Marino edited this page Apr 22, 2026 · 2 revisions

Pair framework: StripeGateway

Pair\Services\StripeGateway wraps common Stripe payment operations while keeping Stripe as an optional provider.

The Pair core does not require the Stripe SDK. Install it only in applications or extension packages that need payments:

composer require stripe/stripe-php

Configuration

STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_API_VERSION=

Keys:

  • STRIPE_SECRET_KEY is required when the default SDK client is created.
  • STRIPE_WEBHOOK_SECRET is required only for webhook signature verification.
  • STRIPE_API_VERSION is optional. Leave it empty to use the Stripe account default.

Extension path

Pair v4 integrations should be registered explicitly. A Stripe package or application bootstrap can expose the gateway as the payments adapter:

use Pair\Core\AdapterKeys;
use Pair\Core\Application;
use Pair\Services\StripeGateway;

$app = Application::getInstance();
$app->setAdapter(AdapterKeys::PAYMENTS, new StripeGateway());

$payments = $app->adapter(AdapterKeys::PAYMENTS, StripeGateway::class);

This keeps provider SDKs optional and avoids automatic discovery.

Constructor

__construct(?object $client = null, ?string $webhookSecret = null)

Creates the gateway with an injected Stripe-compatible client or a default StripeClient configured from Env.

Injecting a client is useful for tests or for extension packages that decorate the Stripe SDK.

Main methods

  • createCheckoutSession(array $lineItems, string $successUrl, string $cancelUrl, array $options = []): string
  • createHostedCheckoutSession(array $lineItems, string $successUrl, string $cancelUrl, array $options = []): array
  • createEmbeddedCheckoutSession(array $lineItems, string $returnUrl, array $options = []): array
  • createSubscriptionCheckoutSession(array $lineItems, string $successUrl, string $cancelUrl, array $options = []): array
  • createCustomerPortalSession(string $customerId, string $returnUrl, array $options = []): string
  • createPaymentIntent(int $amountInMinorUnits, string $currency, array $options = []): array
  • capture(string $paymentIntentId): array
  • refund(string $paymentIntentId, ?int $amountInMinorUnits = null): array
  • constructWebhookEvent(string $payload, string $signature): Stripe\Event
  • constructWebhookEventFromRequest(?string $payload = null, ?string $signature = null): Stripe\Event
  • webhookResponse(string $payload, string $signature, array $handlers = []): JsonResponse
  • webhookResponseFromEvent(object $event, array $handlers = []): JsonResponse
  • toMinorUnits(float $amount): int

Hosted Checkout

Use hosted Checkout when Stripe should own the payment page and redirect back to the application.

use Pair\Http\JsonResponse;
use Pair\Services\StripeGateway;

$gateway = new StripeGateway();

$session = $gateway->createHostedCheckoutSession(
	[
		['price' => 'price_123', 'quantity' => 1],
	],
	'https://example.test/billing/success',
	'https://example.test/billing/cancel',
	[
		'metadata' => ['order_id' => 'order_123'],
		'idempotency_key' => 'checkout_order_123',
	]
);

return new JsonResponse([
	'checkout_url' => $session['url'],
]);

createCheckoutSession() is kept as the backward-compatible shortcut that returns only the hosted session URL.

Embedded Checkout

Use embedded Checkout when the frontend renders Stripe Checkout inside the application.

use Pair\Http\JsonResponse;
use Pair\Services\StripeGateway;

$gateway = new StripeGateway();

$session = $gateway->createEmbeddedCheckoutSession(
	[
		['price' => 'price_123', 'quantity' => 1],
	],
	'https://example.test/billing/return?session_id={CHECKOUT_SESSION_ID}',
	[
		'redirect_on_completion' => 'if_required',
		'idempotency_key' => 'embedded_order_123',
	]
);

return new JsonResponse([
	'client_secret' => $session['client_secret'],
]);

Subscriptions

Use createSubscriptionCheckoutSession() for subscription prices. It forces Stripe Checkout mode=subscription.

use Pair\Services\StripeGateway;

$gateway = new StripeGateway();

$session = $gateway->createSubscriptionCheckoutSession(
	[
		['price' => 'price_monthly_123', 'quantity' => 1],
	],
	'https://example.test/billing/success',
	'https://example.test/billing/cancel',
	[
		'customer' => 'cus_123',
		'idempotency_key' => 'subscription_customer_123',
	]
);

Customer Portal

Use Customer Portal Sessions when customers need to manage payment methods, invoices, and subscriptions through Stripe.

use Pair\Http\JsonResponse;
use Pair\Services\StripeGateway;

$gateway = new StripeGateway();

$portalUrl = $gateway->createCustomerPortalSession(
	'cus_123',
	'https://example.test/account',
	[
		'idempotency_key' => 'portal_customer_123',
	]
);

return new JsonResponse([
	'portal_url' => $portalUrl,
]);

PaymentIntent with idempotency

Amounts must be expressed in minor units. For example, 4990 means 49.90 EUR.

use Pair\Services\StripeGateway;

$gateway = new StripeGateway();

$intent = $gateway->createPaymentIntent(
	StripeGateway::toMinorUnits(49.90),
	'eur',
	[
		'description' => 'Order #123',
		'metadata' => ['order_id' => 'order_123'],
		'idempotency_key' => 'payment_order_123',
	]
);

When no idempotency_key is provided for PaymentIntent and Checkout creation, Pair generates one for the Stripe request. Project code should still prefer deterministic keys for retryable domain operations.

Webhooks

Use webhookResponse() when the controller receives a raw payload and the Stripe-Signature header.

use Pair\Services\StripeGateway;

$gateway = new StripeGateway();

return $gateway->webhookResponse(
	(string)file_get_contents('php://input'),
	(string)($_SERVER['HTTP_STRIPE_SIGNATURE'] ?? ''),
	[
		'checkout.session.completed' => function (object $event): void {
			$session = $event->data->object ?? null;

			if (!$session) {
				return;
			}

			// Persist domain-side payment state here.
		},
	]
);

webhookResponse() verifies the signature through Stripe before invoking a handler. Use webhookResponseFromEvent() only after the event has already been verified.

Operational notes

  • Keep Stripe secrets in .env, never in Git.
  • Store payment state in project tables, not inside the gateway.
  • Use deterministic idempotency keys for retries tied to orders, invoices, or subscriptions.
  • Webhook handlers should be safe to run more than once because Stripe may retry deliveries.
  • Keep provider-specific domain mapping in application services instead of controllers.

Stripe references

See also: API, Integrations, AdapterRegistry, Options.

Clone this wiki locally