zfr/zfr-cash

简化使用Stripe支付网关进行支付的Zend Framework 2模块

v1.2.3 2015-03-28 19:44 UTC

This package is auto-updated.

Last update: 2024-08-29 03:47:19 UTC


README

Build Status Scrutinizer Code Quality Code Coverage Latest Stable Version Total Downloads

ZfrCash是一个高级的Zend Framework 2模块,简化了处理支付的方式。它内部使用Stripe作为支付网关,使用ZfrStripe

以下是ZfrCash允许的一些功能

  • 提供干净的接口来表示Stripe客户和可收费对象(可以接收订阅的对象)。
  • 提供干净的服务来创建客户、卡、折扣、计划和订阅。
  • 提供基本监听器,以同步订阅、卡和折扣。
  • 易于通过事件扩展

依赖项

  • ZF2: >= 2.2
  • Doctrine ORM: >= 2.5(我们使用了仅随Doctrine ORM 2.5提供的某些新功能)
  • DoctrineORMModule: >= 0.9
  • ZfrStripe

虽然我们只使用了Doctrine Common接口,但我怀疑它不会与Doctrine ODM一起工作,因为它需要像实体解析器这样的东西。

安装

要安装ZfrCash,请使用composer

php composer.phar require zfr/zfr-cash:~1.0

在您的 application.config.php 中启用ZfrCash,然后将文件 vendor/zfr/zfr-cash/config/zfr_cash.local.php.dist 复制到应用程序的 config/autoload 目录(不要忘记从文件名中删除 .dist 扩展名)!

关键概念

在深入了解ZfrCash之前,您需要熟悉本模块中使用的某些概念

  • 客户:客户(任何实现ZfrCash\Entity\CustomerInterface的对象)在Stripe中具有客户的意义。客户与Stripe客户标识符、信用卡(可选)和折扣(可选)相关联。

  • 可收费对象:一旦您有了客户,您就可以创建一个或多个定期订阅。ZfrCash引入了可收费对象的概念(任何实现ZfrCash\Entity\BillableInterface接口的对象)。可收费对象只是一个包含订阅的对象(反过来,包含付款人 - 客户)的对象。

ZfrCash足够灵活,可以支持许多不同的用例。以下是一些示例

每个用户一个订阅

如果您的业务基于每个用户一个Stripe订阅(用户本身订阅一个计划),那么您可以让您的用户实现两个ZfrCash接口

use ZfrCash\Entity\BillableInterface;
use ZfrCash\Entity\BillableTrait;
use ZfrCash\Entity\CustomerInterface;
use ZfrCash\Entity\CustomerTrait;

class User implements CustomerInterface, BillableInterface
{
    use CustomerTrait;
    use BillableTrait;
}

这两个特性和默认映射都很有道理。

多个订阅

Stripe支持多个订阅,ZfrCash使支持此用例变得容易。例如,您可能希望按项目计价,每个新项目都会产生一个新订阅(但由同一个人支付)。在这种情况下,可收费对象将是项目,而用户将是客户。

用户现在只实现CustomerInterface

use ZfrCash\Entity\CustomerInterface;
use ZfrCash\Entity\CustomerTrait;

class User implements CustomerInterface
{
    use CustomerTrait;
}

而项目实现了BillableInterface

use ZfrCash\Entity\BillableInterface;
use ZfrCash\Entity\BillableTrait;

class Project implements BillableInterface
{
    use BillableTrait;
}

用法

配置

虽然ZfrCash尽量自动处理,但它需要您进行一些配置。

模块配置

您需要做的第一件事是将zfr_cash.local.php.dist文件复制到您的application/autoload文件夹。有一个强制性选项

  • object_manager:如果您使用Doctrine ORM,那么您应该指定doctrine.entitymanager.orm_default

指定Doctrine解析器

在您的应用程序中添加以下配置

