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
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.8
- psr/log: ^3.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.8
README
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 unusableALERT: Action must be taken immediatelyCRITICAL: Critical conditionsERROR: Runtime errorsWARNING: Warning conditionsNOTICE: Normal but significant conditionINFO: 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 transactionhandle-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
- Verify vendor name: Ensure you're passing your exact Opayo vendor name to
NotificationHandlerconstructor - Check security key: Use the exact security key returned during transaction registration
- 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
- Verify IP whitelisting: Ensure requests are coming from Opayo's servers
- 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:
- All tests pass:
composer test - Code meets PSR-12:
composer phpcs - PHPStan level 8 passes:
composer phpstan - 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.