wayofdev/laravel-stripe-webhooks

使用 Cycle-ORM 支持在 Laravel 应用中处理 Stripe webhooks。

v2.1.47 2024-04-11 23:32 UTC

This package is auto-updated.

Last update: 2024-09-20 16:31:47 UTC


README


Dark WOD logo for light theme Light WOD logo for dark theme

Build Status Total Downloads Latest Stable Version Software License Commits since latest release

使用 Cycle-ORM 集成在 Laravel 应用中处理 Stripe Webhooks

Stripe 可以通过 webhooks 通知应用程序各种事件。此包简化了处理这些 webhooks 的过程。默认情况下,它会验证所有传入请求的 Stripe 签名。

验证后,所有有效的 webhook 调用都将使用 Cycle-ORM 记录到数据库中。您可以在接收到特定事件时轻松定义要分发的作业或事件。

请注意,此包仅管理初始 webhook 请求验证和相应作业或事件的分发。

后续操作(例如,有关支付的操作)应由开发者单独实现。在深入了解此包之前,强烈建议您熟悉 Stripe 关于 webhooks 的全面文档


如果您 喜欢/使用 此包,请考虑 星标 它。谢谢!


💿 安装

→ 使用 Composer

作为依赖项要求

$ composer req wayofdev/laravel-stripe-webhooks

服务提供程序将自动注册自身。

→ 配置包

您必须使用以下命令发布配置文件

$ php artisan vendor:publish \
	--provider="WayOfDev\StripeWebhooks\Bridge\Laravel\Providers\StripeWebhooksServiceProvider" \
	--tag="config"

这是将发布到 config/stripe-webhooks.php 的配置文件内容

<?php

declare(strict_types=1);

use WayOfDev\StripeWebhooks\Profile\StripeWebhookProfile;
use WayOfDev\WebhookClient\Entities\WebhookCall;
use WayOfDev\WebhookClient\Persistence\ORMWebhookCallRepository;

return [
    /*
     * Stripe will sign each webhook using a secret. You can find the used secret at the
     * webhook configuration settings: https://dashboard.stripe.com/account/webhooks.
     */
    'signing_secret' => env('STRIPE_WEBHOOK_SECRET'),

    /*
     * You can define a default job that should be run for all other Stripe event type
     * without a job defined in next configuration.
     * You may leave it empty to store the job in database but without processing it.
     */
    'default_job' => '',

    /*
     * You can define the job that should be run when a certain webhook hits your application
     * here. The key is the name of the Stripe event type with the `.` replaced by a `_`.
     *
     * You can find a list of Stripe webhook types here:
     * https://stripe.com/docs/api#event_types.
     */
    'jobs' => [
        // 'source_chargeable' => \App\Jobs\StripeWebhooks\HandleChargeableSource::class,
        // 'charge_failed' => \App\Jobs\StripeWebhooks\HandleFailedCharge::class,
    ],

    /*
     * The classname of the entity to be used to store webhook calls. The class should
     * be equal or extend WayOfDev\WebhookClient\Entities\WebhookCall.
     */
    'entity' => WebhookCall::class,

    /*
     * The classname of the repository to be used to store webhook calls. The class should
     * implement WayOfDev\WebhookClient\Contracts\WebhookCallRepository.
     */
    'entity_repository' => ORMWebhookCallRepository::class,

    /*
     * This class determines if the webhook call should be stored and processed.
     */
    'profile' => StripeWebhookProfile::class,

    /*
     * Specify a connection and or a queue to process the webhooks
     */
    'connection' => env('STRIPE_WEBHOOK_CONNECTION'),
    'queue' => env('STRIPE_WEBHOOK_QUEUE'),

    /*
     * When disabled, the package will not verify if the signature is valid.
     * This can be handy in local environments.
     */
    'verify_signature' => env('STRIPE_SIGNATURE_VERIFY', true),
];

在配置文件的 signing_secret 键中,您应添加一个有效的 webhook 密钥。您可以在 Stripe 控制面板上的 webhook 配置设置 中找到该密钥。

→ 准备数据库

默认情况下,所有 webhook 调用都将保存到数据库中。

要创建存储 webhook 调用的表

  1. 请确保您已经在您的 Laravel 项目中设置并运行了 wayofdev/laravel-cycle-orm-adapter 包。

  2. 修改 cycle.php 配置以将 WebhookCall 实体包含在搜索路径中

    // ...
    
    'tokenizer' => [
        /*
         * Directories to scan for entities.
         */
        'directories' => [
            __DIR__ . '/../src/Domain', // Your current project Entities
            __DIR__ . '/../vendor/wayofdev/laravel-webhook-client/src/Entities', // Register new Entity
        ],
      
      	// ...
    ],
  3. 更新配置后,运行命令以生成新实体的迁移

    $ php artisan cycle:orm:migrate

    (可选):查看待处理的迁移列表

    $ php artisan cycle:migrate:status
  4. 执行任何未解决的迁移

    $ php artisan cycle:migrate