return [
    'doctrine' => [
        'entity_resolver' => [
            'orm_default' => [
                'resolvers' => [
                    CustomerInterface::class => YourCustomerClass::class,
                    BillableInterface::class => YourBillableClass::class
                ]
            ]
        ]
    ]
]

这实际上将两个ZfrCash接口映射到您的代码中的具体实现。

实现存储库接口

为了使ZfrCash正常工作,您必须创建两个自定义Doctrine存储库

  • 实现CustomerInterface类库的仓库必须实现ZfrCash\Repository\CustomerRepositoryInterface
  • 实现BillableInterface类库的仓库必须实现ZfrCash\Repository\BillableRepositoryInterface

要创建自定义仓库,您需要在Doctrine 2映射中设置repositoryClass选项。以下是一个实现CustomerInterface的用户类示例。

/**
 * @ORM\Entity(repositoryClass="User\Repository\UserRepository")
 */
class User implements CustomerInterface
{
	use CustomerTrait;
}

您的仓库实现给定接口的位置。

namespace User\Repository;

use Doctrine\ORM\EntityRepository;
use ZfrCash\Repository\CustomerRepositoryInterface;

class UserRepository extends EntityRepository implements CustomerRepositoryInterface
{
	public function findOneByStripeId($stripeId)
	{
		return $this->findOneBy(['stripeId' => $stripeId]);
	}
}

对于账单对象(如果使用每个客户一个订阅的架构,这可能是同一个仓库),执行相同的操作。

设置路由

默认情况下,ZfrCash创建两个路由,将监听由Stripe触发的一些事件。

  • /stripe/test-listener:将监听测试事件
  • /stripe/live-listener:将监听实时事件

您可以更改URL,但那些是合理的默认值。ZfrCash足够智能,可以检测传入的事件是否与给定的路由匹配。例如,如果实时事件达到测试监听器,它将不执行任何操作(它使用API密钥前缀来查看是否匹配)。

每当ZfrCash从Stripe收到事件时,它将向Stripe发出额外的API请求以验证webhook,并确保没有人试图攻击您。但是,如果您的应用程序在受控环境中运行(例如,如果您通过Stripe IP过滤传入请求),您可以通过在您的配置中将validate_webhooks选项设置为false来禁用此行为。

return [
	'zfr_cash' => [
		'validate_webhooks' => false
	]
];

默认情况下,ZfrCash将监听以下事件,并采取一些操作

  • customer.discount.createdcustomer.discount.updatedcustomer.discount.deleted:ZfrCash同步各种折扣事件(包括订阅折扣和客户折扣)。这意味着您可以直接在Stripe UI中创建/更新/删除折扣,并将其自动持久化到数据库中。或者,您也可以使用服务在代码中创建/更新/删除折扣。
  • customer.card.updatedcustomer.source.updated:自2015年1月起,Stripe可以自动更新您的Stripe客户的卡,而无需他们手动更新卡。因此,ZfrCash自动更新卡。或者,您可以在代码中创建或删除卡。如果您使用的是API版本较新或等于2015-02-18,您必须使用customer.source.updated,否则使用customer.card.updated
  • customer.subscription.updatedcustomer.subscription.deleted:当您的订阅续订时,ZfrCash将自动更新各种订阅属性(如current_period_startcurrent_period_end)。如果它在Stripe中被删除,它还会删除数据库中的订阅。或者,您可以使用服务在代码中创建、更新和删除订阅。
  • plan.createdplan.updatedplan.deleted:每当您在Stripe中创建、更新或删除计划时,它将自动添加到您的数据库中。

如果您不想让ZfrCash保持数据库同步,您可以通过在配置中将register_listeners设置为false来禁用此行为。

return [
	'zfr_cash' => [
		'register_listeners' => false
	]
];

配置Stripe发送事件

现在ZfrCash已配置,我们需要配置Stripe,以便它正确地将事件发送到您的应用程序。为此,请转到您的Stripe仪表板。

