sudiptpa/paypal-notifications 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

sudiptpa/paypal-notifications

Composer 安装命令:

composer require sudiptpa/paypal-notifications

包简介

Framework-agnostic PayPal Notifications SDK for webhook signature verification and legacy Instant Payment Notification verification.

README 文档

README

Framework-agnostic PHP SDK for PayPal Webhooks and legacy Instant Payment Notification (IPN) verification.

CI Latest Release Packagist Downloads PHP License

Sponsor

If this package has been useful to you, GitHub Sponsors is a simple way to support ongoing maintenance, improvements, and future releases.

Table of Contents

At a Glance

Usage:

  • Verify PayPal webhook signatures before processing events.
  • Parse webhook payloads into typed event models.
  • Route events using clear type-based handlers.
  • Process webhooks with idempotency protection and structured outcomes.
  • Verify legacy Instant Payment Notification (IPN) during migration windows.

Benefits:

  • Security-first verification aligned with PayPal’s flow.
  • Typed models and typed exceptions for safer handling.
  • Retry controls for transient verification failures.
  • Minimal dependencies with framework-agnostic design.
  • Backward-compatible event API paths for smoother upgrades.

Why This Package

This SDK is for PHP projects that need reliable PayPal notification verification without framework lock-in.

Design goals:

  • resource-based API aligned with PayPal docs
  • minimal hard dependencies
  • explicit models and enums
  • transport extensibility
  • safe production defaults

Features

  • Webhook signature verification using PayPal verify-webhook-signature API
  • Case-insensitive extraction of required PayPal headers:
    • PAYPAL-TRANSMISSION-ID
    • PAYPAL-TRANSMISSION-TIME
    • PAYPAL-TRANSMISSION-SIG
    • PAYPAL-CERT-URL
    • PAYPAL-AUTH-ALGO
  • OAuth client credentials flow with in-memory token caching
  • Typed event parsing with mapped models for:
    • PAYMENT.CAPTURE.COMPLETED
    • PAYMENT.CAPTURE.DENIED
    • PAYMENT.CAPTURE.REFUNDED
    • PAYMENT.CAPTURE.PENDING
    • PAYMENT.CAPTURE.REVERSED
    • CHECKOUT.ORDER.APPROVED
    • CHECKOUT.ORDER.COMPLETED
    • CUSTOMER.DISPUTE.CREATED
    • CUSTOMER.DISPUTE.RESOLVED
    • BILLING.SUBSCRIPTION.CREATED
    • BILLING.SUBSCRIPTION.CANCELLED
    • BILLING.SUBSCRIPTION.ACTIVATED
    • BILLING.SUBSCRIPTION.SUSPENDED
    • BILLING.SUBSCRIPTION.EXPIRED
    • BILLING.SUBSCRIPTION.PAYMENT.FAILED
    • PAYMENT.PAYOUTSBATCH.SUCCESS
    • PAYMENT.PAYOUTS-ITEM.SUCCEEDED
    • PAYMENT.PAYOUTS-ITEM.DENIED
  • Unknown event fallback (UnknownWebhookEvent) for forward compatibility
  • Event router helper for clean application handlers
  • Framework adapter contract for framework-specific request bridges
  • High-level WebhookProcessor with structured processing result (easy to log and monitor)
  • Idempotency guard support for duplicate event prevention
  • Optional persisted OAuth token caches (file or Redis-style key-value adapter)
  • Optional persistent idempotency stores (Redis-style key-value adapter and app-managed database stores)
  • Configurable retry strategy for transient verification failures
  • Legacy Instant Payment Notification verification (cmd=_notify-validate)
  • Native cURL transport included (CurlTransport)
  • Custom transport support via TransportInterface
  • Strict exception model and safe error handling

Requirements

  • PHP ^8.2 (supports up to <8.6)
  • ext-json
  • ext-curl

Installation

composer require sudiptpa/paypal-notifications

Quick Start

<?php

declare(strict_types=1);

use Sujip\PayPal\Notifications\Config\ClientConfig;
use Sujip\PayPal\Notifications\Config\Environment;
use Sujip\PayPal\Notifications\PayPalClient;
use Sujip\PayPal\Notifications\Transport\CurlTransport;

$client = new PayPalClient(
    config: new ClientConfig(
        clientId: $_ENV['PAYPAL_CLIENT_ID'],
        clientSecret: $_ENV['PAYPAL_CLIENT_SECRET'],
        webhookId: $_ENV['PAYPAL_WEBHOOK_ID'],
        environment: Environment::Sandbox,
        maxWebhookTransmissionAgeSeconds: 300,
        allowedWebhookClockSkewSeconds: 30,
        strictPayPalCertUrlValidation: true,
    ),
    transport: new CurlTransport(),
);

