vouchedfor/gocardless-enterprise-bundle
Symfony bundle that integrates the GoCardless Enterprise API for direct debit payments, giving you ready-made services and configuration to manage mandates, subscriptions, and payment flows from your Symfony application.
Know what you're actually adopting. Despite the Composer name, this is a Symfony 2 bundle, not a Laravel package. There is no ServiceProvider, no config/ publish tag, no facade, no auto-discovery. A Laravel app cannot boot a Symfony bundle's DependencyInjection/Extension — that only runs inside a Symfony Kernel. So "installing" it in Laravel means one of two things:
gocardless/gocardless, the legacy GoCardless_Client class) and replicate the bundle's service wiring in a Laravel service provider.Where to look first (in the repo):
DependencyInjection/Configuration.php — the real documentation of required config (app_id, app_secret, merchant_id, environment/sandbox, possibly redirect_uri).DependencyInjection/*Extension.php — how those values are mapped onto the GoCardless client constructor.Resources/config/services.yml — the service ID you'd otherwise inject; copy that argument order verbatim into Laravel.Bundle subclass worth ignoring.Minimal Laravel wiring, assuming the SDK is installed:
composer require gocardless/gocardless
// config/services.php
'gocardless' => [
'app_id' => env('GOCARDLESS_APP_ID'),
'app_secret' => env('GOCARDLESS_APP_SECRET'),
'merchant_id' => env('GOCARDLESS_MERCHANT_ID'),
'environment' => env('GOCARDLESS_ENV', 'sandbox'),
],
// app/Providers/GoCardlessServiceProvider.php
$this->app->singleton(\GoCardless_Client::class, fn () => new \GoCardless_Client([
'app_id' => config('services.gocardless.app_id'),
'app_secret' => config('services.gocardless.app_secret'),
'merchant_id' => config('services.gocardless.merchant_id'),
'environment' => config('services.gocardless.environment'),
]));
First use case worth proving out: fetch a merchant ($client->merchant->get()) in php artisan tinker against the sandbox, then create a single bill against a test pre-authorization. If that round-trips, the rest of the resource surface (bills, subscriptions, pre_authorizations, payouts, users) behaves the same way.
Wrap it behind your own interface immediately. The bundle is a dead end; the Enterprise API is a legacy API. Define a DirectDebitGateway contract (createBill, createSubscription, cancelSubscription, findPayout) and implement it with the legacy client. When you inevitably migrate to GoCardless Pro (gocardless/gocardless-pro, API v2), only the implementation changes.
interface DirectDebitGateway {
public function createBill(string $mandateId, int $amountPence, string $description): Bill;
}
Service provider as the only integration seam. Bind the raw client as a singleton, and bind your gateway on top of it. Never let GoCardless_Client leak into controllers or models — it's a static-ish, cURL-driven object with no testing hooks.
Webhook handling is the bulk of day-to-day work. Enterprise webhooks POST a payload parameter (JSON) to your endpoint. In Laravel:
// routes/api.php — outside the 'web' group so CSRF doesn't apply
Route::post('/webhooks/gocardless', [GoCardlessWebhookController::class, 'handle']);
Verify the Signature header against the raw request body ($request->getContent()) with hash_equals() before json_decode, then dispatch a job per resource. Idempotency is on you: record the resource id + event type in a unique index and drop duplicates, because GoCardless retries.
Resource-state reconciliation as a queued job. Don't trust the webhook to be your only source of truth. A nightly job that pulls payouts and bills for the last N days and upserts into your own tables saves you repeatedly during incident triage. Enterprise's list endpoints are simple and cheap.
Pre-authorization (mandate) flow. Redirect the customer to the GoCardless confirmation URL returned by the client, store the returned resource_id/pre_authorization id on redirect-back, then create bills against it. Keep the redirect-back route idempotent — customers double-submit.
Testing pattern. Because the client is constructed from a config array, you can swap the binding in tests:
$this->app->instance(DirectDebitGateway::class, new FakeDirectDebitGateway());
Reserve sandbox calls for a single, tagged integration test suite — not your default phpunit run.
symfony/symfony or symfony/framework-bundle to ~2.3–~2.8, which conflicts with the Symfony components Laravel 5+/6+ requires. Expect composer why-not to be your first command. Do not force it in with --ignore-platform-reqs; pull the SDK instead.gocardless/gocardless-pro.each(), curly-brace string offsets) if you target PHP 8.$request->all() re-encodes JSON and will break the HMAC. Read getContent() first, verify, then decode. Use hash_equals, never ==.GOCARDLESS_ENV typo reach production — assert on boot:abort_if(app()->isProduction() && config('services.gocardless.environment') !== 'live', 500);
Http facade logging won't see anything. Route through a local proxy (mitmproxy) or patch in a CURLOPT_VERBOSE handler temporarily; don't leave verbose cURL logging on — it prints auth headers.app_secret or full request signatures. Log the resource id and your own correlation id; you'll need them to reconcile with GoCardless support.error key. Normalise into a single GoCardlessException at the gateway boundary so callers have one thing to catch.config() inside the provider, never env() — otherwise php artisan config:cache silently returns null credentials in production.How can I help you explore Laravel packages today?