在右上角,单击您的账户名称,然后选择“账户设置”。打开“Webhooks”选项卡。

您可以添加测试监听器或实时监听器。单击“添加URL”。仔细选择正确的模式,并输入正确的URL。例如,对于实时URL:https://www.mysite.com/stripe/live-listener

我们不推荐您发送所有事件,因为Stripe非常健谈,它可能会给您的服务器带来一些压力。至少,我们建议您监听ZfrCash监听的事件。其他一些有趣的事件包括:invoice.payment_succeededinvoice.payment_failed... 在稍后的部分,您将了解如何将您自己的代码挂钩。

在生产之前,请确保在测试模式下测试您的代码,并查看ZfrCash是否正确响应。

监听其他Stripe事件

虽然ZfrCash只提供基本事件的处理行为,Stripe会发送许多其他事件。例如,你可能希望在定期付款失败时发送电子邮件。为此,你必须监听ZfrCash\Event\WebhookEvent::WEBHOOK_RECEIVED事件。

第一步是创建你的监听器类

namespace Application\Listener;

use ZfrCash\Controller\WebhookListenerController;
use ZfrCash\Event\WebhookEvent;

class CustomStripeListener extends AbstractListenerAggregate
{
	public function attachAggregate(EventManagerInterface $eventManager)
	{
		$sharedManager = $eventManager->getSharedManager();
		$sharedManager->attach(WebhookListenerController::class, WebhookEvent::WEBHOOK_RECEIVED, [$this, 'handleStripeEvent']);
	}
	
	/**
	 * @param WebhookEvent $event
	 */
	public function handleStripeEvent(WebhookEvent $event)
	{
		$stripeEvent = $event->getStripeEvent(); // This is the full Stripe event
		
		switch ($stripeEvent['type']) {
			case 'invoice.payment_failed':
				// Do something...
				break;
		}
	}
}

最后,你需要注册你的监听器。在你的Module.php类中

public function onBootstrap(EventInterface $event)
{
    /* @var $application \Zend\Mvc\Application */
    $application    = $event->getTarget();
    $serviceManager = $application->getServiceManager();

    $eventManager = $application->getEventManager();
    $eventManager->attach(new CustomStripeListener());
}

使用CustomerService

你可以通过在服务管理器中使用ZfrCash\Service\CustomerService键来检索CustomerService。

创建

CustomerService是一个内置服务,允许你创建Stripe客户。该服务会自动在Stripe上创建一个Stripe客户,并将各种属性保存到你的数据库中。你可以选择在一次调用中创建带有卡和/或折扣的客户。

大多数情况下,客户将是你的用户类。这就是为什么ZfrCash期望你传递一个实现了ZfrCash\Entity\CustomerInterface的对象。以下是一个简单的用法

class UserController extends AbstractActionController
{
	public function createAction()
	{
		// Create your user... that implements CustomerInterface
		$cardToken = $this->params()->fromQuery('card_token');
		$discount  = $this->params()->fromQuery('discount');
		
		$user = $this->customerService->create($user, [
			'card'     => $cardToken, // If Stripe API version is older than 2015-02-18
			'source'   => $cardToken, // If Stripe API version is newer or equal than 2015-02-18
			'discount' => $discount,
			'email'    => $user->getEmail()
		]);
	}
}

支持以下选项

  • email:设置Stripe的电子邮件属性
  • description:设置Stripe的描述属性
  • card:可以是使用Stripe.JS创建的卡令牌或包含卡属性的完整哈希。对于2015-02-18之前的Stripe API版本,必须使用card代替source
  • source:可以是使用Stripe.JS创建的卡令牌或包含卡属性的完整哈希。对于2015-02-18之后或相同的Stripe API版本,必须使用source代替card
  • coupon:要附加给客户的优惠券
  • metadata:设置Stripe属性的关键值对
  • idempotency_key:用于防止操作被执行两次的密钥

