hoya/masterpass-bundle
Symfony bundle that brings Masterpass online checkout to PHP applications, handling the payment flow and service configuration so shoppers can pay with their Masterpass wallet. Review the README for installation and integration steps.
Set expectations before you install anything. hoya/masterpass-bundle is a Symfony bundle, not a Laravel package: no service provider, no facade, no php artisan vendor:publish. Last release was 2018-07-02 and it targets Symfony 2.x/3.x-era conventions against a wallet program Mastercard has since retired. Day-to-day, there are exactly two ways to "leverage" it:
If neither is true, stop here.
Step 0 — confirm the product still exists for you. Ask the acquirer/PSP whether Masterpass (or its successor Click to Pay / MPGS) is still contractually available in your markets. This gates every line of code below. A 2018 client pointed at retired endpoints is not a coding problem.
Step 1 — check whether it even resolves.
composer require hoya/masterpass-bundle --dry-run
Read the conflict output before committing. Expect PHP 5.6/7.x and old Symfony component constraints. On Laravel 10/11/12 + PHP 8.2 you'll need a fork with relaxed constraints, and even then only the plain-PHP protocol classes are likely usable.
Step 2 — read the bundle in this order. This is the only documentation you have:
DependencyInjection/Configuration.php ← authoritative config keys (consumer key/secret, callback, sandbox vs prod)
DependencyInjection/*Extension.php ← how those keys become service args
Resources/config/services.yml ← service IDs + constructor arguments you must re-declare
src/…Client / …Gateway ← the real API surface: request token, access token, checkout resource
Tests/ ← the most reliable spec of request/response shapes (if any exist)
Step 3 — pick your integration shape.
// app/Providers/MasterpassServiceProvider.php
public function register(): void
{
$this->app->singleton(MasterpassGateway::class, fn () => new MasterpassClient(
config('masterpass.consumer_key'),
config('masterpass.consumer_secret'),
config('masterpass.callback_url'),
config('masterpass.endpoint'),
));
}
First use case — sandbox spike, not production. One "Pay with Masterpass" button on one checkout page: request token → redirect user to the wallet → receive the callback with the verifier → exchange for an access token → fetch the checkout resource → log the masked card/billing payload. Do not create orders yet. The goal is to prove the handshake end to end and record what the sandbox actually returns.
Wrap it behind an interface immediately. The single highest-value pattern: your app should never import a Masterpass class directly.
interface WalletGateway {
public function begin(array $order): Redirect;
public function complete(Request $request): WalletResult;
}
MasterpassGateway implements WalletGateway; later ClickToPayGateway implements WalletGateway and nothing upstream changes. This is what turns a migration from a rewrite into a config swap.
Two-route redirect flow. The protocol is redirect-based, so model it as two endpoints plus state:
POST /checkout/pay → create order (yours), call requestToken, persist {order_id, request_token, created_at}, return a redirect to the wallet URL.GET /checkout/callback?oauth_token=…&oauth_verifier=… → look up the stored row by oauth_token, exchange for access token, fetch the checkout resource, persist the raw payload, then hand off to your normal order-completion path.Never trust the callback alone for order identity — always resolve the order from your stored request_token mapping, not from anything in the query string.
Mirror the bundle's config into Laravel conventions. Copy the keys out of Configuration.php verbatim so the fork stays diffable against upstream:
// config/masterpass.php
return [
'consumer_key' => env('MASTERPASS_CONSUMER_KEY'),
'consumer_secret' => env('MASTERPASS_CONSUMER_SECRET'),
'callback_url' => env('MASTERPASS_CALLBACK_URL'),
'endpoint' => env('MASTERPASS_ENDPOINT'), // sandbox vs prod
'timeout' => env('MASTERPASS_TIMEOUT', 15),
];
Fork-and-pin, not vendor-patch. Fork into an internal namespace, then delete the bundle plumbing that will never execute in Laravel:
*Bundle.php ← delete
DependencyInjection/ ← delete
Resources/config/*.yml ← delete
src/ (clients, DTOs, signer) ← keep, pin, test
Pin every transitive dependency to a current version and replace abandoned HTTP/OAuth subscribers with maintained equivalents. Assign a named owner in the PR description; the fork is first-party payment code the moment you create it.
Record fixtures, not just sandbox runs. Capture sandbox request/response pairs as JSON fixtures and write contract tests around them. With no upstream test suite, characterization tests are the only regression net you'll have when you next touch the signer.
Reconcile the postback leg separately. The wallet confirming payment and your server learning about it are two different events. Queue a recurring job that scans orders in awaiting_confirmation past N minutes and polls/voids them. Treat "wallet says paid, app says pending" as an expected state, not an exception.
Feature-flag the button. Roll out to one market/segment behind a flag. The kill switch matters more than the rollout when the gateway can deprecate under you without a release note.
Legacy Symfony usage (if that's your host): register in AppKernel, put the keys under the bundle's config root, inject the service by constructor argument into your checkout controller, and keep the controller thin — the bundle should own transport, your app should own orders.
The bundle's DI never runs on Laravel. That is the #1 time sink. Configuration.php keys are inert; services.yml is never compiled. Everything must be re-bound by you. If a config value "isn't being picked up," this is why.
OAuth 1.0a signing is where it fails, not HTTP. Expect 401 invalid signature from clock skew, a wrong signature base string, or percent-encoding differences (RFC 3986, not urlencode). Log the base string on failure, and make sure the server clock is NTP-synced. Request tokens are short-lived and single-use — a user who sits on the wallet page and comes back late will fail; handle that with a friendly restart path, not a 500.
Callback replay and duplicate completion. oauth_verifier can arrive twice (back button, refresh, retry). Make the callback idempotent: a unique constraint on the request-token mapping plus a status guard, so the second hit returns the same result instead of double-charging or double-completing.
Silent error swallowing. 2018-era PHP code often catches and continues or returns false. Instrument every client call with structured logging (endpoint, correlation ID, status, latency) before go-live. You will otherwise discover failures as missing orders, weeks later.
Composer drift is a permanent tax. Pin the lockfile, expect Dependabot alerts you must triage manually (no upstream patch path exists), and re-verify the sandbox after any dependency bump. Check TLS/cipher defaults too — old Guzzle versions negotiate settings gateways now reject.
Symfony component version skew against Laravel. Laravel vendors Symfony components; a bundle that constrains old ones can collide even when the package itself "installs." Isolate by keeping the extracted classes dependency-light (PSR HTTP client, no Symfony framework classes in the protocol layer).
PCI scope check. Determine from the code whether cardholder data ever posts to your server or whether it's a pure redirect + token handoff. It changes your compliance obligations and should be written down in the runbook, not inferred later.
Don't auto-enable for all traffic on install. Wire it behind an interface and a flag from day one; a dormant dependency with no CVE response path is a liability, not a feature, the moment it takes real money.
Extension points are yours, not the package's. There is no upstream event system to hook and no maintainer to accept a patch. The interface boundary (WalletGateway) and your fork are the extension surface — design accordingly, and write the runbook while you fork, because the retired product's docs are largely offline and nothing in-repo records your divergences.
How can I help you explore Laravel packages today?