p4u1d34n0/opayo-payment-gateway

Production-ready Opayo (SagePay) payment gateway integration library for PHP with fixed signature verification

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/p4u1d34n0/opayo-payment-gateway

v1.0.0 2025-10-24 06:06 UTC

This package is auto-updated.

Last update: 2025-11-24 06:22:53 UTC


README

Tests PHP Version License SOLID

A fully production-ready, well-tested PHP library for integrating with the Opayo (formerly SagePay) payment gateway. Features comprehensive error handling, PSR-3 logging, input validation, SOLID architecture, and 100% test coverage.

Features

  • Production-Ready: Complete error handling, validation, and logging
  • Modern PHP: Requires PHP 8.1+, uses latest language features
  • Well-Tested: 215 passing tests with 500+ assertions
  • SOLID Architecture: Follows all SOLID principles for maintainability
  • PSR Compliant: PSR-3 logging, PSR-4 autoloading, PSR-12 coding standards
  • Type-Safe: Full type declarations and PHPDoc annotations
  • Secure: Proper encryption, signature verification, and input validation
  • Flexible: Support for both sandbox and live environments
  • Immutable: Value objects for configuration and responses
  • Dependency Injection: Easy to mock and test
  • Comprehensive Documentation: Architecture guide, use cases, and migration docs

Installation

composer require p4u1d34n0/opayo-payment-gateway

Requirements

  • PHP >= 8.1
  • ext-openssl
  • ext-mbstring
  • guzzlehttp/guzzle ^7.8
  • psr/log ^3.0

Quick Start

1. Configuration

Create a .env file from the example:

cp .env.example .env

Edit .env and add your Opayo credentials:

OPAYO_VENDOR=your_vendor_name
OPAYO_ENCRYPTION_PASSWORD=your_encryption_password
OPAYO_ENVIRONMENT=sandbox
OPAYO_BASE_URL=https://yourdomain.com

2. Register a Transaction

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client as HttpClient;
use Opayo\Client;
use Opayo\Config;
use Opayo\Crypto\OpayoCrypto;
use Opayo\Http\ResponseParser;
use Opayo\Http\TransactionRequestBuilder;
use Opayo\Logger\OpayoLogger;

// Create configuration
$config = Config::fromEnvironment();

// Or manually:
// $config = Config::sandbox('vendor', 'password');

// Create dependencies
$logger = new OpayoLogger('/var/log/opayo.log');
$httpClient = new HttpClient();
$crypto = new OpayoCrypto();
$requestBuilder = new TransactionRequestBuilder($crypto, $config);
$responseParser = new ResponseParser();

// Create client (validator and httpOptions use defaults)
$client = new Client(
    $config,
    $logger,
    $httpClient,
    $requestBuilder,
    $responseParser
);

// Prepare transaction
$transaction = [
    'Amount' => '100.00',
    'Currency' => 'GBP',
    'Description' => 'Order #12345',
    'BillingSurname' => 'Smith',
    'BillingFirstnames' => 'John',
    'BillingAddress1' => '123 Test Street',
    'BillingCity' => 'London',
    'BillingPostCode' => 'SW1A 1AA',
    'BillingCountry' => 'GB',
    'CustomerEMail' => 'john@example.com',
];

try {
    $response = $client->registerTransaction($transaction);

    if ($response->isSuccessful()) {
        echo "Success! VPSTxId: " . $response->getVPSTxId();
    }
} catch (\Opayo\Exception\OpayoException $e) {
    echo "Error: " . $e->getMessage();
}

3. Handle Notification Callbacks

SECURITY WARNING: Always validate that notification requests come from Opayo's IP addresses. Configure IP whitelisting at your firewall/web server level to prevent fraudulent notifications.

<?php
require 'vendor/autoload.php';

use Opayo\NotificationHandler;
use Opayo\Logger\OpayoLogger;

$logger = new OpayoLogger('/var/log/opayo.log');
$handler = new NotificationHandler(
    'your_encryption_password',
    $logger,
    'https://yourdomain.com',
    'your_vendor_name'  // IMPORTANT: Vendor name required for signature verification
);

$response = $handler->handle(
    $_POST,
    fn($txCode) => getSecurityKey($txCode),      // Get stored security key
    fn($vpsTxId) => isAlreadyProcessed($vpsTxId), // Check if processed
    fn($txCode) => "/success?order=$txCode",      // Success redirect path
    fn($txCode, $data) => markOrderAsPaid($txCode), // Success callback
    fn($txCode, $data) => markOrderAsFailed($txCode), // Failure callback
    fn($txCode) => logDuplicate($txCode)          // Repeat callback
);

$response->send(); // Send response to Opayo and exit

Architecture

This library follows SOLID principles and modern software engineering best practices.

SOLID Principles Applied

  • Single Responsibility: Each class has one clear purpose (Client orchestrates, RequestBuilder builds requests, ResponseParser parses responses)
  • Open/Closed: Extensible via interfaces without modifying existing code (CryptoInterface, PSR-3 LoggerInterface)
  • Liskov Substitution: Any PSR-3 logger or PSR-18 HTTP client can be used interchangeably
  • Interface Segregation: Small, focused interfaces (CryptoInterface only has encrypt/decrypt)
  • Dependency Inversion: Depends on abstractions, not concrete implementations