所有这些属性都是可选的。

getByStripeId

如果你有客户的Stripe ID,你可以使用Stripe标识符检索完整的客户信息

$customer = $this->customerService->getByStripeAd('cus_abc');

使用card服务

card服务允许你为客户创建和删除卡。

你可以通过在服务管理器中使用ZfrCash\Service\CardService键来检索CardService。

attachToCustomer

如果你想替换客户的默认信用卡,可以使用attachToCustomer方法。它将自动从Stripe和你的数据库中删除旧卡,并附加新卡

$card = $cardService->attachToCustomer($customer, $cardToken);

// $card is the new card

第二个参数可以是使用Stripe.JS创建的卡令牌或卡属性哈希。

删除

你可以使用remove方法删除卡(从Stripe和你的数据库中删除)

$cardService->remove($card);

使用subscription服务

subscription服务用于创建、更新和删除任何订阅。

你可以通过在服务管理器中使用ZfrCash\Service\SubscriptionService键来检索SubscriptionService。

创建

主要操作是创建订阅。订阅是由可计费资源(可能相同,如果你为每个客户有一个订阅)支付的。该方法接受客户、可计费资源、计划和选项。支持以下选项

  • tax_percent:允许设置一个税,该税将作为正常计划价格之外的额外税
  • quantity:为计划设置数量
  • coupon:为给定的订阅设置优惠券
  • trial_end:一个DateTime对象,允许手动设置试用日期
  • application_fee_percent:如果你正在代表其他人通过Stripe Connect创建订阅
  • billing_cycle_anchor:一个DateTime对象,定义了何时开始定期付款
  • metadata:任何元数据对
  • idempotency_key:用于防止操作被执行两次的密钥

例如

$subscription = $subscriptionService->create($customer, $billable, $plan, [
	'quantity'  => 2,
	'trial_end' => (new DateTime())->modify('+7 days')
]);

内部,ZfrCash将在Stripe上创建订阅,将其保存到你的数据库中,并在付款人、订阅和可计费资源之间建立不同的连接。

注意:idempotency_key 是 Stripe 最近添加的新功能,而 ZfrCash 已经支持该功能。基本来说,它允许防止一个操作被重复执行。例如,假设您正在使用订阅服务在由工作者执行的延迟任务中创建订阅。任务被执行,订阅服务正确创建订阅(客户开始支付),但任务因任何原因(HTTP 调用超时,服务器关闭等)失败。因此,任务将自动重新插入以供稍后处理...但问题是订阅将再次创建,客户将支付两次!为了避免这个问题,您可以传递一个 idempotency_key(可以是任何东西,但在这个例子中,任务的唯一标识符是一个很好的候选)。在 24 小时内,如果您尝试使用完全相同的 idempotency_key 创建订阅,Stripe 将返回完全相同的响应,而不会每次都创建一个新的订阅!

取消

您可以通过该服务取消订阅。该方法接受一个可选的第二个参数(默认为 false),允许在当前周期结束时取消订阅,而不是立即停止(默认操作)。

如果将取消设置为立即取消订阅,它将自动将其从您的数据库中删除。

修改计划 / 修改数量

您还可以修改现有的订阅,无论是计划还是数量。

// Update the plan
$anotherPlan = ...;
$subscription = $this->subscriptionService->modifyPlan($subscription, $anotherPlan);

// Update the quantity
$subscription = $this->subscriptionService->modifyQuantity($subscription, 4);

像往常一样,ZfrCash 将调用 Stripe 的 API 并更新您的数据库。

获取器

该服务还具有您可以使用的一些获取器。

  • getById:通过 ID 获取订阅
  • getByStripeId:通过 Stripe ID 获取订阅
  • getByCustomer:获取给定客户的全部订阅

使用计划服务

计划服务允许您更新和删除计划。

您可以通过在服务管理器中使用 ZfrCash\Service\PlanService 键来检索 PlanService。