Release Highlights (v1.0.0)

  • PayPal Webhooks signature verification with PayPal-aligned request fields and validation flow.
  • Legacy Instant Payment Notification support maintained for migration-safe integrations.
  • Typed webhook events and enum-driven routing helpers for cleaner handlers.
  • Idempotency guard support to reduce duplicate webhook side effects.
  • Replay-window and cert URL policy controls for stronger production security.
  • Framework-agnostic adapters for request extraction without framework lock-in.
  • Native cURL transport plus extension interface for custom HTTP stacks.
  • CI-validated on PHP 8.2, 8.3, 8.4, and 8.5.

Webhook Verification (Simple)

<?php

declare(strict_types=1);

use Sujip\PayPal\Notifications\Webhook\VerifyWebhookSignatureRequest;

$rawBody = file_get_contents('php://input') ?: '';
$headers = function_exists('getallheaders') ? getallheaders() : $_SERVER;

$request = $client->webhooks()->requestFromRawPayload($rawBody, $headers);
$result = $client->webhooks()->verifySignature($request);

if (!$result->isSuccess()) {
    http_response_code(400);
    exit('invalid webhook signature');
}

// Webhook is verified; continue with event parsing/handling.

Webhook Verification (Advanced)

Use webhook ID override for multi-tenant endpoints:

$request = VerifyWebhookSignatureRequest::fromRawPayload(
    rawBody: $rawBody,
    headers: $headers,
    webhookId: $resolvedWebhookId,
);

$result = $client->webhooks()->verifyWebhookSignature($request);

Inspect debug fields:

$status = $result->status->value;      // SUCCESS | FAILURE
$debugId = $result->debugId;           // PayPal debug ID if present
$raw = $result->rawResponse;           // Raw API response payload

Typed Event Parsing

Parse to typed models from payload array or raw JSON:

$event = $client->webhooks()->parseRawEvent($rawBody);

if ($event->is('PAYMENT.CAPTURE.COMPLETED')) {
    // typed model returned for mapped event type
}

Mapped event models:

  • PaymentCaptureCompletedEvent
  • PaymentCaptureDeniedEvent
  • PaymentCaptureRefundedEvent
  • PaymentCapturePendingEvent
  • PaymentCaptureReversedEvent
  • CheckoutOrderApprovedEvent
  • CheckoutOrderCompletedEvent
  • CustomerDisputeCreatedEvent
  • CustomerDisputeResolvedEvent
  • BillingSubscriptionCreatedEvent
  • BillingSubscriptionCancelledEvent
  • BillingSubscriptionActivatedEvent
  • BillingSubscriptionSuspendedEvent
  • BillingSubscriptionExpiredEvent
  • BillingSubscriptionPaymentFailedEvent
  • PaymentPayoutsBatchSuccessEvent
  • PaymentPayoutsItemSucceededEvent
  • PaymentPayoutsItemDeniedEvent

Unmapped events return UnknownWebhookEvent and keep the full raw payload.

Event Catalog

PayPal Event Type Typed Class
PAYMENT.CAPTURE.COMPLETED PaymentCaptureCompletedEvent
PAYMENT.CAPTURE.DENIED PaymentCaptureDeniedEvent
PAYMENT.CAPTURE.REFUNDED PaymentCaptureRefundedEvent
PAYMENT.CAPTURE.PENDING PaymentCapturePendingEvent
PAYMENT.CAPTURE.REVERSED PaymentCaptureReversedEvent
CHECKOUT.ORDER.APPROVED CheckoutOrderApprovedEvent
CHECKOUT.ORDER.COMPLETED CheckoutOrderCompletedEvent
CUSTOMER.DISPUTE.CREATED CustomerDisputeCreatedEvent
CUSTOMER.DISPUTE.RESOLVED CustomerDisputeResolvedEvent
BILLING.SUBSCRIPTION.CREATED BillingSubscriptionCreatedEvent
BILLING.SUBSCRIPTION.CANCELLED BillingSubscriptionCancelledEvent
BILLING.SUBSCRIPTION.ACTIVATED BillingSubscriptionActivatedEvent
BILLING.SUBSCRIPTION.SUSPENDED BillingSubscriptionSuspendedEvent
BILLING.SUBSCRIPTION.EXPIRED BillingSubscriptionExpiredEvent
BILLING.SUBSCRIPTION.PAYMENT.FAILED BillingSubscriptionPaymentFailedEvent
PAYMENT.PAYOUTSBATCH.SUCCESS PaymentPayoutsBatchSuccessEvent
PAYMENT.PAYOUTS-ITEM.SUCCEEDED PaymentPayoutsItemSucceededEvent
PAYMENT.PAYOUTS-ITEM.DENIED PaymentPayoutsItemDeniedEvent

