Download the PHP package echodial/cleat without Composer

On this page you can find all versions of the php package echodial/cleat. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package cleat

Cleat

Self-hosted billing and invoicing for PHP 8.2. Subscriptions, invoices, hosted invoice pages and reusable payment links, with every piece of billing state in your own MySQL or MariaDB tables. Authorize.net stores card tokens (CIM) and moves money. Nothing else lives there.

Part of the EchoDial stack: Helm (local server), Keel (SaaS starter kit), Deck (CSS framework). Keel will offer Cleat and Stripe side by side behind a shared billing interface.

Layer What it does Runs on
Cleat Billing (this package) One-time charges, subscriptions, invoices, hosted invoice links, payment links Authorize.net
Cleat Connect (planned) Connected accounts, split payments, payouts A PayFac-as-a-Service driver (likely Finix)

Connect's interface and tables already exist so nothing needs migrating later. Until a Connect driver ships, anything given a connected account throws ConnectNotConfiguredException rather than charging the platform.


Contents

  1. Requirements
  2. Install
  3. Configure
  4. Catalog: products and prices
  5. Customers and cards
  6. Subscriptions
  7. One-time charges and invoices
  8. The hosted invoice page
  9. Payment links and checkout
  10. Refunds
  11. The runner (cron)
  12. Dunning
  13. Webhooks
  14. Events
  15. Email
  16. Templates
  17. Security model
  18. How charging stays safe
  19. Connect (stub)
  20. Testing
  21. Schema notes
  22. Not yet verified against a live sandbox

Requirements

No other runtime dependencies.

Install

Run the migration with your own migration tool. It is plain SQL and safe to run twice:

Or from PHP: Cleat\Support\Schema::migrate($pdo);

Configure

Copy vendor/echodial/cleat/config/cleat.php into your app, fill it in, and call configure() once per request (or once per worker):

Key Default Notes
gateway 'fake' 'authorizenet' for real money.
authorizenet.login_id / transaction_key '' API credentials. The transaction key stays on the server and is never logged.
authorizenet.client_key '' Public key for Accept.js.
authorizenet.signature_key '' Webhook HMAC key. Webhooks are refused while it is empty.
authorizenet.sandbox true Sandbox endpoints, Accept.js from jstest.authorize.net, CIM testMode.
connect.driver null Must stay null until a Connect driver ships.
currency 'USD' Default currency for new invoices.
invoice_prefix 'CLT-' Numbers look like CLT-000001. At most 12 characters, so the number fits Authorize.net's 20.
base_url 'http://localhost' Used to build pay and checkout URLs.
routes /pay/{token}, /checkout/{token} Paths your app serves the two pages on.
app_key '' 32+ byte secret for CSRF. Required outside fake mode: Cleat refuses to boot without it.
brand name, logo_url, support_email, plus hue (Deck --hue-brand) and color (email button).
invoice_link_days 30 Hosted link lifetime from finalize, extended whenever the invoice is (re)sent. null = until paid or void.
rate_limits see file per_ip, per_link, failures_per_ip as [max, window seconds].
automation see Dunning Retry schedule and what happens after it.
grace_days 0 Days a past_due subscription still counts as active().
deck_css jsDelivr @echodial/[email protected] Or a path on your own site. The CSP follows this origin.
templates_path null A folder checked before the package's templates (see Templates).

Generate an app key with php -r "echo bin2hex(random_bytes(32));".

Catalog: products and prices

A price's amount and interval never change. To change what something costs, create a new price and deactivate() the old one. Existing subscriptions keep the price they were sold.

Customers and cards

create() also creates the customer's CIM profile. To save a card, tokenize it in the browser with Accept.js (the bundled templates/partials/payment_form.php does this) and pass only the opaque data:

The card becomes the default payment profile, and its brand, last four and expiry are stored locally. The previous profile is deleted from CIM. A raw card number passed anywhere throws InvalidArgumentException before any network call.

Subscriptions

State checks:

Changes:

Proration uses the seconds left in the current period. The unused part of the old price becomes a credit line and the rest of the period at the new price becomes a debit line, both marked is_proration. A positive net is invoiced and charged immediately. A negative net waits as pending lines and comes off the next renewal; a credit bigger than the renewal carries forward again. Period dates never change on a swap. If the immediate charge fails, the swap still stands and the invoice enters dunning.

Period math works from the subscription's anchor day, so short months do not drift it: Jan 31 → Feb 28 (29 in a leap year) → Mar 31 → Apr 30.

One-time charges and invoices