→ 配置 webhook 路由

Stripe 控制面板 上,指定 Stripe 应发送 webhook 请求的 URL。在应用程序的路由文件中,使用 Route::stripeWebhooks 映射此 URL

Route::stripeWebhooks('webhook-route-configured-at-the-stripe-dashboard');

内部,此命令注册了一个由此包提供的控制器的 POST 路由。由于 Stripe 无法检索 csrf-token,请排除此路由的 VerifyCsrfToken 中间件

protected $except = [
    'webhook-route-configured-at-the-stripe-dashboard',
];

💻 使用

Stripe 为各种事件类型分发了 webhook。查看 Stripe 官方文档中的 完整事件类型列表

Stripe将对访问您的应用程序webhook URL的所有请求进行签名。此包将自动验证签名是否有效。如果签名无效,则请求可能不是由Stripe发送的。

除非出现严重错误,否则此包将始终以200响应webhook请求。发送200将防止Stripe反复重新发送相同的事件。Stripe可能会偶尔发送重复的webhook请求多次。此包确保每个请求只处理一次。所有带有有效签名的webhook请求都将记录在webhook_calls表中。该表有一个payload列,其中保存了传入webhook的整个负载。

如果签名无效,则包不会记录请求,但会抛出WayOfDev\StripeWebhooks\Exceptions\WebhookFailed异常。在webhook调用过程中发生的任何错误都将记录在exception列中。如果有错误,将发送500响应,否则发送200响应。

您可以使用此包以两种方式处理webhook请求:排队作业或监听包的事件。

→ 使用作业处理Webhook请求

要接收特定事件类型的操作,请定义一个作业。以下是一个作业示例

<?php
  
declare(strict_types=1);

namespace Infrastructure\Stripe\Webhooks\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use WayOfDev\WebhookClient\Entities\WebhookCall;

class HandleChargeableSource implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    public WebhookCall $webhookCall;

    public function __construct(WebhookCall $webhookCall)
    {
        $this->webhookCall = $webhookCall;
    }

    public function handle()
    {
        // do your work here

        // you can access the payload of the webhook call with `$this->webhookCall->payload()`
    }
}

为确保对webhook请求的及时响应,请考虑将作业设置为可排队。这允许高效地处理多个Stripe webhook请求,降低超时的可能性。

创建作业后,在stripe-webhooks.php配置文件的jobs数组中注册它。键应该是Stripe事件类型的名称,但将.替换为_。值应该是完全限定的类名。

// config/stripe-webhooks.php

'jobs' => [
    'source_chargeable' => \Infrastructure\Stripe\Webhooks\Jobs\HandleChargeableSource::class,
],

// ...

如果您想将一个作业配置为默认处理所有未定义的事件,您可以在stripe-webhooks.php配置文件中将作业设置为default_job。值应该是完全限定的类名。

默认情况下,配置为空字符串'',这只会将事件存储在数据库中,但不会处理。

// config/stripe-webhooks.php

'default_job' => \Infrastructure\Stripe\Webhooks\Jobs\HandleOtherEvent::class,

// ...

→ 使用事件处理Webhook请求

您可以选择监听此包将触发的事件,而不是排队作业以在收到webhook请求时执行某些工作。每当有效的请求击中您的应用程序时,该包将触发一个stripe-webhooks::<event-name>事件。

事件的负载将是为传入请求创建的WebhookCall实例。

让我们看看如何监听此类事件。在EventServiceProvider中,您可以注册监听器。

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'stripe-webhooks::source.chargeable' => [
        Infrastructure\Stripe\Listeners\ChargeSource::class,
    ],
];

以下是一个此类监听器的示例

<?php

namespace Infrastructure\Stripe\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use WayOfDev\WebhookClient\Entities\WebhookCall;

class ChargeSource implements ShouldQueue
{
    public function handle(WebhookCall $webhookCall)
    {
        // do your work here

        // you can access the payload of the webhook call with `$webhookCall->payload()`
    }
}

我们强烈建议您将事件监听器设置为可排队,因为这将最小化webhook请求的响应时间。这允许您处理更多的Stripe webhook请求并避免超时。

有关在Laravel中处理事件的其它方法的更多信息,请查看Laravel官方关于事件处理的文档


⚙️ 高级使用

→ 重试处理Webhook

所有传入的webhook请求都写入数据库。当处理webhook调用时出现问题时,这非常有价值。您可以在调查并修复失败原因后轻松地重试处理webhook调用,如下所示

use WayOfDev\WebhookClient\Contracts\WebhookCallRepository;
use WayOfDev\StripeWebhooks\Bridge\Laravel\Jobs\ProcessStripeWebhookJob;

class RetryWebhooks
{
    public function __construct(private WebhookCallRepository $repository)
    {
    }

  	public function handle()
    {
        dispatch(new ProcessStripeWebhookJob($repository->findById($id)));
    }
}

→ 执行自定义逻辑

