zfr / zfr-cash
简化使用Stripe支付网关进行支付的Zend Framework 2模块
Requires
- php: >=5.5
- ddeboer/vatin: 1.3.*
- doctrine/doctrine-module: ~0.9
- doctrine/orm: ~2.5
- zendframework/zend-eventmanager: ~2.2
- zendframework/zend-http: ~2.2
- zendframework/zend-modulemanager: ~2.2
- zendframework/zend-mvc: ~2.2
- zendframework/zend-servicemanager: ~2.2
- zfr/zfr-stripe-module: ~3.0
Requires (Dev)
- phpunit/phpunit: ~4.1
- squizlabs/php_codesniffer: 2.2.*
This package is auto-updated.
Last update: 2024-08-29 03:47:19 UTC
README
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.created
、customer.discount.updated
、customer.discount.deleted
:ZfrCash同步各种折扣事件(包括订阅折扣和客户折扣)。这意味着您可以直接在Stripe UI中创建/更新/删除折扣,并将其自动持久化到数据库中。或者,您也可以使用服务在代码中创建/更新/删除折扣。customer.card.updated
或customer.source.updated
:自2015年1月起,Stripe可以自动更新您的Stripe客户的卡,而无需他们手动更新卡。因此,ZfrCash自动更新卡。或者,您可以在代码中创建或删除卡。如果您使用的是API版本较新或等于2015-02-18,您必须使用customer.source.updated
,否则使用customer.card.updated
。customer.subscription.updated
、customer.subscription.deleted
:当您的订阅续订时,ZfrCash将自动更新各种订阅属性(如current_period_start
和current_period_end
)。如果它在Stripe中被删除,它还会删除数据库中的订阅。或者,您可以使用服务在代码中创建、更新和删除订阅。plan.created
、plan.updated
、plan.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_succeeded
、invoice.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服务非常不可靠,经常失败。