Event Routing

Use WebhookEventRouter to map event types to handlers:

use Sujip\PayPal\Notifications\Webhook\Event\WebhookEventType;
use Sujip\PayPal\Notifications\Webhook\WebhookEventRouter;

$router = (new WebhookEventRouter())
    ->onCaptureCompleted(function ($event) {
        // handle capture completed
    })
    ->onType(WebhookEventType::CustomerDisputeCreated, function ($event) {
        // handle dispute created
    })
    ->onSubscriptionPaymentFailed(function ($event) {
        // handle subscription payment failed
    })
    ->fallback(function ($event) {
        // log/ignore unknown event types
    });

$router->dispatch($event);

Framework Adapters

Use WebhookRequestAdapterInterface to bridge framework request objects without adding framework dependencies to this package.

use Sujip\PayPal\Notifications\Contracts\WebhookRequestAdapterInterface;

final readonly class LaravelWebhookAdapter implements WebhookRequestAdapterInterface
{
    public function __construct(private \Illuminate\Http\Request $request)
    {
    }

    public function rawBody(): string
    {
        return (string) $this->request->getContent();
    }

    public function headers(): array
    {
        return $this->request->headers->all();
    }

    public function webhookId(): ?string
    {
        return config('services.paypal.webhook_id');
    }
}

Built-in generic adapters:

  • ArrayWebhookRequestAdapter
  • SuperglobalWebhookRequestAdapter

Webhook Processor

WebhookProcessor handles the full flow: request extraction -> signature verification -> event parsing -> optional idempotency -> optional routing -> structured result.

use Sujip\PayPal\Notifications\Adapter\SuperglobalWebhookRequestAdapter;
use Sujip\PayPal\Notifications\Idempotency\InMemoryIdempotencyStore;
use Sujip\PayPal\Notifications\Idempotency\WebhookIdempotencyGuard;
use Sujip\PayPal\Notifications\Webhook\WebhookEventRouter;

$router = (new WebhookEventRouter())->onCaptureCompleted(fn () => null);
$guard = new WebhookIdempotencyGuard(new InMemoryIdempotencyStore());

$result = $client->webhookProcessor($router, $guard)->process(
    SuperglobalWebhookRequestAdapter::fromGlobals()
);

if (!$result->accepted) {
    http_response_code(400);
}

Token Caching

By default, OAuth tokens are cached in memory for the current process. For multi-worker deployments, use a persisted cache.

File cache:

use Sujip\PayPal\Notifications\Auth\FileTokenCache;

$client = new PayPalClient(
    config: $config,
    transport: new CurlTransport(),
    tokenCache: new FileTokenCache(__DIR__.'/.cache/paypal-oauth'),
);

Redis-style cache (no Redis dependency in core):

use Sujip\PayPal\Notifications\Auth\RedisTokenCache;

$store = new YourRedisBackedKeyValueStore(); // implements KeyValueStoreInterface
$tokenCache = new RedisTokenCache($store);

Idempotency Guard

Use idempotency to avoid duplicate webhook processing:

use Sujip\PayPal\Notifications\Idempotency\InMemoryIdempotencyStore;
use Sujip\PayPal\Notifications\Idempotency\WebhookIdempotencyGuard;

$guard = new WebhookIdempotencyGuard(new InMemoryIdempotencyStore());

if (!$guard->checkAndRemember($event)) {
    // duplicate or missing event ID -> skip processing
    return;
}

// process event exactly once in this store scope

For production, implement IdempotencyStoreInterface with persistent storage (Redis, DB, cache).

Persistent Idempotency Stores

Redis-style store:

use Sujip\PayPal\Notifications\Idempotency\RedisIdempotencyStore;

$store = new RedisIdempotencyStore(new YourRedisBackedKeyValueStore());
$guard = new WebhookIdempotencyGuard($store);

Database-backed idempotency is supported via IdempotencyStoreInterface in your app code. Keep storage logic close to your framework/DB stack and inject your store into WebhookIdempotencyGuard.

Retry Strategy

Webhook signature verification can retry transient failures using conservative defaults.

$config = new ClientConfig(
    clientId: '...',
    clientSecret: '...',
    webhookId: '...',
    verificationMaxRetries: 2,
    verificationRetryBackoffMs: 150,
    verificationRetryMaxBackoffMs: 1000,
    verificationRetryHttpStatusCodes: [429, 500, 502, 503, 504],
);

Set verificationMaxRetries to 0 to disable retries.

Instant Payment Notification (Legacy)

Instant Payment Notification is legacy. Keep using it only for existing integrations while migrating to Webhooks.

Verify from array payload:

<?php

declare(strict_types=1);

