Portal Payment Driver Manager
Contract for registering named hosted-checkout drivers, resolving the configured portal driver, and rejecting unknown driver names.
Overview
App\Services\Payments\PortalPaymentDriverManager is the customer-portal payment seam that maps a driver name such as stripe onto a concrete App\Contracts\Payments\PortalPaymentDriver implementation.
Core binds the manager as a singleton in AppServiceProvider. PortalPaymentService resolves the configured driver through it when opening a hosted checkout, and PortalPaymentWebhookController resolves the named driver when verifying an inbound provider webhook.
As a factual note about the current integration, PluginRegistrar::paymentDriver() forwards named driver registrations into this manager; this is not SDK usage guidance.
Public surface
| Method | Purpose |
|---|---|
register() |
Warehouse or replace the driver class for one name |
driver() |
Resolve a named driver or the configured default driver |
has() |
Report whether a name is currently registered |
availableDrivers() |
Return the currently registered driver names |
Accepted contract and identity
Each registration is a pair of:
- a string driver name
- a
class-string<App\Contracts\Payments\PortalPaymentDriver>
The manager stores the class name and resolves it through the container when driver() is called. Driver implementations may therefore depend on other services through normal container construction.
The class-string<PortalPaymentDriver> requirement is a PHPDoc and static-analysis contract. register() stores the class string without instantiating or runtime-checking it. Container construction failures or a class that does not satisfy the PortalPaymentDriver return type surface only when driver() resolves that registration.
Every driver implements the complete checkout contract:
key(): string
label(): string
createCheckout(Invoice $invoice, Money $amount, string $returnUrl, string $cancelUrl, array $metadata): CheckoutRedirectData
verifyWebhook(Request $request): ?CompletedCheckoutData
key()is the stable machine key the driver is registered and configured under. The manager does not require the registration key to equalPortalPaymentDriver::key().label()is the human-readable name shown in settings and the portal.createCheckout()receives the invoice, aBrick\Money\Moneyamount, the return and cancel URLs, and aarray<string, string>metadata map carrying the invoice id, portal token, and deposit flag. It returns aCheckoutRedirectDatadescribing where to send the customer.verifyWebhook()returns aCompletedCheckoutDatafor a completion event,nullwhen the payload is authentic but not a completion, and must throw when the signature or payload cannot be verified. The manager does not translate driver exceptions.
Optional embedded checkout
A driver may additionally implement App\Contracts\Payments\EmbeddablePortalPaymentDriver, which extends the base contract with one method:
createPaymentIntent(Invoice $invoice, Money $amount, array $metadata): PaymentIntentData
createPaymentIntent()receives the same invoice,Brick\Money\Moneyamount andarray<string, string>metadata map ascreateCheckout(), and returns aPaymentIntentDatacarrying the client secret, the provider reference, and the publishable key the browser needs to mount an in-page payment form.
This interface is deliberately optional. The manager never requires it, and a driver that does not implement it is not deficient — PortalPaymentService checks with instanceof and falls back to the hosted redirect, so plugin drivers written against the base contract keep working unchanged. Embedded checkout additionally requires the payments.portal_checkout_mode setting to be embedded; it defaults to hosted.
Drivers implementing this interface must also decode the provider's payment-intent completion event in verifyWebhook(), returning the same CompletedCheckoutData shape as a hosted completion so that reconciliation stays unaware of which shape was used.
Current core registrations
config/payments.php currently ships with:
| Name | Class |
|---|---|
stripe |
App\Services\Payments\Drivers\StripeCheckoutDriver |
fake |
App\Services\Payments\Drivers\FakePortalPaymentDriver |
The default selection comes from config('payments.portal_driver', 'stripe'). The testing environment defaults that config value to fake, so no test reaches a payment provider.
Default selection, availability, and collisions
The manager captures payments.drivers once, when the singleton manager is constructed. Later changes to that config key do not rebuild its private map; register() mutates the captured map directly. Driver names and their insertion order in that map are what availableDrivers() returns.
Calling driver() with no argument resolves the default configured driver:
$name ??= (string) config('payments.portal_driver', 'stripe');
Calling driver('fake') bypasses the default and resolves that named driver directly.
Unlike the driver map, payments.portal_driver is read each time driver() is called, so changing the configured default after manager construction affects the next unnamed lookup. After choosing a class, the manager asks the container on every successful lookup and does not cache driver objects itself. The container binding for that class therefore determines whether repeated lookups return transient or shared instances.
availableDrivers() returns the current registry keys. There is no extra filtering for environment support or for whether a provider is credentialed; it simply reports what has been registered. has() answers the same question for a single name and never resolves the class.
register() stores drivers in an associative array keyed by name. Registering the same name again replaces the earlier driver class. Duplicate names are not rejected.
Failure behaviour
If driver() cannot find the requested name, it throws InvalidArgumentException with:
Unknown portal payment driver [name].
The manager does not fall back from an unknown explicit name to the configured default, and it does not consult payments.portal_payments_enabled; that master switch is enforced by PortalPaymentService, not by driver resolution.
Current consumers
The main framework consumer is PortalPaymentService, which resolves the configured driver when opening a hosted checkout for a portal document. PortalPaymentWebhookController resolves a named driver when verifying inbound provider webhooks.
The inventory also records PluginRegistrar as a current consumer because that is how plugins register additional named drivers today.
Worked example
These are the current behaviours exercised in tests:
config(['payments.portal_driver' => 'fake']);
$default = app(PortalPaymentDriverManager::class)->driver(); // FakePortalPaymentDriver
$named = app(PortalPaymentDriverManager::class)->driver('stripe'); // StripeCheckoutDriver
$registered = app(PortalPaymentDriverManager::class)->has('stripe'); // true
$drivers = app(PortalPaymentDriverManager::class)->availableDrivers(); // ['stripe', 'fake', ...]
If the name does not exist, driver('missing-driver') throws instead of silently choosing another provider.