Skip to content

Ship HeaderAwareGetHttpRequestAction in the package - #64

Merged
loevgaard merged 5 commits into
2.xfrom
feat/header-aware-get-http-request
Aug 17, 2026
Merged

Ship HeaderAwareGetHttpRequestAction in the package#64
loevgaard merged 5 commits into
2.xfrom
feat/header-aware-get-http-request

Conversation

@loevgaard

@loevgaard loevgaard commented Aug 10, 2026

Copy link
Copy Markdown
Member

Fixes gap G2 of #57.

Problem

payum/core's plain-PHP GetHttpRequest bridge never populates GetHttpRequest::$headers, and NotifyAction reads the QuickPay-Checksum-Sha256 header off exactly that property. So on a plain-PHP Payum every callback is rejected as unsigned (400) — silently, for every payment — unless the consumer found and copied the helper class hidden in examples/e2e/. That's a total failure of the notify path for a whole class of consumers, shipped as dev tooling.

Fix

  • The action now lives in the package proper: Setono\Payum\Quickpay\Bridge\PlainPhp\Action\HeaderAwareGetHttpRequestAction, with the registration snippet (addCoreGatewayFactoryConfig(['payum.action.get_http_request' => …])) in its docblock and in docs/UPGRADE-2.0.md. It is not auto-registered: the core gateway factory already defines that key, so a default could never take effect, and force-setting it would stomp the Symfony bridge for Symfony/Sylius consumers who don't need this.
  • Headers are rebuilt from $_SERVER only — no getallheaders(). That function exists only on some SAPIs, and the polyfill that fills the gap (ralouphie/getallheaders) is old and passes $_SERVER values through unsanitized. $_SERVER is available on every SAPI and carries every request header under the CGI HTTP_* convention, so it's the one source that behaves identically everywhere. The two entity headers the CGI spec leaves unprefixed (CONTENT_TYPE, CONTENT_LENGTH) are mapped too; non-scalar values are dropped. NotifyAction matches the header name case-insensitively, so the reconstructed casing doesn't matter.
  • The e2e harness uses the shipped class; its local copy is deleted.
  • GetHttpRequest is declared a universalObjectCratesClass for PHPStan — headers is a dynamic property (payum marks the class #[AllowDynamicProperties]), and this is PHPStan's documented way to accept that contract.

The README section pointing plain-PHP consumers at this class comes in #66, which is stacked on this one.

Tests

HeaderAwareGetHttpRequestActionTest (class at 100% lines/methods): unsupported request throws; headers populated alongside the parent's fields; the reconstructed checksum-header name is findable by NotifyAction's case-insensitive match; Content-Type/Content-Length mapped; non-header and non-scalar $_SERVER entries dropped, scalars stringified.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.16%. Comparing base (4aeed1e) to head (3a4a98a).
⚠️ Report is 4 commits behind head on 2.x.

Additional details and impacted files
@@             Coverage Diff              @@
##                2.x      #64      +/-   ##
============================================
+ Coverage     96.93%   97.16%   +0.22%     
- Complexity      144      163      +19     
============================================
  Files            15       16       +1     
  Lines           392      423      +31     
============================================
+ Hits            380      411      +31     
  Misses           12       12              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

payum/core's plain-PHP GetHttpRequest bridge never populates the headers
property, and NotifyAction reads the QuickPay-Checksum-Sha256 header off
exactly that property — so on a plain-PHP Payum every callback was
rejected as unsigned with a 400, silently, for every payment, unless the
consumer found and copied the class hidden in examples/e2e/.

The action now lives in the package proper
(Setono\Payum\Quickpay\Bridge\PlainPhp\Action) with a registration
snippet in its docblock and the upgrade guide; the e2e harness uses the
shipped class and its local copy is gone. Headers from getallheaders()
(real or polyfilled — guzzle ships one that passes $_SERVER values
through untouched) and from the $_SERVER fallback are sanitized the
same way, so the result is deterministic across SAPIs.

GetHttpRequest is declared a universalObjectCratesClass for PHPStan:
headers is a dynamic property (payum marks the class
AllowDynamicProperties), and this is PHPStan's documented way to accept
that contract.
Under PHPUnit a getallheaders() polyfill is loaded (guzzle ships
ralouphie/getallheaders), so the default source never reached the
$_SERVER reconstruction and codecov reported the class at 62%: the
fallback every SAPI without getallheaders() relies on was dead code in
CI.

The raw-header source is now an injectable callable defaulting to
defaultHeaderSource() (getallheaders() when present and non-empty, else
headersFromServer()), and headersFromServer() is a public static so the
reconstruction is exercised directly and deterministically — with or
without a polyfill on the include path. Line coverage 62% -> 91%; the
rest is the getallheaders()-absent branch, unreachable while the
polyfill is loaded.
The dependency analyser resolves getallheaders() to guzzle's dev-only
polyfill (ralouphie/getallheaders) and reports it as a shadow dependency.
It is a PHP-internal function on the Apache/FPM SAPIs, and
HeaderAwareGetHttpRequestAction guards it with function_exists() and a
$_SERVER fallback precisely so it works with or without the polyfill —
depending on the polyfill would be the wrong fix.
@loevgaard
loevgaard force-pushed the feat/header-aware-get-http-request branch from 9d14bc2 to 5fcb7f9 Compare August 17, 2026 06:56
Two real gaps behind the remaining codecov failure on #64: the
unsupported-request guard in execute() was only exercised through
supports(), and the fall-through when getallheaders() exists but returns
an empty list was never driven. Both are tested now — the latter
deterministically by clearing every HTTP_* entry so the polyfill has
nothing to read.

defaultHeaderSource() also collapses its two nested ifs into the single
decision they really are ("did the SAPI hand us headers?"): a missing
getallheaders() and one answering with an empty list are the same case,
and the $_SERVER reconstruction serves both. Same behavior, no branch
left that is unreachable while a polyfill is autoloaded.
getallheaders() exists only on some SAPIs, and the polyfill that fills
the gap (ralouphie/getallheaders) is old and passes $_SERVER values
through unsanitized — so the action was juggling two sources, needed an
injectable header source to test the fallback, and needed a dependency-
analyser ignore because the symbol resolved to the polyfill.

$_SERVER is available on every SAPI and carries every request header
under the CGI HTTP_* convention, so it is the one source that behaves
the same everywhere. The action now reads it exclusively (mapping the
two entity headers the CGI spec leaves unprefixed, CONTENT_TYPE and
CONTENT_LENGTH, as well); the injectable source, the polyfill-specific
tests and the analyser ignore are gone.
@loevgaard
loevgaard merged commit 7e6ed13 into 2.x Aug 17, 2026
21 checks passed
@loevgaard
loevgaard deleted the feat/header-aware-get-http-request branch August 17, 2026 07:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant