Download the PHP package a7my/payment-hub without Composer
On this page you can find all versions of the php package a7my/payment-hub. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download a7my/payment-hub
More information about a7my/payment-hub
Files in a7my/payment-hub
Package payment-hub
Short Description Enterprise-grade provider-agnostic payment framework for Laravel 10+
License MIT
Homepage https://github.com/A7my/payment-hub
Informations about the package payment-hub
Laravel Payment Hub
A provider-agnostic payment framework for Laravel 10+ / PHP 8.2+, built around the Strategy pattern: switch payment providers via config, not application code. Stripe, Paymob, and MyFatoorah are built in today; PayPal is planned but not yet implemented — see Status below.
Install
No repositories block needed — the package is on Packagist.
Install from GitHub directly (before Packagist, or for unreleased commits)
Add to your app's `composer.json`:Publish config & migrations:
Set provider credentials in .env — see Environment variables below.
Never commit real keys; use placeholders in docs and keep secrets in .env only.
Environment variables
Shared:
Stripe — test mode is encoded in the key (sk_test_… / pk_test_…):
Paymob (KSA) — test mode is encoded in the key (sau_sk_test_… / sau_pk_test_…):
MyFatoorah — test mode is the host (apitest vs api-sa), not the key prefix.
Use MYFATOORAH_SANDBOX so MyFatoorah can stay live while Stripe/Paymob stay on test keys:
MYFATOORAH_COUNTRY_CODE |
Live host |
|---|---|
SAU |
https://api-sa.myfatoorah.com |
ARE / QAT / EGY |
api-ae / api-qa / api-eg |
KWT / BHR / OMN / JOR |
https://api.myfatoorah.com |
After changing .env: php artisan config:clear.
Driver reference docs
Provider-specific details (webhooks, callbacks, env quirks) live in separate files:
| Driver | Reference |
|---|---|
| Stripe | STRIPE.md |
| Paymob | PAYMOB.md |
| MyFatoorah | MYFATOORAH.md |
| Checkout (sdk, confirm, callbacks) | CHECKOUT.md |
Core concepts
- Two ways to call every method: a plain array (shown throughout this
README) or a hand-built DTO (
PaymentRequest,SubscriptionRequest, …). Arrays are converted to DTOs internally byPaymentRequestFactory— pick whichever is more convenient; nothing behaves differently either way. - Every response is an object, never a raw array:
isSuccessful(),getStatus(),getMessage(),getRawResponse()(the untouched provider payload, for debugging) are available on all of them. CheckisSuccessful()before trusting the outcome — some operations return a perfectly valid, non-throwing response that still represents a decline or an in-progress state (see each method below). idempotency_keyis optional on every call — if you don't pass one, a UUID is generated for you. Pass your own when you need to safely retry the same logical operation (e.g. a queued job retry) without risking a duplicate charge.- Amounts are always the smallest currency unit — cents for USD, so
1000= $10.00. - Exceptions vs. soft failures: a declined card comes back as a normal,
non-throwing response with
isSuccessful() === false— inspectgetStatus()/getMessage(). An actual unrecoverable error (bad API key, unknown transaction id, network failure) throws aMifatoyeh\LaravelPaymentFramework\Exceptions\PaymentExceptionsubclass.
Usage
This is how you call any driver — the calling code below is not
Stripe-specific. Every array key, method name, and response method
(isSuccessful(), getStatus(), …) is identical no matter which provider is
configured; only what happens behind the call differs per provider. Switch
providers by changing Payment::driver('…') or PAYMENT_DRIVER in .env —
no application code changes required. Stripe, Paymob, and MyFatoorah are
built in today (see Status).
Every example below uses $payment from this line.
charge() — take a payment immediately
authorize() / capture() / void() — reserve funds, then settle or release
refund() / partialRefund()
verify() / lookup() — read-only status checks
saveCard() / chargeToken() — save a card, charge it later
Saving a card creates a Stripe Customer behind the scenes (this package has
no other way to identify "this card belongs to this Stripe customer" later).
Persist getProviderReference() yourself — it's your only way to charge
that card again.
provider_customer_reference is required for chargeToken() — Stripe
rejects charging a saved payment method under any customer other than the
one it was originally attached to, so omitting it throws a clear
InvalidArgumentException before any API call is made, rather than a
cryptic Stripe error.
createSubscription() / cancelSubscription()
plan_id must be an existing Stripe Price ID (create it once via the Stripe
dashboard or API) — this driver does not support ad-hoc/inline subscription
pricing, and does not create Products/Prices on your behalf.
createPaymentLink() — hosted checkout page
See the dedicated walkthrough below.
createPaymentLink() walkthrough
createPaymentLink() itself is a generic driver method (same call shape for
any provider), but the explanation below of what happens behind it is
specifically how Stripe's driver currently implements it (via a Checkout
Session) — another provider's driver would generate the hosted page
differently internally, even though the call into it looks identical.
What each field does, and what actually happens on Stripe's side:
amount/currency/description— build a single, one-off line item on the fly (Stripe calls this "inline pricing"). No Stripe Price or Product needs to exist beforehand — that's specific tocreatePaymentLink();createSubscription()'splan_idabove does need a pre-existing Price.descriptionbecomes the line item's name on the hosted checkout page.customer— currently onlyemailis forwarded to Stripe (ascustomer_email, to prefill the checkout page).nameis accepted by the DTO but not currently sent to Stripe — a known limitation, not a bug; say if you want that wired up.return_url— required. Stripe calls thissuccess_url— where the browser lands after the customer completes payment. Important: landing on this URL is not proof the payment succeeded — see the warning below.cancel_url— optional. Where the browser lands if the customer backs out. Without it, Stripe just doesn't show a "back" button on the checkout page; nothing else changes.metadata— forwarded to Stripe as-is, visible on the Checkout Session in the Stripe dashboard. Handy for reconcilingorder_idagainst your own records later.$response->getPaymentUrl()— thehttps://checkout.stripe.com/...URL to send the customer to.$response->isSuccessful()here only means "the Checkout Session and URL were created successfully" — it says nothing about whether the customer has paid yet, since no charge happens at creation time. The actual charge happens later, asynchronously, when the customer completes the hosted page.
One thing to fix in the snippet above: return redirect($response->getPaymentUrl())
is correct — it sends an HTTP redirect. If you instead write
return $response->getPaymentUrl(); (dropping redirect()), Laravel just
returns the URL as plain text in the response body — the browser is never
actually redirected anywhere. Keep the redirect() wrapper.
Don't trust return_url alone to mark an order as paid. A customer can
reach return_url by hitting the browser back/forward buttons, refreshing,
or even guessing the URL, none of which mean they paid. Once webhook
handling (processWebhook()) lands in this package, listen for
checkout.session.completed there to confirm payment server-side. Until
then, use lookup() (via the transaction/session id) to confirm the actual
payment status before fulfilling an order — never fulfil directly off of a
return_url hit.
Generic checkout endpoint
Everything above requires you to write your own controller/route. This package can also expose a single, ready-to-use checkout endpoint that works against any of your own Eloquent models, with zero routes or controllers of your own — you implement one interface on a model, register it in config, and the endpoint already exists.
1. Implement Payable on your model
total_cents must already be in the smallest currency unit (cents), same as
everywhere else in this package. currency_code must be a value Currency::from()
accepts (e.g. 'USD', 'EGP') — either a plain string column or store a
Currency enum cast, both work.
If your amount isn't a single stored column (e.g. it's computed from related
rows), skip the trait and implement getPaymentAmount()/getPaymentCurrency()
yourself — they just need to return Money/Currency values, however you
get there.
Example — Package with a stored price in major units (SAR):
Optional — snapshot data at checkout time (CapturesCheckoutContext):
Some values only exist during the original POST /payment/checkout call (a
locked-in price, a discount code). Implement captureCheckoutContext() to
persist them into the pending transaction; read them back in
onPaymentCompleted() via $context->get('key') or $context->payer().
See CHECKOUT.md for confirmation flows (callback, webhook,
confirm()), and the app-wide CheckoutPaymentConfirmed event.
2. Register it in config
This is a deliberate allowlist, not optional boilerplate — the endpoint never
resolves a class from the request directly; only keys registered here are
reachable at all. An unregistered model_type gets a 422, not a lookup
attempt.
3. Call the endpoint from your frontend
Stripe (webview + web — package handles callback redirect):
Paymob (KSA):
MyFatoorah (Saudi):
The request body shape is identical for every driver — only "driver" changes.
model_type— the key you used inpayment.payables("order"above), not a class name.model_id— the record's primary key.driver—"stripe","paymob", etc. — must also be in that model's owngetSupportedPaymentDrivers().driver_type—"webview"(redirects to a hosted checkout page, same ascreatePaymentLink()) or"sdk"(returns a client-confirmable reference for a native/client-side SDK instead — a driver that doesn't support it yet returns a clear 422, not a silent wrong response). SeeCHECKOUT.mdfor the full sdk-mode walkthrough, per-model/per-driver payment callbacks, background verification, and transaction persistence — none of that is duplicated here.os—"web"or"mobile", required. Decides how you eventually learn the outcome — seeCHECKOUT.mdfor the one combination (webview+web) where this package handles the entire round trip (including redirecting the browser back toreturn_url) with noconfirm()call needed from you at all.return_url/cancel_url— forwebview+os: "web",return_urlis required and is where the browser lands after this package has verified the outcome (not forwarded to the provider directly — see CHECKOUT.md). For every other combination, both remain optional and, when present, are forwarded straight through to the driver'screatePaymentLink().
Success response (HTTP 200, webview mode):
Redirect the browser (or open a webview) to checkout_url. Failure responses
always look like {"status": "fail", "message": "..."}, with the HTTP status
telling you what kind of failure it was: 404 (no such record), 422
(unknown model_type, driver not allowed for this model, invalid
driver_type, or a driver that doesn't support the requested driver_type),
403 (authorizePayment() returned false), 502 (the provider API itself
failed).
Whichever driver_type you use, createPaymentLink()/createSdkIntent()
alone never confirms a payment happened — see
CHECKOUT.md for why POST {route}/confirm is a required
second step, not an optional extra.
Route configuration
Configured under payment.checkout in step 2 above. Change route if
/payment/checkout collides with something in your app, or set enabled to
false to register the route yourself. Don't rely on middleware alone —
authorizePayment() is always checked by the controller regardless of route
middleware.
Events
Every mutating operation dispatches a Laravel event you can listen for —
PaymentInitiated, PaymentSucceeded, PaymentFailed, PaymentCaptured,
PaymentVoided, PaymentRefunded, CardSaved, TokenCharged,
SubscriptionCreated, SubscriptionCancelled, PaymentLinkCreated,
TransactionLookuped — each carrying the original request DTO and the
resulting response.
Status
This package is under active development. Driver method support, per provider:
| Driver | charge / authorize / void / capture / refund | verify / lookup | saveCard / chargeToken | subscriptions | createPaymentLink | sdk checkout | webhooks |
|---|---|---|---|---|---|---|---|
| Stripe | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Paymob | ✅ | ✅ | ✅ | 🚫 | ✅ | ✅ | ⚠️ |
| MyFatoorah | ✅ | ✅ | 🚫 | 🚫 | ✅ | ✅ | ⚠️ |
| PayPal | — | — | — | — | — | — | planned, not started |
✅ implemented and tested · 🚧 not yet implemented (throws until it is) · 🚫 not supported by this provider (throws UnsupportedOperationException) · ⚠️ implemented, but unverified against a real provider signature — see CHECKOUT.md before relying on it in production
Stripe's and Paymob's webhooks both double as the automatic checkout-confirmation trigger — see CHECKOUT.md.
Two things worth knowing about Paymob specifically before relying on it:
- It has no official SDK, so its driver was built from general API knowledge and live-debugged against real errors rather than verified against SDK source the way Stripe's was — treat it as less battle-tested.
createSubscription()/cancelSubscription()are permanently unsupported, not just unimplemented — Paymob has no recurring-billing API resembling Stripe's Subscription object.
MyFatoorah-specific notes:
- Test vs live is determined by host (
MYFATOORAH_SANDBOX/PAYMENT_SANDBOX→apitest), not by a key prefix like Stripe/Paymob — seeMYFATOORAH.md. saveCard()/chargeToken()/ subscriptions are permanently unsupported.- For webview checkout, MyFatoorah uses
CallBackUrl(package callback route); webhooks are for sdk-mode intents — seeCHECKOUT.md.
Publishing (maintainers)
To make composer require a7my/payment-hub work for everyone without a VCS
repository block, submit this repo to Packagist:
- Public GitHub repo —
https://github.com/A7my/payment-hubmust be public. -
Create a release tag (Packagist needs at least one tag for stable installs):
Supported frameworks: Laravel 10 / 11 / 12 / 13 (
laravel/framework: ^10|^11|^12|^13), PHP ^8.2. - Register on Packagist — sign in at packagist.org with GitHub.
- Submit package — paste
https://github.com/A7my/payment-huband confirm. - Enable auto-update — on the package page, set up the GitHub webhook so new tags/commits sync automatically.
After that, anyone can run composer require a7my/payment-hub with no extra config.
License
MIT
All versions of payment-hub with dependencies
laravel/framework Version ^10.0|^11.0|^12.0|^13.0
stripe/stripe-php Version ^13.0|^14.0|^15.0|^16.0|^17.0|^18.0|^19.0|^20.0