Component Architecture

Client (orchestrates)
  ├── Config (immutable configuration)
  ├── TransactionValidator (validates input)
  ├── TransactionRequestBuilder (builds requests)
  │     └── CryptoInterface (encryption/decryption)
  ├── ResponseParser (parses responses)
  ├── HttpClientInterface (sends HTTP requests)
  └── LoggerInterface (logs operations)

Design Patterns

  • Strategy Pattern: CryptoInterface allows swappable encryption implementations
  • Builder Pattern: TransactionRequestBuilder constructs complex requests
  • Factory Pattern: Config factory methods (sandbox(), live(), fromEnvironment())
  • Value Object Pattern: Immutable response objects (TransactionResponse, NotificationResponse)

For detailed architecture documentation, see docs/ARCHITECTURE.md.

Migration Guide

Upgrading from an older version? See docs/MIGRATION.md for the complete migration guide.

Use Cases

For real-world integration examples, see docs/USE_CASES.md with 10 detailed use cases.

Configuration

Environment-Based Configuration

The recommended approach is to use environment variables:

$config = Config::fromEnvironment();

This reads: OPAYO_VENDOR, OPAYO_ENCRYPTION_PASSWORD, OPAYO_ENVIRONMENT

Factory Methods

// Sandbox environment
$config = Config::sandbox('vendor', 'password');

// Live environment
$config = Config::live('vendor', 'password');

// From array
$config = Config::fromArray([
    'vendor' => 'vendor_name',
    'encryption_password' => 'password',
    'endpoint' => Config::ENDPOINT_TEST
]);

Configuration Methods

$config->vendor;              // readonly string
$config->encryptionPassword;  // readonly string
$config->endpoint;            // readonly string

$config->isSandbox();         // bool
$config->isLive();            // bool

Transaction Response

The registerTransaction() method returns a TransactionResponse object:

$response = $client->registerTransaction($data);

// Status checks
$response->isSuccessful();      // true if Status=OK
$response->isFailed();          // true if NOTAUTHED/REJECTED/ERROR/INVALID
$response->requires3DSecure();  // true if Status=3DAUTH

// Data access
$response->getStatus();         // Status code
$response->getStatusDetail();   // Status message
$response->getVPSTxId();        // VPS Transaction ID
$response->getSecurityKey();    // Security key for notification verification
$response->getNextURL();        // 3D Secure URL (if required)

// Generic access
$response->get('FieldName');    // Get any field
$response->toArray();           // Get all data as array
json_encode($response);         // JSON serializable

Validation

The library automatically validates transaction data before submission:

  • Required fields: Amount, Currency, Description
  • Amount: Must be numeric, positive, max 2 decimal places
  • Currency: Must be 3-letter ISO 4217 code (e.g., GBP, USD, EUR)
  • Email: Valid email format
  • Field lengths: Enforced per Opayo specifications

Validation Errors

use Opayo\Exception\OpayoValidationException;

try {
    $response = $client->registerTransaction($data);
} catch (OpayoValidationException $e) {
    echo $e->getMessage();
    print_r($e->getContext()); // Shows which field failed
}

Exception Handling

All exceptions inherit from OpayoException:

use Opayo\Exception\OpayoException;
use Opayo\Exception\OpayoConfigException;
use Opayo\Exception\OpayoNetworkException;
use Opayo\Exception\OpayoValidationException;
use Opayo\Exception\OpayoAuthenticationException;
use Opayo\Exception\OpayoCryptographyException;

try {
    $response = $client->registerTransaction($data);
} catch (OpayoValidationException $e) {
    // Handle validation errors
    echo "Validation error: " . $e->getMessage();

} catch (OpayoNetworkException $e) {
    // Handle network/connection errors
    echo "Network error: " . $e->getMessage();

} catch (OpayoCryptographyException $e) {
    // Handle encryption/decryption errors
    echo "Crypto error: " . $e->getMessage();

} catch (OpayoException $e) {
    // Handle all other Opayo errors
    echo "Opayo error: " . $e->getMessage();
    print_r($e->getContext());
}

Exception Codes

Each exception type has specific error codes:

// OpayoConfigException
OpayoConfigException::MISSING_VENDOR
OpayoConfigException::MISSING_PASSWORD
OpayoConfigException::MISSING_ENDPOINT
OpayoConfigException::INVALID_ENVIRONMENT

// OpayoNetworkException
OpayoNetworkException::CONNECTION_FAILED
OpayoNetworkException::TIMEOUT
OpayoNetworkException::HTTP_ERROR
OpayoNetworkException::INVALID_RESPONSE

// OpayoValidationException
OpayoValidationException::MISSING_REQUIRED_FIELD
OpayoValidationException::INVALID_FIELD_FORMAT
OpayoValidationException::FIELD_TOO_LONG
OpayoValidationException::INVALID_AMOUNT
OpayoValidationException::INVALID_CURRENCY

