Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
os: >-
['ubuntu-latest', 'windows-latest']
php: >-
['8.1', '8.2', '8.3']
['8.2', '8.3', '8.4']
2 changes: 1 addition & 1 deletion .github/workflows/composer-require-checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.1', '8.2', '8.3']
['8.2', '8.3', '8.4']
2 changes: 1 addition & 1 deletion .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.1', '8.2', '8.3']
['8.2', '8.3', '8.4']
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A modern PHP 8.4+ library providing a unified interface for multiple payment gat

## Requirements

- PHP 8.1 or higher.
- PHP 8.2 or higher.

## Installation

Expand Down Expand Up @@ -155,7 +155,7 @@ $intent = new PaymentIntent(
id: 'pi_123', // null for new intents
amount: 1000, // $10.00
currency: 'usd',
status: 'requires_payment_method',
status: PaymentIntentStatus::RequiresPaymentMethod,
customerId: 'cus_123',
paymentMethodId: 'pm_123',
metadata: ['order_id' => 'abc123']
Expand Down Expand Up @@ -201,7 +201,7 @@ const { paymentMethod, error } = await stripe.createPaymentMethod({
```php
$paymentMethod = $gateway->createPaymentMethod(new PaymentMethod(
id: $_POST['payment_method_id'],
type: 'card',
type: PaymentMethodType::Card,
customerId: $customer->id
));
```
Expand Down Expand Up @@ -318,7 +318,7 @@ $stripe = new StripeGateway(
#### PayPal

```php
use PaymentGateway\Gateways\PayPalGateway;
use \Yiisoft\Payments\Gateways\PayPalGateway;

$paypal = new PayPalGateway(
'your-paypal-client-id',
Expand All @@ -334,7 +334,7 @@ $paypal = new PayPalGateway(

```php
// Create a customer
$customer = new \PaymentGateway\Models\Customer(
$customer = new \Yiisoft\Payments\Models\Customer(
null, // id will be generated by the gateway
'customer@example.com',
'John Doe'
Expand All @@ -357,9 +357,9 @@ $gateway->deleteCustomer('cus_123');

```php
// Create a payment method (e.g., card)
$paymentMethod = new \PaymentGateway\Models\PaymentMethod(
$paymentMethod = new \Yiisoft\Payments\Models\PaymentMethod(
null, // id will be generated by the gateway
'card',
\Yiisoft\Payments\Models\PaymentMethodType::CARD,
[
'number' => '4242424242424242',
'exp_month' => '12',
Expand Down Expand Up @@ -451,7 +451,7 @@ use Yiisoft\PaymentGateway\Gateways\AbstractGateway;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerInterface;use Yiisoft\Payments\Enums\PaymentIntentStatus;

final class AcmePayGateway extends AbstractGateway
{
Expand Down Expand Up @@ -503,7 +503,7 @@ final class AcmePayGateway extends AbstractGateway

return new PaymentIntent(
id: $response['id'],
status: $response['status'],
status: PaymentIntentStatus::tryFrom($response['status']),
amount: $intent->amount,
currency: $intent->currency,
customerId: $intent->customerId,
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
}
],
"require": {
"php": "^8.1",
"php": "^8.2",
"ext-json": "*",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
Expand Down
16 changes: 16 additions & 0 deletions src/Enums/PaymentIntentStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Payments\Enums;

enum PaymentIntentStatus: string
{
case RequiresPaymentMethod = 'requires_payment_method';
case RequiresConfirmation = 'requires_confirmation';
case RequiresAction = 'requires_action';
case Processing = 'processing';
case RequiresCapture = 'requires_capture';
case Canceled = 'canceled';
case Succeeded = 'succeeded';
}
58 changes: 31 additions & 27 deletions src/Gateways/PayPalGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,37 @@

namespace Yiisoft\Payments\Gateways;

use Yiisoft\Payments\Enums\PaymentIntentStatus;
use Yiisoft\Payments\Models\Customer;
use Yiisoft\Payments\Models\PaymentIntent;
use Yiisoft\Payments\Models\PaymentMethod;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
use Yiisoft\Payments\Models\PaymentMethodType;

class PayPalGateway extends AbstractGateway

Check failure on line 17 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.4-ubuntu-latest

ClassMustBeFinal

src/Gateways/PayPalGateway.php:17:7: ClassMustBeFinal: Class Yiisoft\Payments\Gateways\PayPalGateway is never extended and is not part of the public API, and thus must be made final. (see https://psalm.dev/361)

Check failure on line 17 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

ClassMustBeFinal

src/Gateways/PayPalGateway.php:17:7: ClassMustBeFinal: Class Yiisoft\Payments\Gateways\PayPalGateway is never extended and is not part of the public API, and thus must be made final. (see https://psalm.dev/361)

Check failure on line 17 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

ClassMustBeFinal

src/Gateways/PayPalGateway.php:17:7: ClassMustBeFinal: Class Yiisoft\Payments\Gateways\PayPalGateway is never extended and is not part of the public API, and thus must be made final. (see https://psalm.dev/361)
{
private ?string $accessToken = null;
private ?int $tokenExpires = null;
private const TOKEN_EXPIRY_BUFFER = 300; // 5 minutes in seconds
private string $clientId;
private string $clientSecret;
private bool $sandbox;
private const TOKEN_EXPIRY_BUFFER = 300;

Check failure on line 21 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.4-ubuntu-latest

MissingClassConstType

src/Gateways/PayPalGateway.php:21:19: MissingClassConstType: Class constant "Yiisoft\Payments\Gateways\PayPalGateway::TOKEN_EXPIRY_BUFFER" should have a declared type. (see https://psalm.dev/359)

Check failure on line 21 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

MissingClassConstType

src/Gateways/PayPalGateway.php:21:19: MissingClassConstType: Class constant "Yiisoft\Payments\Gateways\PayPalGateway::TOKEN_EXPIRY_BUFFER" should have a declared type. (see https://psalm.dev/359)

public function __construct(
string $clientId,
string $clientSecret,
bool $sandbox,
private string $clientId,
private string $clientSecret,
private bool $sandbox,
ClientInterface $httpClient,
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
?LoggerInterface $logger = null
) {
parent::__construct($httpClient, $requestFactory, $streamFactory, $logger);
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->sandbox = $sandbox;
}

protected function getBaseUri(): string
{
return $this->sandbox
return $this->sandbox
? 'https://api-m.sandbox.paypal.com/v1'
: 'https://api-m.paypal.com/v1';
}
Expand Down Expand Up @@ -68,11 +64,11 @@
);

$data = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);

if (!isset($data['access_token'])) {
throw new \RuntimeException('Failed to get access token: ' . ($data['error'] ?? 'Unknown error'));
}

$this->accessToken = $data['access_token'];
$this->tokenExpires = time() + ($data['expires_in'] ?? 3600);

Expand All @@ -82,12 +78,12 @@
protected function createRequest(string $method, string $endpoint, array $data = []): \Psr\Http\Message\RequestInterface
{
$request = parent::createRequest($method, $endpoint, $data);

// Skip adding auth header for token requests
if (str_contains($endpoint, '/oauth2/token')) {
return $request;
}

return $request
->withHeader('Authorization', 'Bearer ' . $this->getAccessToken())
->withHeader('PayPal-Request-Id', uniqid('', true));
Expand All @@ -100,7 +96,7 @@
'name' => [
'given_name' => $customer->name ?? 'Customer',
// PayPal requires a last name, so we'll use a placeholder if not provided
'surname' => $customer->name ? ' ' : 'Customer',

Check failure on line 99 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.4-ubuntu-latest

RiskyTruthyFalsyComparison

src/Gateways/PayPalGateway.php:99:30: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 99 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

RiskyTruthyFalsyComparison

src/Gateways/PayPalGateway.php:99:30: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 99 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

RiskyTruthyFalsyComparison

src/Gateways/PayPalGateway.php:99:30: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
],
'email' => $customer->email,
'phone' => $customer->phone,
Expand All @@ -109,7 +105,7 @@
];

$response = $this->sendRequest($this->createRequest('POST', '/customer/partner-referrals', $data));

// PayPal doesn't have a direct customer creation endpoint in the same way as Stripe
// This is a simplified implementation
return new Customer(
Expand Down Expand Up @@ -152,7 +148,7 @@
'email_address' => $customer->email,
'name' => [
'given_name' => $customer->name,
'surname' => $customer->name ? ' ' : 'Customer',

Check failure on line 151 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.4-ubuntu-latest

RiskyTruthyFalsyComparison

src/Gateways/PayPalGateway.php:151:34: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 151 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

RiskyTruthyFalsyComparison

src/Gateways/PayPalGateway.php:151:34: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 151 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

RiskyTruthyFalsyComparison

src/Gateways/PayPalGateway.php:151:34: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
],
'phone' => $customer->phone,
'metadata' => $customer->metadata,
Expand All @@ -161,7 +157,7 @@
];

$this->sendRequest($this->createRequest('PATCH', "/customer/partner-referrals/{$customer->id}", [$data]));

return $customer;
}

Expand All @@ -183,14 +179,14 @@

// In PayPal, payment methods are typically associated with orders, not directly with customers
// This is a simplified implementation
return new PaymentMethod($paymentMethod->id, 'paypal', [], $paymentMethod->customerId);
return new PaymentMethod($paymentMethod->id, PaymentMethodType::PAYPAL, [], $paymentMethod->customerId);
}

public function attachPaymentMethod(string $paymentMethodId, string $customerId): PaymentMethod
{
// In PayPal, payment methods are typically associated with orders, not directly with customers
// This is a simplified implementation
return new PaymentMethod($paymentMethodId, 'paypal', [], $customerId);
return new PaymentMethod($paymentMethodId, PaymentMethodType::PAYPAL, [], $customerId);
}

public function createPaymentIntent(PaymentIntent $intent): PaymentIntent
Expand Down Expand Up @@ -230,10 +226,18 @@
break;
}
}


// Map PayPal state to PaymentIntentStatus
$status = match (strtolower($response['state'] ?? '')) {
'created' => PaymentIntentStatus::RequiresPaymentMethod,
'approved' => PaymentIntentStatus::Succeeded,
'failed' => PaymentIntentStatus::Canceled,
default => PaymentIntentStatus::RequiresPaymentMethod,
};

return new PaymentIntent(
id: $response['id'],
status: strtoupper($response['state'] ?? 'created'),
status: $status,
amount: $intent->amount,
currency: strtolower($response['transactions'][0]['amount']['currency'] ?? 'usd'),
customerId: $intent->customerId,
Expand All @@ -242,7 +246,7 @@
metadata: $intent->metadata,
receiptEmail: $intent->receiptEmail,
statementDescriptor: $intent->statementDescriptor,
createdAt: strtotime($response['create_time'] ?? 'now')

Check failure on line 249 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.4-ubuntu-latest

PossiblyFalseArgument

src/Gateways/PayPalGateway.php:249:24: PossiblyFalseArgument: Argument 11 of Yiisoft\Payments\Models\PaymentIntent::__construct cannot be false, possibly int|null value expected (see https://psalm.dev/104)

Check failure on line 249 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

PossiblyFalseArgument

src/Gateways/PayPalGateway.php:249:24: PossiblyFalseArgument: Argument 11 of Yiisoft\Payments\Models\PaymentIntent::__construct cannot be false, possibly int|null value expected (see https://psalm.dev/104)

Check failure on line 249 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

PossiblyFalseArgument

src/Gateways/PayPalGateway.php:249:24: PossiblyFalseArgument: Argument 11 of Yiisoft\Payments\Models\PaymentIntent::__construct cannot be false, possibly int|null value expected (see https://psalm.dev/104)
);
}

Expand All @@ -255,11 +259,11 @@
public function capturePaymentIntent(string $intentId, array $params = []): PaymentIntent
{
$response = $this->createRequest('POST', "/checkout/orders/{$intentId}/capture", $params);

return new PaymentIntent(
$response['id'],

Check failure on line 264 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.4-ubuntu-latest

UndefinedInterfaceMethod

src/Gateways/PayPalGateway.php:264:13: UndefinedInterfaceMethod: Method Psr\Http\Message\RequestInterface::offsetGet does not exist (see https://psalm.dev/181)

Check failure on line 264 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

UndefinedInterfaceMethod

src/Gateways/PayPalGateway.php:264:13: UndefinedInterfaceMethod: Method Psr\Http\Message\RequestInterface::offsetGet does not exist (see https://psalm.dev/181)

Check failure on line 264 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

UndefinedInterfaceMethod

src/Gateways/PayPalGateway.php:264:13: UndefinedInterfaceMethod: Method Psr\Http\Message\RequestInterface::offsetGet does not exist (see https://psalm.dev/181)
strtolower($response['status']),
PaymentIntentStatus::tryFrom(strtolower($response['status'])),

Check failure on line 265 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

UndefinedInterfaceMethod

src/Gateways/PayPalGateway.php:265:53: UndefinedInterfaceMethod: Method Psr\Http\Message\RequestInterface::offsetGet does not exist (see https://psalm.dev/181)
(int) ((float) $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'] * 100),

Check failure on line 266 in src/Gateways/PayPalGateway.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

UndefinedInterfaceMethod

src/Gateways/PayPalGateway.php:266:28: UndefinedInterfaceMethod: Method Psr\Http\Message\RequestInterface::offsetGet does not exist (see https://psalm.dev/181)
strtolower($response['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']),
null,
null,
Expand All @@ -274,10 +278,10 @@
{
// In PayPal, we void the authorization
$response = $this->createRequest('POST', "/checkout/orders/{$intentId}/void-authorization", $params);

return new PaymentIntent(
$response['id'],
'canceled',
PaymentIntentStatus::Canceled,
null,
null,
null,
Expand All @@ -301,7 +305,7 @@
$response = $this->sendRequest(
$this->createRequest('POST', "/v1/payments/capture/{$captureId}/refund", $data)
);

return [
'id' => $response['id'],
'state' => $response['state'],
Expand All @@ -326,7 +330,7 @@

return new PaymentIntent(
id: $data['id'],
status: strtoupper($data['state']),
status: PaymentIntentStatus::tryFrom(strtolower($data['state'])),
amount: (int)($amount['total'] * 100) ?? 0,
currency: $amount['currency'] ?? null,
customerId: $data['payer']['payer_info']['customer_id'] ?? null,
Expand Down
4 changes: 1 addition & 3 deletions src/Gateways/StripeGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,16 @@

class StripeGateway extends AbstractGateway
{
private string $apiKey;
private string $apiVersion = '2023-10-16';

public function __construct(
string $apiKey,
private string $apiKey,
ClientInterface $httpClient,
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
?LoggerInterface $logger = null
) {
parent::__construct($httpClient, $requestFactory, $streamFactory, $logger);
$this->apiKey = $apiKey;
}

protected function getBaseUri(): string
Expand Down
18 changes: 5 additions & 13 deletions src/Models/PaymentIntent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Yiisoft\Payments\Models;

use Yiisoft\Payments\Constants\PaymentIntentStatus;
use Yiisoft\Payments\Enums\PaymentIntentStatus;
use Yiisoft\Payments\Exceptions\InvalidArgumentException;

/**
Expand All @@ -25,17 +25,9 @@ private function validateCurrency(string $currency): string
return strtoupper($currency);
}

public const STATUS_REQUIRES_PAYMENT_METHOD = 'requires_payment_method';
public const STATUS_REQUIRES_CONFIRMATION = 'requires_confirmation';
public const STATUS_REQUIRES_ACTION = 'requires_action';
public const STATUS_PROCESSING = 'processing';
public const STATUS_REQUIRES_CAPTURE = 'requires_capture';
public const STATUS_CANCELED = 'canceled';
public const STATUS_SUCCEEDED = 'succeeded';

/**
* @param string|null $id The unique identifier for the payment intent.
* @param string|null $status The status of the payment intent.
* @param PaymentIntentStatus|null $status The status of the payment intent.
* @param int|null $amount The amount to be collected by this payment intent.
* @param string|null $currency Three-letter ISO currency code.
* @param string|null $customerId ID of the customer this payment intent is for.
Expand All @@ -56,7 +48,7 @@ private function validateCurrency(string $currency): string

public function __construct(
public ?string $id = null,
public ?string $status = null,
public ?PaymentIntentStatus $status = null,
public ?int $amount = null,
?string $currency = null,
public ?string $customerId = null,
Expand Down Expand Up @@ -84,7 +76,7 @@ public function toArray(): array
{
return [
'id' => $this->id,
'status' => $this->status,
'status' => $this->status->value,
'amount' => $this->amount,
'currency' => $this->currency,
'customer_id' => $this->customerId,
Expand Down Expand Up @@ -118,7 +110,7 @@ public static function fromArray(array $data): self

return new self(
id: $data['id'] ?? null,
status: $data['status'] ?? null,
status: PaymentIntentStatus::tryFrom($data['status']),
amount: $data['amount'] ?? null,
currency: $currency,
customerId: $data['customer_id'] ?? $data['customerId'] ?? null,
Expand Down
6 changes: 2 additions & 4 deletions src/PaymentGatewayInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@

/**
* Payment Gateway Interface
*
*
* This interface defines the standard contract that all payment gateway implementations must follow.
* It provides a consistent API for processing payments, managing customers, and handling payment methods
* across different payment service providers.
*
* Implementations of this interface should handle the communication with specific payment gateways
* (like Stripe, PayPal, etc.) while providing a unified interface to the application.
*
* @package Yiisoft\\Payments\\Core
*/
interface PaymentGatewayInterface
{
Expand Down Expand Up @@ -51,7 +49,7 @@ public function createPaymentMethod(PaymentMethod $paymentMethod): PaymentMethod
* Attaches a payment method to a customer
*/
public function attachPaymentMethod(
string $paymentMethodId,
string $paymentMethodId,
string $customerId
): PaymentMethod;

Expand Down
Loading
Loading