Skip to content
Merged
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
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ Note: sandbox keys have a `sbox_` or `test_` identifier, for Default and Previou

If you don't have your own API keys, you can sign up for a test account [here](https://www.checkout.com/get-test-account).

### Subdomain value

Requests must be made through your merchant-specific subdomain (MSSD): the first 8 characters of your client ID (excluding `cli_`). For example, if your client ID is `cli_vkuhvk4vjn2edkps7dfsq6emqm`, your subdomain is `vkuhvk4v`. When `environmentSubdomain` is set the SDK sends requests to `https://vkuhvk4v.api.checkout.com`. See [Base URLs](https://api-reference.checkout.com/#section/Base-URLs) and [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) for further details, and for where to find your unique client ID.

Private Link merchants use their `pl-` prefixed subdomain (for example `pl-vkuhvk4v`), which the SDK also accepts.

### Default

Expand All @@ -77,7 +82,7 @@ $checkoutApi = CheckoutSdk::builder()->staticKeys()
->publicKey("public_key") // optional, only required for operations related with tokens
->secretKey("secret_key")
->environment(Environment::sandbox()) // or production()
->environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
->environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
->logger($logger) //optional, for a custom Logger
->httpClientBuilder($client) // optional, for a custom HTTP client
->build();
Expand All @@ -95,7 +100,7 @@ $checkoutApi = CheckoutSdk::builder()->oAuth()
->clientCredentials("client_id", "client_secret")
->scopes([OAuthScope::$Gateway, OAuthScope::$Vault]) // array of scopes
->environment(Environment::sandbox()) // or production()
->environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
->environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
->logger($logger) //optional, for a custom Logger
->httpClientBuilder($client) // optional, for a custom HTTP client
->build();
Expand Down Expand Up @@ -149,6 +154,22 @@ The execution of integration tests require the following environment variables s
* For default account systems (OAuth): `CHECKOUT_DEFAULT_OAUTH_CLIENT_ID` & `CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET`
* For Previous account systems (ABC): `CHECKOUT_PREVIOUS_PUBLIC_KEY` & `CHECKOUT_PREVIOUS_SECRET_KEY`

## Legacy domain (emergency use only)

> :warning: **Only use if merchant specific sub domains are causing issues.** Connecting through your merchant-specific subdomain (see [Subdomain value](#subdomain-value)) is the supported way of using the Checkout.com API, and non-subdomain usage will be deprecated.

If, in exceptional circumstances, you cannot use your merchant-specific subdomain, you can explicitly opt out by calling `useLegacyDomain()` instead of `environmentSubdomain(...)`:

```php
$checkoutApi = CheckoutSdk::builder()->staticKeys()
->secretKey("secret_key")
->environment(Environment::sandbox())
->useLegacyDomain() // deprecated, emergency fallback only
->build();
```

This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method is marked `@deprecated`. Exactly one of `environmentSubdomain(...)` or `useLegacyDomain()` must be set: the SDK throws a `CheckoutArgumentException` if both, or neither, are set. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement.

## Code of Conduct

Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)
Expand Down
70 changes: 68 additions & 2 deletions lib/Checkout/AbstractCheckoutSdkBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ abstract class AbstractCheckoutSdkBuilder
{

protected $environment;
protected $environmentSubdomain = null;
protected $subdomain = null;
private $environmentSubdomain = null;
protected $useLegacyDomain = false;
protected $httpClientBuilder;
protected $logger;

Expand All @@ -28,6 +30,7 @@ public function __construct()
public function environment(Environment $environment)
{
$this->environment = $environment;
$this->environmentSubdomain = null;
return $this;
}

Expand All @@ -37,10 +40,73 @@ public function environment(Environment $environment)
*/
public function environmentSubdomain($subdomain)
{
$this->environmentSubdomain = new EnvironmentSubdomain($this->environment, $subdomain);
$this->subdomain = $subdomain;
$this->environmentSubdomain = null;
return $this;
}

/**
* Opts out of the merchant-specific subdomain, sending every request to the shared
* hosts instead (api.checkout.com and access.checkout.com, or their sandbox equivalents).
*
* @deprecated this is an emergency fallback for the rare case where the merchant-specific
* subdomain cannot be used, and will be removed in a future release. Call
* environmentSubdomain() instead. See https://api-reference.checkout.com/#section/Base-URLs
* @return $this
*/
public function useLegacyDomain()
{
$this->useLegacyDomain = true;
return $this;
}

/**
* @return EnvironmentSubdomain|null
* @throws CheckoutArgumentException
*/
protected function getEnvironmentSubdomain()
{
if ($this->subdomain === null) {
return null;
}
if ($this->environmentSubdomain === null) {
$this->environmentSubdomain = new EnvironmentSubdomain($this->environment, $this->subdomain);
}
return $this->environmentSubdomain;
}

/**
* Whether this builder requires the merchant-specific subdomain to be configured. The
* Previous (ABC) platform predates merchant-specific subdomains, so it overrides this
* to false.
*
* @return bool
*/
protected function requiresEnvironmentSubdomain()
{
return true;
}

/**
* @throws CheckoutArgumentException
*/
protected function validateEnvironmentSettings()
{
if ($this->subdomain !== null && $this->useLegacyDomain) {
throw new CheckoutArgumentException(
"environmentSubdomain and useLegacyDomain cannot both be set - provide only your " .
"merchant-specific subdomain"
);
}
if ($this->subdomain === null && !$this->useLegacyDomain && $this->requiresEnvironmentSubdomain()) {
throw new CheckoutArgumentException(
"environmentSubdomain is required - provide your merchant-specific subdomain (typically your " .
"client ID excluding the cli_ prefix, see https://api-reference.checkout.com/#section/Base-URLs), " .
"or call useLegacyDomain() to opt out only if merchant specific sub domains are causing issues"
);
}
}

/**
* @param HttpClientBuilderInterface $httpClientBuilder
* @return $this
Expand Down
18 changes: 14 additions & 4 deletions lib/Checkout/CheckoutOAuthSdkBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ protected function getSdkCredentials()
"Invalid configuration. Please specify an Environment or a specific OAuth authorization URI."
);
}
if ($this->environmentSubdomain !== null) {
$this->authorizationUri = $this->environmentSubdomain->getAuthorizationUri();
$environmentSubdomain = $this->getEnvironmentSubdomain();
if ($environmentSubdomain !== null) {
$this->authorizationUri = $environmentSubdomain->getAuthorizationUri();
} else {
$this->authorizationUri = $this->environment->getAuthorizationUri();
}
Expand All @@ -84,14 +85,23 @@ protected function getSdkCredentials()
*/
public function build()
{
if ($this->authorizationUri !== null && $this->subdomain !== null) {
throw new CheckoutArgumentException(
"authorizationUri and environmentSubdomain cannot both be set - the token endpoint is derived " .
"from your subdomain; combine authorizationUri with useLegacyDomain() if you need a custom " .
"token host"
);
}
$this->validateEnvironmentSettings();
$configuration = new CheckoutConfiguration(
$this->getSdkCredentials(),
$this->environment,
$this->httpClientBuilder,
$this->logger
);
if ($this->environmentSubdomain !== null) {
$configuration->setEnvironmentSubdomain($this->environmentSubdomain);
$environmentSubdomain = $this->getEnvironmentSubdomain();
if ($environmentSubdomain !== null) {
$configuration->setEnvironmentSubdomain($environmentSubdomain);
}
return new CheckoutApi($configuration);
}
Expand Down
6 changes: 4 additions & 2 deletions lib/Checkout/CheckoutStaticKeysSdkBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ protected function getSdkCredentials()
*/
public function build()
{
$this->validateEnvironmentSettings();
$this->validatePublicKey($this->publicKey, self::PUBLIC_KEY_PATTERN);
$this->validateSecretKey($this->secretKey, self::SECRET_KEY_PATTERN);
$configuration = new CheckoutConfiguration(
Expand All @@ -50,8 +51,9 @@ public function build()
$this->httpClientBuilder,
$this->logger
);
if ($this->environmentSubdomain !== null) {
$configuration->setEnvironmentSubdomain($this->environmentSubdomain);
$environmentSubdomain = $this->getEnvironmentSubdomain();
if ($environmentSubdomain !== null) {
$configuration->setEnvironmentSubdomain($environmentSubdomain);
}
return new CheckoutApi($configuration);
}
Expand Down
45 changes: 26 additions & 19 deletions lib/Checkout/EnvironmentSubdomain.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@

final class EnvironmentSubdomain
{
/**
* The D modifier anchors $ to the very end of the subject, so a value with a trailing
* newline (for example one read from a file) is rejected instead of producing a
* malformed URL downstream.
*/
const SUBDOMAIN_PATTERN = '/^(?:pl-)?[a-z0-9]+$/D';

private $baseUri;
private $authorizationUri;

/**
* @param Environment $environment
* @param $subdomain
* @throws CheckoutArgumentException
*/
public function __construct(Environment $environment, $subdomain)
{
Expand All @@ -18,35 +26,34 @@ public function __construct(Environment $environment, $subdomain)
}

/**
* Applies subdomain transformation to any given URL.
* If the subdomain is valid (alphanumeric pattern), prepends it to the host.
* Otherwise, returns the original URL unchanged.
* Applies subdomain transformation to any given URL, prepending the subdomain to the host.
*
* @param string $originalUrl the original URL to transform
* @param string $subdomain the subdomain to prepend
* @return string the transformed URL with subdomain, or original URL if subdomain is invalid
* @return string the transformed URL with subdomain
* @throws CheckoutArgumentException if the subdomain is not a valid merchant-specific subdomain
*/
private function createUrlWithSubdomain($originalUrl, $subdomain)
{
$newEnvironment = $originalUrl;

$regex = '/^(?:pl-)?[a-z0-9]+$/';
if (preg_match($regex, $subdomain)) {
$urlParts = parse_url($originalUrl);
$newHost = $subdomain . '.' . $urlParts['host'];
if ($subdomain === null || !preg_match(self::SUBDOMAIN_PATTERN, $subdomain)) {
throw new CheckoutArgumentException(
"invalid environment subdomain - provide your merchant-specific subdomain, typically your " .
"client ID excluding the cli_ prefix (see https://api-reference.checkout.com/#section/Base-URLs)"
);
}

$newUrl = $urlParts['scheme'] . '://' . $newHost;
if (isset($urlParts['port'])) {
$newUrl .= ':' . $urlParts['port'];
}
if (isset($urlParts['path'])) {
$newUrl .= $urlParts['path'];
}
$urlParts = parse_url($originalUrl);
$newHost = $subdomain . '.' . $urlParts['host'];

$newEnvironment = $newUrl;
$newUrl = $urlParts['scheme'] . '://' . $newHost;
if (isset($urlParts['port'])) {
$newUrl .= ':' . $urlParts['port'];
}
if (isset($urlParts['path'])) {
$newUrl .= $urlParts['path'];
}

return $newEnvironment;
return $newUrl;
}

/**
Expand Down
17 changes: 15 additions & 2 deletions lib/Checkout/Previous/CheckoutStaticKeysPreviousSdkBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ class CheckoutStaticKeysPreviousSdkBuilder extends AbstractStaticKeysCheckoutSdk
const PUBLIC_KEY_PATTERN = "/^pk_(test_)?(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$/";
const SECRET_KEY_PATTERN = "/^sk_(test_)?(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$/";

/**
* The Previous (ABC) platform predates merchant-specific subdomains, so it is exempt from
* the mandatory environmentSubdomain/useLegacyDomain configuration.
*
* @return bool
*/
protected function requiresEnvironmentSubdomain()
{
return false;
}

/**
* @param string $publicKey
* @return $this
Expand Down Expand Up @@ -48,6 +59,7 @@ protected function getSdkCredentials()
*/
public function build()
{
$this->validateEnvironmentSettings();
$this->validatePublicKey($this->publicKey, self::PUBLIC_KEY_PATTERN);
$this->validateSecretKey($this->secretKey, self::SECRET_KEY_PATTERN);
$configuration = new CheckoutConfiguration(
Expand All @@ -56,8 +68,9 @@ public function build()
$this->httpClientBuilder,
$this->logger
);
if ($this->environmentSubdomain !== null) {
$configuration->setEnvironmentSubdomain($this->environmentSubdomain);
$environmentSubdomain = $this->getEnvironmentSubdomain();
if ($environmentSubdomain !== null) {
$configuration->setEnvironmentSubdomain($environmentSubdomain);
}
return new CheckoutApi($configuration);
}
Expand Down
3 changes: 3 additions & 0 deletions test/Checkout/Tests/Accounts/AccountsIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,9 @@ private function getAccountsCheckoutApi()
getenv("CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET")
)
->scopes([OAuthScope::$Accounts, OAuthScope::$Files])
// The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so
// the token request would come back invalid_client. Opting out explicitly until they are.
->useLegacyDomain()
->build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ private static function getPayoutSchedulesCheckoutApi()
getenv("CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET")
)
->scopes([OAuthScope::$Accounts])
// The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so
// the token request would come back invalid_client. Opting out explicitly until they are.
->useLegacyDomain()
->build();
}
}
Loading
Loading