Replace the single-file SDK with a layered PSR-4 package - #26
Merged
Conversation
Add PSR-4 autoload for the Unitpay\ namespace alongside the existing classmap (strangler bridge) and wire src/ into phpstan, php-cs-fixer and the lint glob. Extract the exception hierarchy into src/Exception/ and CashItem plus the 54-FZ dictionaries (Nds, PaymentObject, PaymentMethod, Measure, PaymentType) into src/Model/ and src/Model/Enum/ as PHP 7.4-safe const-classes. The single-file UnitPay.php still powers the SDK; the new layers only coexist.
Add Unitpay\Http (TransportInterface + CurlTransport with the cURL/file_get_contents fallback and TLS on), Unitpay\Signature\SignatureBuilder (with the PHP_INT_MAX forgery guard and locale-safe float serialization), and Unitpay\Webhook (WebhookVerifier + IpAllowlist) covering signature + IP-allowlist verification, fail-safe refresh, and handler responses. Standalone testable units; the facade wiring and single-file removal come next.
Add Unitpay\Api (AbstractService request pipeline with telemetry fingerprint, PendingParams fluent-param holder, and typed PaymentService/SubscriptionService/PayoutService/ReferenceService whose method names mirror the backend) and the Unitpay\Unitpay facade (composition root: lazy service getters, fluent setters, form(), webhook() accessor). The global UnitPay.php still powers the SDK and stays until the test port so the single-file removal is one green cutover.
Rework tests/ to mirror src/. The callable transport double becomes
Tests\Support\FakeTransport, a TransportInterface implementation that records
the URL and headers of each call and replays a queue of canned bodies. Adds
focused coverage for the new seams: facade service getters, their memoization,
and the fact that one injected transport serves both the API services and the
webhook IP-feed fetch. 121 -> 150 tests, 318 assertions.
Remove the legacy UnitPay.php along with its classmap autoload entry, PHPStan
path, php-cs-fixer append and the lint/md script globs.
Two tests are deliberately gone: api('doesNotExist') is no longer a code path,
so the unsupported-method case is covered only where it still applies, in
WebhookVerifier. Per-service method-name coverage replaces the old allowlist
test, and required-param validation - now enforced by the service method
signatures rather than a runtime dictionary - is pinned via ArgumentCountError.
examples/ still require the removed file and are updated separately.
Every example now autoloads from vendor/ and uses the Unitpay\ namespace:
api('method', $params) becomes the matching service call, the webhook handler
goes through webhook(), and the CashItem dictionaries come from Model\Enum
(Nds, PaymentObject, PaymentMethod, Measure, PaymentType).
Account-level calls read better under the new signatures: the login is the
first argument and only the account secretKey stays in the options array.
Note that getBinInfo lives on the payouts service, which accountInfo.php now
reflects.
Verified past parallel-lint, which only sees syntax: every `use Unitpay\...`
import resolves through the real autoloader, and PHPStan level 0 over
examples/ is clean, so method names and arity are checked too.
README and docs/ now describe the service API: Unitpay\Unitpay as the entry point, payments()/subscriptions()/payouts()/reference() for calls, webhook() for inbound verification, and Model\Enum for the fiscal dictionaries. The API reference is grouped by service with full method signatures instead of one flat api() table. Adds docs/migration-v3.md: every class, method and constant rename from 2.x, with the old names taken from the pre-removal UnitPay.php rather than guessed. It opens with what did not change - signature algorithm, wire format, webhook semantics, exception hierarchy - since that is most of the surface. The v3.0.0 changelog entry lists the breaking changes and records two decisions explicitly: no compatibility shim ships, and the payment objects v2.1.0 announced for "removal in 3.0" (excise, gambling_bet, gambling_prize, lottery_prize, composite) are kept and deferred to 4.0 instead of lapsing silently.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Splits the single-file SDK into a layered PSR-4 package (
Unitpay\→src/) behind a thinUnitpay\Unitpayfacade. Nothing changed on the wire: same endpoints, same flat requestformat, same signature algorithm, same response shapes. What changed is the PHP surface you
call. Still
php >=7.4andext-json, cURL still optional, no runtime dependencies added.Architecture
Layers, with dependencies pointing strictly downward:
Unitpayform(), fluent setters, service gettersApi/AbstractServiceplus thePayment/Subscription/Payout/Referenceservices andPendingParamsHttp/TransportInterfaceport andCurlTransportadapterSignature/SignatureBuilderWebhook/WebhookVerifierandIpAllowlistModel/,Model/Enum/CashItemand the const-classesNds,PaymentObject,PaymentMethod,Measure,PaymentTypeException/Model/,Model/Enum/andException/are leaves and import nothing internal. Servicesnever construct a transport. They receive
TransportInterfacefrom the facade, which ownsthe single shared instance (it also serves the webhook IP feed), so a custom HTTP stack is
injected in one place.
Breaking changes
UnitPay.phpis gone, so installation is Composer-only. The package needs the PSR-4autoloader and can no longer be
required as a single file.UnitPaybecomesUnitpay\Unitpay(note the casing),CashItembecomesUnitpay\Model\CashItem,UnitpayIpAllowlistbecomesUnitpay\Webhook\IpAllowlist, andevery exception moves into
Unitpay\Exception\. Exception class names and theirInvalidArgumentException/UnexpectedValueExceptionparents did not change, soexisting
catchblocks keep working once the imports are updated.api('method', [...])is replaced by four service objects reached from the facade:payments(),subscriptions(),payouts()andreference(). Each method takes itsrequired parameters as arguments and the rest in a trailing options array. Watch out for
getBinInfo: it sits onpayouts(), next togetSbpBankList, not onreference().REQUIRED_UNITPAY_METHODS_PARAMSdictionary. A missing one is anArgumentCountErrorthat static analysis catches, rather than a
UnitpayValidationExceptionat request time.UnitpayUnsupportedMethodExceptionno longer applies to outbound calls, since there is nomethod-name string left to mistype; it covers inbound webhooks only.
Model\Enumand lost the redundantprefix:
CashItem::NDS_20becomesNds::VAT20,CashItem::PAYMENT_OBJECT_COMMODITYbecomes
PaymentObject::COMMODITY,CashItem::PAYMENT_METHOD_PAYMENT_FULLbecomesPaymentMethod::PAYMENT_FULL,CashItem::MEASURE_KGbecomesMeasure::KG, andUnitPay::PAYMENT_TYPE_CARDbecomesPaymentType::CARD. One case is irregular:CashItem::NDS_NONEbecomesNds::NONE, with noVATprefix.$unitpay->webhook(), that isUnitpay\Webhook\WebhookVerifier:checkHandlerRequest(),getHandlerMethod(),getHandlerParams(),getSuccessHandlerResponse(),getErrorHandlerResponse(),setAllowedIps(),addAllowedIps(),getAllowedIps()andrefreshAllowedIps().getIp()andisAllowedIp()stayprotected, so running behind a proxy meanssubclassing the verifier, not the facade.
getSignature()is no longer public on the facade. Direct signing lives inUnitpay\Signature\SignatureBuilder::build($params, $secretKey, $method).callabletoTransportInterface.Omit it and you still get the default
CurlTransportwith unchanged behavior: cURL witha
file_get_contents()fallback, TLS verified.Explicit decisions
No compatibility shim ships. A
class_aliaswould restore the old class names without theold methods and constants, so code would compile and then fail at runtime. A real compat
layer would re-create the god class this release removes, and it would force dropping
finalfromUnitpay\Model\CashItem. Migration is a mechanical one-time edit instead.The deprecated payment objects stay for now.
excise,gambling_bet,gambling_prize,lottery_prizeandcompositewere announced in v2.1.0 for removal in 3.0, but pullingthem alongside the namespace break adds migration friction for no functional gain. They are
deferred to 4.0 and remain available as
PaymentObject::EXCISEand friends. They should notbe used in new code.
Unchanged, and verified as such
The signature algorithm and the
{up}delimiter are untouched, as is the mandatoryPHP_INT_MAXguard against signature forgery inSignatureBuilder::build(), which tworegression tests cover. Also unchanged: constant-time comparison via
hash_equals(), theREMOTE_ADDR-only IP source (never the spoofableX-Forwarded-For), TLS verification onthe IP-feed fetch, the fail-safe allowlist refresh, the flat request format, the response
shapes, PHP >= 7.4 support and the zero-dependency policy.
Tests & QA
assertions. It now mirrors the
src/tree.Tests\Support\FakeTransportreplaces the old callable double. It implementsTransportInterface, records the URL and headers of every call, and replays a queue ofcanned bodies; pass
falseto simulate a transport failure.injection,
PendingParamsdraining.composer.jsonmoves fromclassmaptopsr-4,lintandmdnow target
src, andphpstan.neonand.php-cs-fixer.dist.phpdo the same.Docs
docs/getting-started.md,docs/api-methods.md,docs/receipts.md,docs/webhooks.md,docs/telemetry.md) were rewritten for theservice API.
docs/api-methods.mdnow groups methods by service and gives fullsignatures.
docs/migration-v3.mdis new: 8 sections plus a checklist, with exact 2.x to 3.0 renametables.
examples/*.phpnow usevendor/autoload.phpand the namespaced API.Verification
The CI matrix was replicated locally in Docker before pushing. PHP 7.4.33, 8.0.30, 8.1.34,
8.2.32, 8.3.32 and 8.4.23 all pass: 150 tests and 318 assertions on each, 55 files linted
clean. The quality job was replayed on 8.3, where php-cs-fixer reports 0 of 55, PHPMD is
clean, PHPStan level 6 says
[OK] No errors, andcomposer auditfinds nothing.One caveat is worth a follow-up. PHPStan exhausts the default 128M
memory_limitin aparallel worker and needs
--memory-limit=512M, so the result currently depends on theenvironment's
php.ini. Pinning the limit inphpstan.neonor in thecomposer stanscript would make it deterministic. That change is not part of this PR.
Merge note
The branch was rebased onto
master, somasteris an ancestor and this mergesfast-forward. Please do not squash: the six commits are ordered so that each one builds and
passes tests on its own.