Statuses: draft → open → paid | void | uncollectible. Numbers come from a row-locked sequence inside the finalize transaction, so they are gapless: drafts have no number, and a rolled-back finalize gives its number back. Line items are frozen once the invoice is finalized.

The hosted invoice page

The customer opens the link from the invoice email and pays without logging in: with their saved card, a new card, or a new card they choose to save.

Situation Response
Unknown or draft token 404
Link expired, invoice void or uncollectible 410
Already paid 200, "this invoice is paid" view
Open 200, invoice and card form
Declined 402, form again with a generic message (never the gateway's reason text)
Held for fraud review 200, "payment under review"
Bad or missing CSRF token 403
Rate limited 429, plain text, Retry-After

Paying a past_due subscription's invoice reactivates the subscription and clears its scheduled retry. In Keel, Laravel or Slim, convert the Response into the framework's own response object instead.

Payment links and checkout

CheckoutPage is wired exactly like InvoicePayPage. On submit it checks rate limits, then CSRF and the optional captcha, then validates input. Next it finds or creates a guest customer and either charges a one-time price (nothing stored) or saves the card and starts a subscription (honouring the price's trial). Finally it counts the use atomically.

If two buyers race for the last use of a max_uses link, both may pass the "sold out?" check while their cards are being charged. The one that counts its use second is refunded (or voided if the charge has not settled), its invoice is voided, and it gets a 410 saying so. This is tested with two real PHP processes against the database.

A recurring checkout never replaces a stored card that another guest's renewals depend on. If a guest with that email already has a card, a new guest record is created.

Refunds

Authorize.net only refunds settled transactions. For an unsettled one (error 54), a full refund becomes a void, and $refund->method says which happened. A partial refund of an unsettled charge cannot be voided and throws RefundFailedException explaining that it must wait for settlement.

The runner (cron)

Everything time-based happens in Billing\Runner. Run it every 15 minutes:

cleat-bootstrap.php is any file that calls Cleat::configure(...). Options: --now="2026-03-01 00:00:00" runs as of a given UTC time, and --json prints the report. Exit code 0 means all good, 1 means some rows failed (each is logged; the rest still ran), 2 means bad usage.

Each run, in order:

  1. Trials ending now or earlier: invoice the first paid period and charge it. No card on file takes the past-due path, with a pay-link email.
  2. Active subscriptions whose period has ended (and are not set to cancel): roll the period, create the renewal invoice (with any pending proration lines), charge it.
  3. Subscriptions set to cancel whose ends_at has passed: mark them canceled.
  4. Open invoices with a retry due: retry through dunning.
  5. Deactivate expired payment links and prune rate-limit rows older than 24 hours.

It is idempotent. Every step is claim-then-act: one short transaction locks a row (SELECT ... FOR UPDATE), re-checks it, and changes it so it no longer qualifies (rolls the period, sets a one-hour lease) before any gateway call. A second run at the same moment, or a second server, finds nothing to claim. Overlapping runs are also prevented with a named lock. From code: (new Runner())->run($now).

Dunning

Every failed charge is recorded, and PaymentFailed fires with the attempt number and whether a retry is scheduled. That much always happens.

dunning_enabled = true. For automatic charges on subscription invoices (renewals, trial conversions, prorations):

dunning_enabled = false. Nothing changes, nothing is scheduled, and nothing is sent. Only the record and the event remain. What happens next is up to you, and the pay link still works if you send it.

Customer attempts on the pay page, first payments of new subscriptions, and one-off invoices never enter the retry schedule.

Webhooks

Event What Cleat does
net.authorize.payment.authcapture.created Reconciles a charge whose response never arrived (timeout, crash) by invoice number and amount, so the money is recorded and never taken twice.
net.authorize.payment.fraud.approved / .declined Settles a held charge: paid, or failed and into dunning.
net.authorize.payment.refund.created / void.created Records refunds and voids made in the merchant interface.
net.authorize.customer.paymentProfile.deleted Clears the stored card fields.
anything else Stored, ignored.

Events

PaymentSucceeded, PaymentFailed, SubscriptionCreated, SubscriptionCanceled, SubscriptionPastDue, InvoiceCreated, InvoiceSent, InvoicePaid, CheckoutCompleted, and ChargeRefunded.

Events fire after the database work they describe has committed; an event raised in a transaction that rolls back is dropped. By default a listener that throws is logged and skipped, because by then money has moved. Call Cleat::events()->throwListenerExceptions() in tests.

Email

Cleat sends three emails: the invoice (with a "Pay invoice" button), payment failed (with the pay link), and the receipt (also used for "your trial has started"). Each comes as HTML plus plain text, through any mailer you wrap:

Without a mailer Cleat uses NullMailer. ArrayMailer keeps messages in memory for tests. A mailer failure is logged and never undoes billing work.

Templates

Plain PHP with Deck classes, in templates/:

File Used for
invoice.php $invoice->render(): screen, PDF, print (A4 and Letter)
pay_invoice.php the hosted invoice page
checkout.php, checkout_success.php payment links
error.php 403 / 404 / 410 pages
emails/invoice.php, emails/payment_failed.php, emails/receipt.php email, inline styles, bulletproof table buttons
partials/payment_form.php the Accept.js card form shared by every page that takes a card

To customise one, set templates_path and drop a file with the same relative path (for example emails/receipt.php) into that folder. Every template gets $e() (escape), $partial() and $json(). Every printed value goes through $e(), which a test enforces.

Deck loads from jsDelivr (@echodial/[email protected]/dist/deck.min.css) unless deck_css points elsewhere. Every class the templates use is a public class in Deck's published API, or a .cleat-* class in Cleat's own scoped style block; a test checks this against the installed package. Pages are mobile first and checked at 375px and 1280px, in light and dark mode.

In fake mode the card form shows a test-card picker instead of loading Accept.js, so the whole flow works locally with no Authorize.net account.

Security model

How charging stays safe

Every charge follows the same three steps:

  1. Claim, in one transaction. Lock the invoice row. Refuse if another charge on it is pending or held. Write a pending charge row with a unique idempotency key (inv_{id}_attempt_{n} for automatic attempts) and bump attempt_count. Commit.
  2. Call the gateway, outside any transaction, with a 30-second timeout.
  3. Record, in one transaction. Write the outcome. On success, mark the invoice paid and reactivate its subscription.

A crash between 1 and 3 leaves a pending row that blocks every further attempt until it is reconciled, by the authcapture webhook or by hand.

Unknown outcomes. A timeout, a dropped connection or an unreadable response is recorded as failed with reason timeout (or network, gateway_exception, invalid_response) and is never retried in the same run. The money may still have moved, so such a charge blocks every further attempt on its invoice until it is settled:

Authorize.net has no idempotency key. refId is only echoed back, so these guarantees come from Cleat's own rows and locks.

Cleat refuses to charge while a database transaction is open on its connection, because a host transaction would keep the pending row from being committed first. Don't wrap Cleat payment calls in your own transaction.

Connect (stub)

Connect\ConnectGatewayInterface defines createAccount, onboardingUrl, refreshAccount, chargeOnBehalfOf, transfer, payout and balance. Cleat::connect() returns NullConnectGateway, whose methods all throw ConnectNotConfiguredException. Connect\ConnectedAccount reads and writes cleat_connected_accounts with no gateway calls. cleat_transfers and cleat_payouts exist and are unused. Application fees are computed from application_fee_percent with Money::percentage(), which uses integer math and banker's rounding. Connect never routes through AuthorizeNetGateway.

Testing

The suite runs against MariaDB or MySQL with the FakeGateway. It creates and rebuilds cleat_test from the real migration on every run. Set CLEAT_TEST_DB_HOST, _PORT, _NAME, _USER and _PASS to point it elsewhere (defaults: 127.0.0.1, 3306, cleat_test, root, empty). Tests run with MySQL 8's default strict sql_mode.

To try everything in a browser with no Authorize.net account:

FakeGateway decides outcomes by token: tok_decline, tok_insufficient, tok_error, tok_held, tok_timeout, tok_timeout_captured, tok_invalid; anything else approves (tok_visa, tok_mastercard, tok_amex, tok_discover pick a brand).

Schema notes

database/migrations/001_create_cleat_tables.sql follows the original spec, with four small additions:

All amounts are BIGINT cents. All timestamps are DATETIME written in UTC, so the connection's time zone can never shift them.

Not yet verified against a live sandbox

Everything above is covered by tests against the FakeGateway and a recorded Authorize.net transport. These pieces have not been exercised against a real Authorize.net sandbox yet, and should be before the first live charge:

MySQL 8 is covered by the CI workflow in .github/workflows/ci.yml; locally the suite has run on MariaDB 10.11 in MySQL 8 strict mode.

License

MIT


All versions of cleat with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
ext-curl Version *
ext-json Version *
ext-pdo Version *
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package echodial/cleat contains the following files

Loading the files please wait ...