paymentsuite/paypal-web-checkout-bundle
Symfony bundle for adding PayPal Web Checkout to PHP applications. It provides configuration and service integration for initiating PayPal checkout payments and handling responses, helping developers implement PayPal payment flows with less boilerplate and consistent PaymentSuite wiring.
The bundle is a thin Symfony 2.x integration around the PayPal Express Checkout (redirect) flow, built on top of paymentsuite/payment-core-bundle. Treat it as legacy glue rather than a self-contained payment SDK.
composer require paymentsuite/paypal-web-checkout-bundle. This will pull in paymentsuite/payment-core-bundle and the underlying PayPal library. Expect PHP 5.x / Symfony 2.3–2.8 constraints — on any modern Symfony/PHP you will need to fork or vendor-patch the composer.json.app/AppKernel.php:
new PaymentSuite\PaypalWebCheckoutBundle\PaypalWebCheckoutBundle(),
The core bundle (PaymentSuite\PaymentCoreBundle\PaymentCoreBundle) must be registered before it.DependencyInjection/Configuration.php and DependencyInjection/PaypalWebCheckoutExtension.php. Every config key (business, username, password, signature, sandbox, etc.) is defined there — do not guess from the README, the README is largely a stub.config.yml, using sandbox values from developer.paypal.com (Classic API → API Signature):
paymentsuite_paypal_web_checkout:
business: "[email protected]"
username: "…"
password: "…"
signature: "…"
sandbox: true
debug: true
SetExpressCheckout/DoExpressCheckoutPayment — the bundle handles the redirect and the DoExpressCheckoutPayment call for you.The whole day-to-day surface is: one PaymentMethod service, three routes (process/success/fail), and a PaymentBridge that you own.
1. Implement your PaymentBridge. This is the only class you write. The core bundle needs a bridge to convert your order into a monetary amount and to persist the transaction result:
use PaymentSuite\PaymentCoreBundle\PaymentBridge\PaymentBridgeInterface;
class OrderPaymentBridge implements PaymentBridgeInterface
{
public function getAmount() { return $this->order->getTotal(); }
public function getCurrency() { return 'EUR'; }
public function getTransactionId() { return $this->order->getPaypalToken(); }
}
Register the bridge as a tagged service (paymentsuite.payment.bridge in Symfony 2.x).
2. Drive the flow through events. The bundle emits paymentsuite.payment.success / paymentsuite.payment.failed events from the success/fail controllers. Subscribe to them instead of putting business logic in controllers:
public function onPaymentSuccess(PaymentOrderSuccessEvent $event)
{
$this->orderManager->markPaid($event->getPaymentBridge()->getTransactionId());
}
This is the only reliable extension point the bundle exposes — keep the controller thin, do the work in a listener.
3. Support multiple payment methods. The core bundle is designed for several methods side by side; register PayPal as one PaymentMethod, gate it behind a config flag, and branch on the PaymentMethod chosen in your checkout controller. Don't couple your order entity to PayPal directly.
4. Server-side redirect only. The bundle never exposes a JS SDK — the button is a plain form POST to your own route, and the bundle returns a RedirectResponse to paypal.com. This is fine for legacy integration but means no on-page card fields, no Apple Pay, no SCA-aware flows.
5. Test loop: sandbox buyer account (paypal.com → Developer → Sandbox accounts) → your process route → approve → your success URL. Use ngrok or a publicly reachable sandbox host — PayPal must be able to redirect back to you.
paypal/paypal-checkout-sdk instead. Only reach for this if you are maintaining an existing Symfony 2 shop.sandbox: true is a footgun in production. It silently redirects to sandbox.paypal.com, and payments look "successful" in tests only. Make it environment-driven (%kernel.debug%-adjacent), and assert it is false in your deploy checklist.username/password/signature come from the Classic API Signature, not the REST client ID/secret. Mixing these up yields Authentication/Authorization Failed (10002) on the first SetExpressCheckout call.TOKENs expire after ~15 minutes and are single-use. If the user sits on the PayPal page too long, the success callback arrives with an already-consumed token. Handle 10415/10416 gracefully and re-initiate rather than marking the order failed.DoExpressCheckoutPayment. It does not subscribe to IPN. Without an IPN listener of your own, you cannot reconcile orders where the user closes the browser after paying — you will lose sales.getAmount() is passed straight through; PayPal expects a decimal string with 2 places and rejects mismatches between SetExpressCheckout, GetExpressCheckoutDetails, and DoExpressCheckoutPayment. Round once, in one place, in getAmount().debug: true and inspect the bundle's logger channel; it dumps the raw NVP request/response. The single most useful thing during integration is the raw ACK/L_LONGMESSAGE0 pair — the exception messages are otherwise generic.PaymentBridge and overriding route names in config. If you need fee breakdown, shipping callbacks, or multi-currency, you will be patching the vendored bundle.How can I help you explore Laravel packages today?