您可以通过在stripe-webhooks配置文件的entity键中指定自己的实体来添加一些应在排队作业调度之前和/或之后执行的自定义逻辑。您可以通过指定自己的实体在stripe-webhooks配置文件中的entity键来完成此操作。该类应扩展WayOfDev\StripeWebhooks\Bridge\Laravel\Jobs\ProcessStripeWebhookJob

以下是一个示例

use WayOfDev\StripeWebhooks\Bridge\Laravel\Jobs\ProcessStripeWebhookJob;

class MyCustomStripeWebhookJob extends ProcessStripeWebhookJob
{
    public function handle(): void
    {
        // do some custom stuff beforehand

        parent::handle();

        // do some custom stuff afterwards
    }
}

→ 确定是否应处理请求

您可以自行编写逻辑来判断是否处理请求。您可以通过在 stripe-webhooks 配置文件中的 profile 键中指定自己的配置来实现。该类应实现 WayOfDev\WebhookClient\Contracts\WebhookProfile 接口。

在本例中,我们将确保仅处理尚未处理过的请求。

<?php

declare(strict_types=1);

namespace WayOfDev\StripeWebhooks\Profile;

use Cycle\Database\Injection\Parameter;
use Cycle\ORM\ORMInterface;
use Illuminate\Http\Request;
use WayOfDev\WebhookClient\Contracts\WebhookProfile;
use WayOfDev\WebhookClient\Entities\WebhookCall;
use WayOfDev\WebhookClient\Persistence\ORMWebhookCallRepository;

class StripeWebhookProfile implements WebhookProfile
{
    public function __construct(private readonly ORMInterface $orm)
    {
    }

    public function shouldProcess(Request $request): bool
    {
        /** @var ORMWebhookCallRepository $webhookCallsRepository */
        $webhookCallsRepository = $this->orm->getRepository(WebhookCall::class);

        $exists = $webhookCallsRepository
            ->select()
            ->where(['name' => 'stripe'])
            ->andWhere("JSON_EXTRACT(payload, '$.id')", '=', new Parameter(['payloadId' => $request->get('id')]))
            ->count();

        return 0 === $exists;
    }
}

→ 处理多个签名密钥

当使用 Stripe Connect 时,您可能希望包能够处理多个端点和密钥。以下是如何配置此行为。

如果您使用的是 Route::stripeWebhooks 宏,您可以按如下方式附加 configKey

Route::stripeWebhooks('webhook-url/{configKey}');

或者,如果您手动定义路由,您可以像这样添加 configKey

Route::post(
  'webhook-url/{configKey}',
  \WayOfDev\StripeWebhooks\Bridge\Laravel\Http\Controllers\StripeWebhooksController::class,
);

如果此路由参数存在,验证中间件将使用不同的配置键查找密钥,通过将给定的参数值附加到默认配置键。例如,如果 Stripe 将数据发送到 webhook-url/my-named-secret,您将添加一个新的配置名为 signing_secret_my-named-secret

Connect 的示例配置可能如下所示

// secret for when Stripe posts to webhook-url/account
'signing_secret_account' => 'whsec_abc',
// secret for when Stripe posts to webhook-url/connect
'signing_secret_connect' => 'whsec_123', 

→ 将 Webhook 有效负载转换为 Stripe 对象

您可以将 Webhook 有效负载转换为 Stripe 对象,以便辅助访问其各种方法和属性。

为此,使用 Stripe\Event::constructFrom($payload) 方法,并传入 WebhookCall 的有效负载

use Stripe\Event;

// ...

public function handle(WebhookCall $webhookCall)
{
    /** @var \Stripe\StripeObject|null */
    $stripeObject = Event::constructFrom($webhookCall->payload())->data?->object;
}

例如,如果您已为 invoice.created 事件设置 Stripe Webhook,则可以将有效负载转换为 StripeInvoice 对象

/** @var \Stripe\StripeInvoice|null */
$stripeInvoice = Event::constructFrom($webhookCall->payload())->data?->object;

// $stripeInvoice->status
// $stripeInvoice->amount_due
// $stripeInvoice->amount_paid
// $stripeInvoice->amount_remaining

foreach ($stripeInvoice->lines as $invoiceLine) {
    // ...
}

⚡️序列图

Architecture Diagram


🧪 运行测试

→ PHPUnit 测试

要运行测试,请运行以下命令

$ make test

→ 静态分析

使用 PHPStan 进行代码质量分析

$ make lint-stan

→ 编码标准修复

使用 The PHP Coding Standards Fixer (PHP CS Fixer) 修复代码以遵循我们的标准

$ make lint-php

🤝 许可证

Licence


🧱 致谢和有用资源

此存储库基于 spatie/laravel-stripe-webhooks 的工作。


🙆‍♂️ 作者信息

lotyp / wayofdev2023 创建


🙌 想要贡献?

感谢您考虑为 wayofdev 社区做出贡献!我们欢迎各种形式的贡献。如果您想

  • 🤔 建议一个功能
  • 🐛 报告一个问题
  • 📖 改进文档
  • 👨‍💻 为代码做出贡献