From 7f50f3d7a645cdc252d12e7d587602f37c7ec37d Mon Sep 17 00:00:00 2001 From: jules Date: Mon, 26 Oct 2026 09:54:27 +0200 Subject: [PATCH 1/3] Support PHP 8 composer.json only allowed PHP 7.x, so the package could not be installed on any PHP release from the 21 Oct 2026 security update (8.2.35, 8.3.36, 8.4.27, 8.5.12). - Allow "^7.2 || ^8.0". The floor moves to 7.2 because the explicit nullable type and omnipay/tests ^4.1 need it; projects on 7.0/7.1 stay on the previous release. - Make sendRequest()'s $data explicitly nullable (implicit nullable types are deprecated in PHP 8.4). - Guard response getters against missing keys so error and partial payloads don't raise "Undefined array key" warnings on PHP 8. Return values are unchanged. - Cast the transaction reference before rawurlencode() (passing null is deprecated in PHP 8.1). - omnipay/tests ~3.0 -> ^4.1 (PHPUnit 8.5-11), setUp(): void, and a phpunit.xml.dist those versions all accept. - Drop phpro/grumphp: 0.14 only installs with Composer 1 and current releases need PHP 8.2+. - Disallow the php-http/discovery Composer plugin; it isn't needed. - Add tests for completePurchase() (success, declined, unknown reference, no reference, partial payload) and for failed purchase and refund responses, with the previously empty failure mocks filled. Co-Authored-By: Claude Opus 5.5 --- composer.json | 12 ++- grumphp.yml | 15 --- phpunit.xml.dist | 16 +-- src/Message/AbstractRequest.php | 2 +- src/Message/CompletePurchaseRequest.php | 4 +- src/Message/CompletePurchaseResponse.php | 12 +-- src/Message/PurchaseResponse.php | 6 +- src/Message/RefundResponse.php | 6 +- tests/GatewayTest.php | 125 ++++++++++++++++++++++- tests/Message/AuthorizeRequestTest.php | 2 +- tests/Message/ResponseTest.php | 2 +- tests/Mock/CompletePurchaseFailure.txt | 21 ++++ tests/Mock/CompletePurchaseNotFound.txt | 10 ++ tests/Mock/PurchaseFailure.txt | 10 ++ tests/Mock/RefundFailure.txt | 10 ++ 15 files changed, 202 insertions(+), 51 deletions(-) delete mode 100644 grumphp.yml create mode 100644 tests/Mock/CompletePurchaseNotFound.txt create mode 100644 tests/Mock/RefundFailure.txt diff --git a/composer.json b/composer.json index 461fc54..7ff8e6d 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,12 @@ } ], "require": { - "php": "^7.0", + "php": "^7.2 || ^8.0", "omnipay/common": "^3.0" }, "require-dev": { - "omnipay/tests": "~3.0", - "squizlabs/php_codesniffer": "^3", - "phpro/grumphp": "^0.14" + "omnipay/tests": "^4.1", + "squizlabs/php_codesniffer": "^3" }, "autoload": { "psr-4": { @@ -43,6 +42,11 @@ "dev-master": "1.0-dev" } }, + "config": { + "allow-plugins": { + "php-http/discovery": false + } + }, "minimum-stability": "stable", "prefer-stable": true } \ No newline at end of file diff --git a/grumphp.yml b/grumphp.yml deleted file mode 100644 index 4b767a0..0000000 --- a/grumphp.yml +++ /dev/null @@ -1,15 +0,0 @@ -parameters: - git_dir: . - bin_dir: vendor/bin - tasks: - phpunit: - config_file: ~ - testsuite: ~ - group: [] - always_execute: false - phpcs: - standard: PSR2 - warning_severity: ~ - ignore_patterns: - - tests/ - triggered_by: [php] \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 535809e..433fc3c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,22 +1,10 @@ - + stopOnFailure="false"> ./tests/ - - - ./src - - diff --git a/src/Message/AbstractRequest.php b/src/Message/AbstractRequest.php index fbc9bec..48c14a5 100644 --- a/src/Message/AbstractRequest.php +++ b/src/Message/AbstractRequest.php @@ -37,7 +37,7 @@ public function setSecretKey($value) $this->setParameter('secret_key', $value); } - protected function sendRequest($method, $endpoint, array $data = null) + protected function sendRequest($method, $endpoint, ?array $data = null) { $headers = [ 'Authorization' => 'Bearer ' . $this->getSecretKey(), diff --git a/src/Message/CompletePurchaseRequest.php b/src/Message/CompletePurchaseRequest.php index 538acd9..fdc761a 100644 --- a/src/Message/CompletePurchaseRequest.php +++ b/src/Message/CompletePurchaseRequest.php @@ -11,7 +11,7 @@ class CompletePurchaseRequest extends AbstractRequest */ public function getApiEndpoint() { - return '/transaction/verify/' . rawurlencode($this->getTransactionReference()); + return '/transaction/verify/' . rawurlencode((string) $this->getTransactionReference()); } /** @@ -35,4 +35,4 @@ public function sendData($data) return $this->response = new CompletePurchaseResponse($this, $response); } -} \ No newline at end of file +} diff --git a/src/Message/CompletePurchaseResponse.php b/src/Message/CompletePurchaseResponse.php index a846bdd..337b8cf 100644 --- a/src/Message/CompletePurchaseResponse.php +++ b/src/Message/CompletePurchaseResponse.php @@ -15,7 +15,7 @@ public function isSuccessful() public function getTransactionId() { if (isset($this->data['data']) && $data = $this->data['data']) { - if ($data['reference']) { + if (!empty($data['reference'])) { return $data['reference']; } } @@ -24,13 +24,13 @@ public function getTransactionId() public function getMessage() { if (isset($this->data['data']) && $data = $this->data['data']) { - if ($data['message']) { + if (!empty($data['message'])) { return $data['message']; } } if (isset($this->data['data']) && $data = $this->data['data']) { - if ($data['gateway_response']) { + if (!empty($data['gateway_response'])) { return $data['gateway_response']; } } @@ -45,7 +45,7 @@ public function getMessage() public function getTransactionReference() { if (isset($this->data['data']) && $data = $this->data['data']) { - return $data['reference']; + return $data['reference'] ?? null; } return ''; @@ -55,9 +55,9 @@ public function getTransactionReference() public function getCode() { if (isset($this->data['data']) && $data = $this->data['data']) { - return $data['status']; + return $data['status'] ?? null; } return ''; } -} \ No newline at end of file +} diff --git a/src/Message/PurchaseResponse.php b/src/Message/PurchaseResponse.php index 6ff11b3..f5dd0c3 100644 --- a/src/Message/PurchaseResponse.php +++ b/src/Message/PurchaseResponse.php @@ -24,7 +24,7 @@ public function isRedirect() public function getRedirectUrl() { if (isset($this->data['data']) && $data = $this->data['data']) { - return $data['authorization_url']; + return $data['authorization_url'] ?? null; } return ''; @@ -53,7 +53,7 @@ public function getRedirectMethod() public function getCode() { if (isset($this->data['data']) && $data = $this->data['data']) { - return $data['access_code']; + return $data['access_code'] ?? null; } return ''; @@ -62,7 +62,7 @@ public function getCode() public function getTransactionReference() { if (isset($this->data['data']) && $data = $this->data['data']) { - return $data['reference']; + return $data['reference'] ?? null; } return ''; diff --git a/src/Message/RefundResponse.php b/src/Message/RefundResponse.php index 7c95480..10ba49e 100644 --- a/src/Message/RefundResponse.php +++ b/src/Message/RefundResponse.php @@ -10,7 +10,7 @@ class RefundResponse extends AbstractResponse public function isSuccessful() { - if (isset($this->data['data']) && $transaction = $this->data['data']['transaction']) { + if (isset($this->data['data']['transaction']) && $transaction = $this->data['data']['transaction']) { if (isset($transaction['status']) && $transaction['status'] == 'reversed') { return true; } @@ -36,7 +36,7 @@ public function getMessage() public function getCode() { if (isset($this->data['data']) && $data = $this->data['data']) { - return $data['access_code']; + return $data['access_code'] ?? null; } return ''; @@ -46,7 +46,7 @@ public function getTransactionReference() { if (isset($this->data['data']) && $data = $this->data['data']) { if (isset($data['transaction']) && $transaction = $data['transaction']) { - return $transaction['reference']; + return $transaction['reference'] ?? null; } } diff --git a/tests/GatewayTest.php b/tests/GatewayTest.php index 4ed4dc4..4bccb65 100644 --- a/tests/GatewayTest.php +++ b/tests/GatewayTest.php @@ -3,6 +3,9 @@ namespace Omnipay\PayStack\Tests; use Omnipay\Paystack\Gateway; +use Omnipay\Paystack\Message\CompletePurchaseResponse; +use Omnipay\Paystack\Message\PurchaseResponse; +use Omnipay\Paystack\Message\RefundResponse; use Omnipay\Tests\GatewayTestCase; class GatewayTest extends GatewayTestCase @@ -19,7 +22,7 @@ class GatewayTest extends GatewayTestCase /** * */ - public function setUp() + public function setUp(): void { parent::setUp(); @@ -69,4 +72,124 @@ public function testRefund() $this->assertTrue($response->isSuccessful(), 'Refund is successful status'); $this->assertEquals(static::PURCHASE_REFERENCE, $response->getTransactionReference(), 'Reference is as we gave it'); } + + /** + * + */ + public function testPurchaseFailure() + { + $this->setMockHttpResponse('PurchaseFailure.txt'); + + $response = $this->gateway->purchase(array_merge($this->options, [ + 'email' => 'email@email.com' + ]))->send(); + + $this->assertFalse($response->isSuccessful()); + $this->assertFalse($response->isRedirect()); + $this->assertSame('Invalid key', $response->getMessage()); + $this->assertSame('', $response->getRedirectUrl()); + $this->assertSame('', $response->getTransactionReference()); + } + + /** + * + */ + public function testRefundFailure() + { + $this->setMockHttpResponse('RefundFailure.txt'); + + $response = $this->gateway->refund($this->options)->send(); + + $this->assertFalse($response->isSuccessful()); + $this->assertSame('Transaction has been fully reversed', $response->getMessage()); + $this->assertSame('', $response->getTransactionReference()); + } + + /** + * + */ + public function testCompletePurchaseSuccess() + { + $this->setMockHttpResponse('CompletePurchaseSuccess.txt'); + + $response = $this->gateway->completePurchase(['transactionReference' => '0rujfjycxi'])->send(); + + $this->assertTrue($response->isSuccessful()); + $this->assertSame('success', $response->getCode()); + $this->assertSame('0rujfjycxi', $response->getTransactionReference()); + $this->assertSame('Successful', $response->getMessage()); + } + + /** + * + */ + public function testCompletePurchaseFailure() + { + $this->setMockHttpResponse('CompletePurchaseFailure.txt'); + + $response = $this->gateway->completePurchase(['transactionReference' => '0rujfjycxj'])->send(); + + $this->assertFalse($response->isSuccessful()); + $this->assertSame('failed', $response->getCode()); + $this->assertSame('Declined', $response->getMessage()); + } + + /** + * + */ + public function testCompletePurchaseReferenceNotFound() + { + $this->setMockHttpResponse('CompletePurchaseNotFound.txt'); + + $response = $this->gateway->completePurchase(['transactionReference' => 'unknown'])->send(); + + $this->assertFalse($response->isSuccessful()); + $this->assertSame('Transaction reference not found', $response->getMessage()); + $this->assertSame('', $response->getTransactionReference()); + } + + /** + * + */ + public function testCompletePurchaseWithoutReference() + { + $this->setMockHttpResponse('CompletePurchaseNotFound.txt'); + + $response = $this->gateway->completePurchase()->send(); + + $this->assertFalse($response->isSuccessful()); + } + + /** + * A payload missing optional keys must not raise warnings on PHP 8. + */ + public function testCompletePurchasePartialPayload() + { + $request = $this->gateway->completePurchase(['transactionReference' => 'abc']); + $response = new CompletePurchaseResponse($request, ['status' => true, 'data' => ['status' => 'abandoned']]); + + $this->assertFalse($response->isSuccessful()); + $this->assertNull($response->getTransactionId()); + $this->assertNull($response->getTransactionReference()); + $this->assertSame('', $response->getMessage()); + } + + /** + * A payload missing optional keys must not raise warnings on PHP 8. + */ + public function testPurchaseAndRefundPartialPayload() + { + $purchase = new PurchaseResponse($this->gateway->purchase(), ['status' => true, 'data' => ['id' => 1]]); + + $this->assertFalse($purchase->isRedirect()); + $this->assertNull($purchase->getRedirectUrl()); + $this->assertNull($purchase->getCode()); + $this->assertNull($purchase->getTransactionReference()); + + $refund = new RefundResponse($this->gateway->refund(), ['status' => true, 'data' => ['id' => 1]]); + + $this->assertFalse($refund->isSuccessful()); + $this->assertNull($refund->getCode()); + $this->assertSame('', $refund->getTransactionReference()); + } } diff --git a/tests/Message/AuthorizeRequestTest.php b/tests/Message/AuthorizeRequestTest.php index f76880a..e49bd2d 100644 --- a/tests/Message/AuthorizeRequestTest.php +++ b/tests/Message/AuthorizeRequestTest.php @@ -11,7 +11,7 @@ class AuthorizeRequestTest extends TestCase */ private $request; - public function setUp() + public function setUp(): void { $this->markTestSkipped('Must be revisited.'); diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 5b1c0d8..8929eec 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -6,7 +6,7 @@ class ResponseTest extends TestCase { - public function setUp() + public function setUp(): void { $this->markTestSkipped('Must be revisited.'); diff --git a/tests/Mock/CompletePurchaseFailure.txt b/tests/Mock/CompletePurchaseFailure.txt index e69de29..8494f25 100644 --- a/tests/Mock/CompletePurchaseFailure.txt +++ b/tests/Mock/CompletePurchaseFailure.txt @@ -0,0 +1,21 @@ +HTTP/1.1 200 OK +Server: Apache +Date: Sun, 11 May 2014 21:17:31 GMT +Content-Type: application/json; charset=utf-8 +Status: 200 OK + +{ + "status": true, + "message": "Verification successful", + "data": { + "id": 549256455, + "domain": "test", + "status": "failed", + "reference": "0rujfjycxj", + "amount": 250000, + "message": null, + "gateway_response": "Declined", + "channel": "card", + "currency": "NGN" + } +} diff --git a/tests/Mock/CompletePurchaseNotFound.txt b/tests/Mock/CompletePurchaseNotFound.txt new file mode 100644 index 0000000..b6f8ed9 --- /dev/null +++ b/tests/Mock/CompletePurchaseNotFound.txt @@ -0,0 +1,10 @@ +HTTP/1.1 400 Bad Request +Server: Apache +Date: Sun, 11 May 2014 21:17:31 GMT +Content-Type: application/json; charset=utf-8 +Status: 400 Bad Request + +{ + "status": false, + "message": "Transaction reference not found" +} diff --git a/tests/Mock/PurchaseFailure.txt b/tests/Mock/PurchaseFailure.txt index e69de29..7a3a1d9 100644 --- a/tests/Mock/PurchaseFailure.txt +++ b/tests/Mock/PurchaseFailure.txt @@ -0,0 +1,10 @@ +HTTP/1.1 401 Unauthorized +Server: Apache +Date: Sun, 11 May 2014 21:17:31 GMT +Content-Type: application/json; charset=utf-8 +Status: 401 Unauthorized + +{ + "status": false, + "message": "Invalid key" +} diff --git a/tests/Mock/RefundFailure.txt b/tests/Mock/RefundFailure.txt new file mode 100644 index 0000000..ef4a40d --- /dev/null +++ b/tests/Mock/RefundFailure.txt @@ -0,0 +1,10 @@ +HTTP/1.1 400 Bad Request +Server: Apache +Date: Sun, 11 May 2014 21:17:31 GMT +Content-Type: application/json; charset=utf-8 +Status: 400 Bad Request + +{ + "status": false, + "message": "Transaction has been fully reversed" +} From 2a09120314d52a6deb211de488fbe5d2dd360d7c Mon Sep 17 00:00:00 2001 From: jules Date: Mon, 26 Oct 2026 09:54:27 +0200 Subject: [PATCH 2/3] Replace Travis CI with GitHub Actions Travis CI no longer runs for this repo. The new workflow tests PHP 7.2-8.5 with highest dependencies, plus lowest dependencies on 7.2 and 8.5. On PHPUnit 10+ it fails on PHP deprecations, warnings and notices, which PHPUnit otherwise only reports. Actions are pinned to commit SHAs, the checkout doesn't persist credentials, and the job token is read-only. Scrutinizer's external_code_coverage is removed: coverage was uploaded from Travis, so inspections would wait for it and time out. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 2 +- .github/workflows/tests.yml | 48 +++++++++++++++++++++++++++++++++++++ .scrutinizer.yml | 5 ---- .travis.yml | 27 --------------------- 4 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.gitattributes b/.gitattributes index b263871..aaf7d3d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,7 @@ # Ignore all test and documentation with "export-ignore". /.gitattributes export-ignore /.gitignore export-ignore -/.travis.yml export-ignore +/.github export-ignore /phpunit.xml.dist export-ignore /.scrutinizer.yml export-ignore /tests export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4d52b5f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,48 @@ +name: Tests + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + dependencies: ['highest'] + include: + - php: '7.2' + dependencies: 'lowest' + - php: '8.5' + dependencies: 'lowest' + name: PHP ${{ matrix.php }} (${{ matrix.dependencies }}) + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + with: + php-version: ${{ matrix.php }} + coverage: none + - name: Install dependencies + run: | + if [ "${{ matrix.dependencies }}" = "lowest" ]; then + composer update --prefer-lowest --prefer-stable --no-interaction --no-progress + else + composer update --no-interaction --no-progress + fi + - name: Run tests + # PHPUnit 10+ only reports PHP deprecations, warnings and notices; make them fail the build. + run: | + if vendor/bin/phpunit --version | grep -qE 'PHPUnit (1[0-9])\.'; then + vendor/bin/phpunit --fail-on-deprecation --fail-on-warning --fail-on-notice + else + vendor/bin/phpunit + fi + - name: Check code style + run: vendor/bin/phpcs --standard=PSR2 src/ diff --git a/.scrutinizer.yml b/.scrutinizer.yml index aea6609..1165e72 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -16,8 +16,3 @@ checks: fix_line_ending: true fix_identation_4spaces: true fix_doc_comments: true - -tools: - external_code_coverage: - timeout: 600 - runs: 3 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 36001fc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: php - -php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - -matrix: - include: - - php: 5.6 - env: setup=lowest - -before_script: - - travis_retry composer self-update - - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source - -script: - - vendor/bin/phpcs --standard=PSR2 src/ - - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover - -after_script: - - | - if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' ]]; then - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover - fi From f21d3c932b03fd062d07bd25d2b761b3f4c4158e Mon Sep 17 00:00:00 2001 From: jules Date: Mon, 26 Oct 2026 09:54:27 +0200 Subject: [PATCH 3/3] Docs: PHP requirement and changelog for PHP 8 support Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 ++++++++++--- README.md | 9 ++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe83ff..fedf3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ All Notable changes to `omnipay-paystack` will be documented in this file ### Added -- Nothing +- PHP 8 support (`"php": "^7.2 || ^8.0"`), so the package installs on the PHP releases from the 21 Oct 2026 security update (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp): 8.2.35, 8.3.36, 8.4.27, 8.5.12 +- GitHub Actions test matrix for PHP 7.2–8.5, failing on PHP deprecations, warnings and notices on PHPUnit 10+ +- Tests for `completePurchase()` (success, declined, unknown reference, partial payload) and for failed purchase and refund responses ### Deprecated @@ -14,11 +16,16 @@ All Notable changes to `omnipay-paystack` will be documented in this file ### Fixed -- Nothing +- Implicitly nullable `$data` parameter in `AbstractRequest::sendRequest()`, which is deprecated in PHP 8.4 +- "Undefined array key" warnings on PHP 8 when the Paystack API returns an error or partial payload. Return values are unchanged +- Passing `null` to `rawurlencode()` when `completePurchase()` is sent without a transaction reference (deprecated in PHP 8.1) ### Removed -- Nothing +- Travis CI config (`.travis.yml`), replaced by GitHub Actions +- `phpro/grumphp` dev dependency and `grumphp.yml`: version 0.14 only installs with Composer 1, and current versions require PHP 8.2+ +- PHP 7.0 and 7.1 support: the nullable type in `sendRequest()` and the test tools need PHP 7.2+. Composer keeps those projects on the previous release +- Scrutinizer `external_code_coverage`: coverage was uploaded from Travis CI, which is removed ### Security diff --git a/README.md b/README.md index 423bb8a..370160d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ # Paystack support for Omnipay [Omnipay](https://github.com/thephpleague/omnipay) is a framework agnostic, multi-gateway payment -processing library for PHP 5.3+. This package implements Paystack support for Omnipay. +processing library for PHP. This package implements Paystack support for Omnipay. + +Requires PHP 7.2 or later. The test suite runs on PHP 7.2–8.5. + +In production, use a supported PHP release that includes the 21 Oct 2026 security fixes for critical +remote code execution flaws (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp): **8.2.35, 8.3.36, 8.4.27, +8.5.12 or later** ([php.net advisory](https://www.php.net/releases/8_2_35.php)). PHP 8.1 and older +no longer receive security fixes. ## Install