更新

您可以使用此方法更新计划名称(不推荐)或计划元数据。计划元数据(最多 20 个键/值)可用于在 API 中编码计划限制等。

停用

ZfrCash 不允许从数据库中删除计划。相反,它只允许停用计划。原因是您可能仍然有订阅链接到计划,删除它可能会损坏您的数据。

相反,当您停用计划(或当您从 Stripe 中删除它并且 ZfrCash 处理该事件时),它只是软删除。

从 Stripe 同步

当您第一次使用 ZfrCash 时,您可能在 Stripe 上已经有了计划。您不必手动将所有计划创建到您的数据库中,可以使用 syncFromStripe 方法。它将从您的 Stripe 账户检索所有创建的计划,并在本地数据库中创建它们。

每次导入或创建新计划时,默认情况下都会将其停用(以避免泄漏并使新创建的计划对客户可见)。您负责自行激活它。

使用客户折扣服务

客户折扣服务允许您处理客户折扣(在 Stripe 中,为客户创建的折扣将应用于所有定期付款)。

您可以通过在服务管理器中使用 ZfrCash\Service\CustomerDiscountService 键来检索 CustomerDiscountService。

为客户创建折扣

您可以通过传递优惠券代码为客户创建新的折扣。它将自动调用 Stripe 并更新您的数据库。如果客户已有优惠券,它将更新它。

$discount = $this->customerDiscountService->createForCustomer($customer, 'COUPON_15');

更改优惠券

您可以使用changeCoupon方法更新现有优惠券。这将自动调用Stripe API并更新您的数据库。

$discount = $this->customerDiscountService->changeCoupon($discount, 'COUPON_30');

removeCoupon

最后,您可以删除现有优惠券。这将从Stripe和您的数据库中删除它。

$this->customerDiscountService->remove($discount);

获取器

最后,该服务提供了一些您可以使用的方法。

  • getById:通过ID获取折扣。
  • getByCustomer:获取指定客户的折扣。

使用订阅折扣服务

订阅折扣服务允许您处理订阅折扣(在Stripe中,为订阅创建的折扣将仅应用于特定订阅的周期性支付)。

您可以通过服务管理器中的ZfrCash\Service\SubscriptionDiscountService密钥检索SubscriptionDiscountService。

createForSubscription

您可以通过传递优惠券代码来创建一个针对订阅的新折扣。它将自动调用Stripe API并更新您的数据库。如果订阅已有优惠券,则将更新它。

$discount = $this->subscriptionDiscountService->createForSubscription($customer, 'COUPON_15');

更改优惠券

您可以使用changeCoupon方法更新现有优惠券。这将自动调用Stripe API并更新您的数据库。

$discount = $this->subscriptionDiscountService->changeCoupon($discount, 'COUPON_30');

removeCoupon

最后,您可以删除现有优惠券。这将从Stripe和您的数据库中删除它。

$this->subscriptionDiscountService->remove($discount);

获取器

最后,该服务提供了一些您可以使用的方法。

  • getById:通过ID获取折扣。
  • getBySubscription:获取指定订阅的折扣。

其他

ZfrCash附带一个用于欧洲增值税号码(VIES)的验证器。例如,如果您有一个输入过滤器,可以添加验证器。

$inputFilter->add([
    'name'       => 'tax_number',
    'required'   => false,
    'validators' => [
        ['name' => ViesValidator::class]
    ]
]);

默认情况下,验证器仅遵循正确的格式规则,但不强制执行增值税号码的真实存在。这需要调用VIES Web服务的一个额外调用。要启用此功能,您可以使用选项check_existence

$inputFilter->add([
    'name'       => 'tax_number',
    'required'   => false,
    'validators' => [
        [
            'name'    => ViesValidator::class,
            'options' => ['check_existence' => true]
        ]
    ]
]);

然而,请注意,根据我的经验,VIES Web服务非常不可靠,经常失败。