use Sujip\PayPal\Notifications\InstantPaymentNotification\VerifyInstantPaymentNotificationRequest;

$result = $client->ipn()->verify(
    VerifyInstantPaymentNotificationRequest::fromArray($_POST)
);

if ($result->isVerified()) {
    // VERIFIED
}

Verify from raw body:

$raw = file_get_contents('php://input') ?: '';
$result = $client->instantPaymentNotification()->verifyRaw($raw);

Transport Extension

Use any HTTP client stack by implementing this contract:

use Sujip\PayPal\Notifications\Contracts\TransportInterface;
use Sujip\PayPal\Notifications\Http\HttpRequest;
use Sujip\PayPal\Notifications\Http\HttpResponse;

final class CustomTransport implements TransportInterface
{
    public function send(HttpRequest $request): HttpResponse
    {
        // call your preferred HTTP client and map response
    }
}

Inject custom transport into PayPalClient.

Popular choices for custom implementations are Guzzle, Symfony HttpClient, and Laravel HTTP client. Keep these adapters in your app or in separate bridge packages to avoid dependency lock-in in core.

Extending Event Mappings

See docs/adding-event-mapping.md for the full workflow and test requirements.

Event files are organized by PayPal categories under src/Webhook/Event/ (Payments, Disputes, Orders, Subscriptions, Payouts).

Dead-Letter Guidance

Dead-letter persistence is application-specific and intentionally not built into core. Recommended fields for replay workflows:

  • webhook event ID
  • raw payload
  • normalized headers
  • verification status and PayPal debug ID
  • processing error class/message
  • first-seen and last-seen timestamps

Examples

  • examples/webhook-endpoint.php - full webhook verification + event routing flow.
  • examples/ipn-endpoint.php - legacy Instant Payment Notification verification endpoint.
  • examples/custom-transport.php - transport contract integration template.
  • examples/framework-adapter-template.php - framework adapter contract template.

Error Handling

Main exceptions:

  • ConfigurationException
  • TransportException
  • TransportFailed
  • AuthenticationException
  • InvalidWebhookHeadersException
  • InvalidPayloadException
  • VerificationException
  • SignatureVerificationFailed
  • MalformedPayload

Example:

try {
    $result = $client->webhooks()->verifySignature($request);
} catch (\Sujip\PayPal\Notifications\Exception\PayPalException $e) {
    // log and fail closed
}

Production Checklist

  • Configure a unique webhook endpoint per environment (sandbox and live).
  • Verify webhook signatures before any business logic.
  • Set maxWebhookTransmissionAgeSeconds and allowedWebhookClockSkewSeconds for replay protection.
  • Keep strictPayPalCertUrlValidation enabled in production.
  • Persist webhook event IDs in a durable store and enable idempotency guard.
  • Handle verification failures with fail-closed behavior (return non-2xx on invalid signatures).
  • Keep clientSecret in a secure secret store; never commit credentials.
  • Log PayPal debug_id for failed verification calls to simplify support investigations.
  • Monitor duplicate, failed, and unknown event-type counts.
  • Prefer Webhooks for all new integrations; treat Instant Payment Notification as legacy-only.

Testing

composer install
composer test
composer stan

Manual Sandbox Smoke Test

Use scripts/smoke/sandbox-webhook-smoke.php to call your local webhook endpoint with controlled headers and payloads while validating your integration.

php scripts/smoke/sandbox-webhook-smoke.php \
  --url="http://127.0.0.1:8000/webhook/paypal" \
  --payload='{"id":"WH-TEST","event_type":"PAYMENT.CAPTURE.COMPLETED","resource":{"id":"CAP-1"}}' \
  --header="PAYPAL-TRANSMISSION-ID: trans-1" \
  --header="PAYPAL-TRANSMISSION-TIME: 2026-02-21T00:00:00Z" \
  --header="PAYPAL-TRANSMISSION-SIG: sig" \
  --header="PAYPAL-CERT-URL: https://api-m.sandbox.paypal.com/v1/notifications/certs/CERT-123" \
  --header="PAYPAL-AUTH-ALGO: SHA256withRSA"

Contributing

Contributions are welcome. Please include tests for behavior changes.

See CONTRIBUTING.md for contributor workflow, SUPPORT.md for support flow, and SECURITY.md for vulnerability reporting.

License

MIT

sudiptpa/paypal-notifications 适用场景与选型建议

sudiptpa/paypal-notifications 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 356 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 02 月 21 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「notifications」 「paypal」 「ipn」 「instant payment notification」 「webhooks」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 sudiptpa/paypal-notifications 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 sudiptpa/paypal-notifications 我们能提供哪些服务?
定制开发 / 二次开发

基于 sudiptpa/paypal-notifications 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 356
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 7
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-02-21