paymentsuite/paypal-express_checkout-bundle
A PaymentSuite bundle that adds PayPal Express Checkout support to PHP/Symfony applications, helping you configure and integrate express checkout payment flows into your existing payment stack with less boilerplate.
Treat this package as a reference implementation, not a dependency. It is a Symfony2 bundle: there is no Laravel service provider, no facade, no publishable config, no auto-discovered routes, and no migrations. Its last release was January 2021, it has 26 stars and zero dependents, so expect composer constraints and APIs from the Symfony 2/3/PHP 7 era.
If you want to evaluate it, do it in a throwaway Symfony app or by reading the monorepo, not by adding it to a production Laravel composer.json.
Look at these first, in this order:
composer.json of the split package: check the php, symfony/*, paymentsuite/payment-core-bundle, and Omnipay/Payum constraints. Run composer why-not paymentsuite/paypal-express_checkout-bundle against your Laravel app before wasting time.DependencyInjection/Configuration.php and ...Extension.php: exact config keys (API username, password, signature, sandbox flag, branding, etc.). Do not assume the key names.Resources/config/services.yml and Resources/config/routing.yml: service IDs, route names, and controller wiring.Controller/: the Express Checkout redirect and return flow.PaypalExpressCheckoutMethod or similar): it implements PaymentSuite's payment method interface and is invoked by payment-core-bundle.The first real use case is the classic PayPal Express Checkout flow:
SetExpressCheckout — create a token and redirect the buyer to PayPal.returnUrl.GetExpressCheckoutDetails — fetch payer and order details.DoExpressCheckoutPayment — capture the payment.In Laravel, reproduce that flow with omnipay/paypal rather than porting the bundle:
composer require omnipay/paypal
// config/services.php
'paypal' => [
'username' => env('PAYPAL_API_USERNAME'),
'password' => env('PAYPAL_API_PASSWORD'),
'signature' => env('PAYPAL_API_SIGNATURE'),
'sandbox' => env('PAYPAL_SANDBOX', true),
],
$gateway = Omnipay::create('PayPal_Express');
$gateway->setUsername(config('services.paypal.username'));
$gateway->setPassword(config('services.paypal.password'));
$gateway->setSignature(config('services.paypal.signature'));
$gateway->setTestMode(config('services.paypal.sandbox'));
$response = $gateway->purchase([
'amount' => $order->total,
'currency' => $order->currency,
'returnUrl' => route('paypal.return', $order),
'cancelUrl' => route('paypal.cancel', $order),
'description' => "Order #{$order->id}",
])->send();
if (! $response->isRedirect()) {
throw new PaymentException($response->getMessage());
}
return redirect($response->getRedirectUrl());
Wrapper service pattern (recommended). Put the gateway behind a small Laravel service so controllers and jobs do not touch Omnipay directly:
final class PaypalExpressCheckout
{
public function __construct(private GatewayInterface $gateway) {}
public function start(Order $order): string
public function complete(Request $request): PaymentResult
}
Bind it in a service provider and keep credentials in config/services.php. This is the closest equivalent to what the Symfony bundle gives you: one injectable payment method with a process()-style API.
Order correlation before redirect. Persist the order ID, amount, currency, and a unique attempt ID before sending the buyer to PayPal. On return, load the order by your own reference, not only by the PayPal token. Never trust amount, currency, or PayerID from the query string alone.
Complete the payment server-side. On the return route, call completePurchase() / DoExpressCheckoutPayment, verify ACK is Success or SuccessWithWarning, verify the amount and currency against your stored order, then transition the order. A return URL is not proof of payment.
Mirror PaymentSuite's event model. The bundle integrates with payment-core-bundle through PaymentMethodManager and events such as payment.success, payment.failed, and payment.cancelled. In Laravel, publish your own domain events (OrderPaid, PaymentFailed, PaymentCancelled) from the completion service and let listeners handle fulfillment, email, and stock. That keeps the same separation the Symfony bundle is trying to give you.
Symfony interoperability pattern. If you have an existing Symfony/PaymentSuite application and are adding a Laravel service beside it, treat the bundle's routes, config keys, and event payloads as a contract. Share order IDs and transaction references through the database, not through the bundle's container. Do not try to boot a Symfony kernel inside Laravel to reuse the bundle.
Testing workflow. Use PayPal Sandbox business and personal accounts. Expose your local return URL with php artisan serve plus a tunnel (ngrok, Expose, or Sail + tunnel). Test at least: successful payment, buyer cancellation, double-hitting the return URL, expired token, and amount mismatch. Log the raw NVP request/response with credentials redacted.
Idempotency. DoExpressCheckoutPayment can be retried, but duplicate captures and duplicate "order paid" listeners are common. Store the PayPal transaction ID with a unique constraint and guard state transitions in a transaction.
ServiceProvider, no facade, no config/ publish, no route registration, no Http kernel integration. Any installation instructions you find are Symfony YAML and will not apply.HttpFoundation vs Laravel's request/response. The bundle's controllers type-hint Symfony Request/Response and use Symfony sessions and flash bags. Those are not interchangeable with Illuminate\Http\Request and Laravel's session.EventDispatcher and event names do not map to Laravel's dispatcher. You will rewrite the event wiring, not reuse it.composer why-not before attempting a port.srmklive/laravel-paypal, direct REST calls, or omnipay/paypal if you specifically need Express Checkout. Do not start a new integration on NVP just because this bundle exists.DoExpressCheckoutPayment unless you commit earlier. If you set commit=true on SetExpressCheckout, the buyer sees "Pay Now" and the amount is fixed earlier. If not, you must set and verify the final amount at capture time. Never let the client decide it.NOSHIPPING, SOLUTIONTYPE=Mark, LANDINGPAGE=Billing, BRANDNAME, and locale settings all change the PayPal experience and can silently break address collection.token and PayerID in the URL are inputs to GetExpressCheckoutDetails, not proof of payment. Always call PayPal and verify ACK and the amount server-side.getRequest()->getData() and getResponse()->getData() with secrets redacted. Most failures are credential, signature, sandbox/live mismatch, or currency not enabled on the account.paymentsuite/paymentsuite; read it alongside paymentsuite/payment-core-bundle, and file issues in the monorepo. The split package alone can be misleading about dependencies and configuration.How can I help you explore Laravel packages today?