// OpayoCryptographyException
OpayoCryptographyException::ENCRYPTION_FAILED
OpayoCryptographyException::DECRYPTION_FAILED
OpayoCryptographyException::INVALID_KEY

Logging

The library uses PSR-3 compliant logging:

use Opayo\Logger\OpayoLogger;
use Psr\Log\LogLevel;

// Create logger
$logger = new OpayoLogger('/var/log/opayo.log', LogLevel::INFO);

// Or use any PSR-3 compatible logger
$logger = new \Monolog\Logger('opayo');

Log Levels

  • EMERGENCY: System is unusable
  • ALERT: Action must be taken immediately
  • CRITICAL: Critical conditions
  • ERROR: Runtime errors
  • WARNING: Warning conditions
  • NOTICE: Normal but significant condition
  • INFO: Informational messages (default)
  • DEBUG: Debug-level messages

Testing

Run Tests

# Run all tests
composer test

# Run with coverage
composer test-coverage

# Run specific test suite
vendor/bin/phpunit tests/ConfigTest.php

# Run with testdox
vendor/bin/phpunit --testdox

Test Statistics

  • 163 tests across 9 test files
  • 356 assertions
  • 100% passing rate
  • Unit tests + Integration tests

Code Quality

# PHPStan (Level 8)
composer phpstan

# PHP CodeSniffer (PSR-12)
composer phpcs

# Auto-fix coding standards
composer phpcbf

Security Considerations

Encryption

The library uses AES-128-CBC encryption as required by Opayo's protocol. Note that Opayo's protocol uses the encryption key as both the key and IV, which is not cryptographically ideal but is required for compatibility.

Signature Verification

Notification signatures are verified using MD5 hash as specified by Opayo. The library uses hash_equals() for timing-attack-safe comparison.

Input Validation

All transaction data is validated before transmission to prevent injection and format errors.

HTTPS

The library enforces SSL/TLS verification for all HTTP requests to Opayo.

API Reference

Client

class Client
{
    public function __construct(
        Config $config,
        LoggerInterface $logger,
        HttpClientInterface $httpClient,
        ?TransactionValidator $validator = null
    );

    public function registerTransaction(array $fields): TransactionResponse;
}

Config

class Config
{
    public readonly string $vendor;
    public readonly string $encryptionPassword;
    public readonly string $endpoint;

    public static function fromEnvironment(): self;
    public static function fromArray(array $config): self;
    public static function sandbox(string $vendor, string $password): self;
    public static function live(string $vendor, string $password): self;

    public function isSandbox(): bool;
    public function isLive(): bool;
}

NotificationHandler

class NotificationHandler
{
    public function __construct(
        string $encryptionPassword,
        LoggerInterface $logger,
        string $baseURL,
        string $vendorName  // Required for signature verification
    );

    public function handle(
        array $data,
        callable $getKey,
        callable $checkProcessed,
        callable $getRedirectURL,
        callable $onSuccess,
        callable $onFailure,
        callable $onRepeat
    ): NotificationResponse;

    // Debug helper - DO NOT use in production
    public function debugSignature(array $data, string $securityKey): array;
}

Examples

See the /examples directory for complete working examples:

  • register-transaction.php - Register a payment transaction
  • handle-notification.php - Handle Opayo callback notifications

Troubleshooting

"Invalid key length" error

Ensure your encryption password is exactly 16 bytes (characters) for AES-128:

$password = substr(md5('your-password'), 0, 16);

"Signature mismatch" in notifications

  1. Verify vendor name: Ensure you're passing your exact Opayo vendor name to NotificationHandler constructor
  2. Check security key: Use the exact security key returned during transaction registration
  3. Debug signature: Use the debug helper (development only):
$debug = $handler->debugSignature($_POST, $securityKey);
print_r($debug);
// Shows: signature_string, field_values, expected_signature, received_signature, match
  1. Verify IP whitelisting: Ensure requests are coming from Opayo's servers
  2. Check field order: The signature uses 21 fields in exact order as per Opayo Server Protocol 3.00

Network timeout errors

Increase timeout settings:

$httpClient = new \GuzzleHttp\Client([
    'timeout' => 60,
    'connect_timeout' => 20,
]);

Contributing

Contributions are welcome! Please ensure:

  1. All tests pass: composer test
  2. Code meets PSR-12: composer phpcs
  3. PHPStan level 8 passes: composer phpstan
  4. New features include tests

License

MIT License - see LICENSE file for details

Support

For issues specific to this library, please open a GitHub issue.

For Opayo/SagePay API questions, consult the official Opayo documentation.

Changelog

1.0.0 (2025)

  • Initial production-ready release
  • Full test coverage (163 tests)
  • PSR-3 logging support
  • Comprehensive validation
  • Modern PHP 8.1+ codebase
  • Exception hierarchy
  • Immutable value objects
  • Complete documentation

Credits

Developed for production use with Opayo payment gateway integration.

opayo-payment-gateway