quartet/webpay-bundle
Quartet WebPay Bundle integrates WebPay payment services into Symfony/PHP applications, with configurable service wiring and a streamlined setup for handling payment requests and responses. It reduces boilerplate so you can add WebPay support quickly while keeping payment configuration centralized a...
First, correct the mental model. This is not a Laravel package and there is no composer require path into a modern app: it is a Symfony 2 bundle (getContainerExtension(), Resources/config/services.yml, AppKernel registration) wrapping the webpay/webpay PHP client for WebPay — the Japanese card processor now part of Stripe. "Leveraging it day-to-day" therefore means reading it as a specification and porting the thin API layer, not installing it.
Minimal steps, in order:
DependencyInjection/Configuration.php — the config tree: API key, public key, and any api_version/endpoint overrides. This is the entire public surface you need to replicate in config/services.php.Resources/config/services.yml — shows what the bundle actually constructs; usually one service for the \WebPay\WebPay client with the key injected. That tells you how much code there really is (very little).DependencyInjection/*Extension.php — how config maps onto the client (e.g. \WebPay\WebPay::setApiKey() or a client constructor argument).composer.json of the bundle and note the pinned webpay/webpay and Guzzle constraints. That constraint is the single most important fact for your port: if it pins Guzzle 3/5, do not let it near the app's dependency graph — copy the source into app/Support/WebPay/ (vendored) or, better, write the client yourself against the documented HTTP shape.token → server calls charge-create with that token → charge object returns id, paid, amount, currency, captured. Everything else (auth-only capture, refund, customer/saved-card, webhooks) is a variation on that shape.Your deliverable after that read is a Laravel WebPayGateway behind a PaymentGateway interface — not a Symfony bundle in your tree.
Config → config/services.php + .env. Replicate only the keys you saw in Configuration.php, and never let the secret key reach a committed file:
// config/services.php
'webpay' => [
'secret_key' => env('WEBPAY_SECRET_KEY'),
'public_key' => env('WEBPAY_PUBLIC_KEY'),
'api_version' => env('WEBPAY_API_VERSION', '2019-05-23'), // pin explicitly
],
Resolve via a singleton binding rather than env() calls inside services, so the value is config-cached safely.
Bind a contract, not the vendor class. The whole point of porting rather than adopting is that your application layer should not know "WebPay" exists:
interface PaymentGateway {
public function charge(int $amountJpy, string $token, array $opts = []): ChargeResult;
public function capture(string $chargeId, ?int $amountJpy = null): ChargeResult;
public function refund(string $chargeId, ?int $amountJpy = null): ChargeResult;
public function find(string $chargeId): ChargeResult;
}
ChargeResult is a framework-agnostic DTO (id, paid, captured, amount, currency, failureCode, raw). The WebPay adapter maps WebPay's response into it; Stripe/PAY.JP/GMO adapters map theirs. This is what makes the bundle's shape worth studying — its API surface is a decent template for that interface (charge, capture, refund, retrieve).
Extract, then wrap as a service provider. Keep the ported HTTP client in app/Services/WebPay/ as a plain class (constructor takes base URL + key), and register it:
$this->app->singleton(WebPayClient::class, fn ($app) => new WebPayClient(
$app['config']->get('services.webpay'),
// inject an HTTP client you control for fakes/timeouts
));
$this->app->bind(PaymentGateway::class, WebPayGateway::class);
The injection point for the HTTP client is your testability lever — the Symfony bundle's use of DI is the one pattern worth copying exactly.
Idempotency at the adapter boundary. WebPay-era APIs generally did not give you an idempotency key header, so synthesize one: a payments table row with your own idempotency_key unique index, written before the outbound call. On retry, look up the row first; if a charge ID exists, return it rather than re-charging. This is the single most important addition to anything you lift from the bundle — the original almost certainly does not have it.
Order state machine driven by webhooks, not by the redirect. Model pending → authorized → captured → refunded/failed and let webhook handlers (charge.succeeded, charge.failed, charge.refunded) advance state. Use the bundle to see whether it verified webhook signatures (\WebPay\WebPay::webhooks()-style verification against the signature header) — if it did not, you must implement verification yourself, and in the meantime treat the return URL purely as a UI signal.
Contract tests with recorded fixtures. Run the extract against the sandbox once, capture raw JSON responses (success, card declined, 3DS-required, refund, partial refund), and commit them as fixtures. Your adapter tests replay fixtures through a faked HTTP client — meaning your suite never needs sandbox credentials in CI. The bundle is useful here as a source of which responses to expect.
Local dev loop. Fake the gateway binding in local (PaymentGateway bound to a FakeGateway that returns canned charge IDs) so you never need network calls to iterate on order flows; swap in the real adapter only for the sandbox spike and integration tests.
composer.json. Symfony 2 is EOL (Nov 2018) and the bundle's dependency pins (Guzzle 3/5, old webpay/webpay) will fight your modern Laravel HTTP stack. Dependency-resolution failure hours into a sprint is the predictable outcome. Vendor or rewrite; never require.each()/create_function-adjacent patterns, loose == on response codes, and old TLS/cipher defaults in the HTTP layer. Grep for ->{$var} assignments on classes without #[AllowDynamicProperties]/declared props, and replace any bundled Guzzle options (verify, curl) with your own client config.assert(in_array($currency, ['JPY']) ? $amount === (int) $amount : true)) and a test that pins 1000 → ¥1,000, not ¥10.capture semantics. Many charge-create calls auto-capture. If you need auth-then-capture (common for order-then-ship), confirm the flag name in the bundle's call sites and test the auth-only → capture → partial-refund path explicitly; partial-capture arithmetic is a classic bug source.card blocks) must be scrubbed at the adapter boundary before any Log::debug. Wire it into a finally that redacts known keys.Configuration.php is a clean inventory of "everything variable about this gateway." Mirror it as your config array and your future gateway swap becomes a config change plus one class.PaymentGateway interface narrow and the WebPay-specific code in one directory. The day you switch to Stripe/PAY.JP/GMO, the diff should be one adapter plus a config block — not a search-and-replace across the domain.How can I help you explore Laravel packages today?