freddyhaddad/paypal-ipn-bundle
A Symfony bundle for validating PayPal Instant Payment Notifications (IPN), with configurable listener services to verify payments and handle confirmed or invalid IPN callbacks in your application.
This is a Symfony2-era bundle (last touched around the Symfony 2.x/3.x transition), so the first decision is whether it fits your stack at all. On Symfony 4+ it will not autowire cleanly without shims; on a legacy 2.8/3.4 app it drops in almost unchanged.
Minimal steps:
composer require freddyhaddad/paypal-ipn-bundle (expect to pin "*@dev" or a specific tag; the package has effectively no release cadence).app/AppKernel.php:
new FreddyHaddad\PaypalIpnBundle\PaypalIpnBundle(),
# app/config/routing.yml
paypal_ipn:
resource: "@PaypalIpnBundle/Resources/config/routing.yml"
prefix: /paypal
app/config/config.yml, then run the bundle's migration or doctrine:schema:update if it ships an IPN log entity.Where to look first: the bundle's controller and its IPN entity/repository. The controller is the listener PayPal hits; the entity shows you exactly which raw fields are persisted before you decide what to do with them.
First use case: point the PayPal Sandbox IPN simulator at https://your-host/paypal/ipn (or your ngrok URL), send a test Completed payment, and confirm a row lands in the IPN log table. That single round trip validates routing, Doctrine mapping, and outbound verification against PayPal in one shot.
Listener endpoint as the only entry point. Let the bundle's route be the URL you register in PayPal's account profile (or per-transaction notify_url). Don't build your own controller until you've confirmed the bundle's verification path works — it re-POSTs the raw body back to PayPal with cmd=_notify-validate, which is the part most people get wrong by hand.
Log-then-process. Treat the bundle as a durable inbox: it writes the raw IPN, you act on it afterward. A typical flow:
// inside your own service, triggered after the IPN row is persisted
if ($ipn->getPaymentStatus() !== 'Completed') {
return; // handle Refunded/Reversed/Pending separately
}
if ($this->orders->findByTxnId($ipn->getTxnId())) {
return; // idempotency: PayPal will retry
}
$this->orders->markPaid($ipn->getCustom(), $ipn->getMcGross(), $ipn->getMcCurrency());
Reconcile against your own records, never trust the payload alone. Verify receiver_email/business matches your PayPal account, mc_gross and mc_currency match the expected order total, and that custom maps to a real pending order. The bundle gives you the fields; the comparison is yours.
Bridge to domain events. Persist the IPN, then dispatch an application event (OrderPaid, PaymentRefunded) so controllers and mailers stay decoupled from the bundle. Keep the IPN row ID in the event so you can trace back to the raw payload during support.
Fast 200, slow work elsewhere. PayPal expects a prompt 200 OK and will retry on non-2xx or timeouts. Do the persistence and verification inline, then push anything heavy (inventory, emails, ERP sync) onto a queue or a kernel.terminate listener.
Sandbox and live in parallel. Run the bundle in sandbox mode against www.sandbox.paypal.com in dev/staging with the IPN simulator, and flip the endpoint to www.paypal.com in production via environment-specific config. Keep separate log tables or tag rows by environment so sandbox noise never pollutes real reconciliation.
application/x-www-form-urlencoded, not JSON. Read from $request->request, not $request->getContent() decoded as JSON. If you proxy through nginx, ensure the body isn't consumed before PHP sees it.INVALID response. If you rewrite the controller, capture php://input verbatim.txn_id (or txn_id + payment_status) and treat insert conflicts as success.form_login redirect, no CSRF token, and no locale/session middleware that could 302 the request. PayPal treats a redirect as failure.VERIFIED → payment_status handled → receiver_email matches → amount/currency match → txn_id unique. Log each step with the IPN row ID.postPersist for the entity if you need a hook without patching vendor code.How can I help you explore Laravel packages today?