Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ build
vendor
phpcs.xml
.phpunit.result.cache
*/.DS_Store
*/.DS_Store
src/assets/regions.json
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changelog
## [1.2.1](https://github.com/contentstack/contentstack-utils-php/tree/v1.2.1) (2024-03-02)
- Support for the fragment tag in nested list
## [1.3.0](https://github.com/contentstack/contentstack-utils-php/tree/v1.3.0) (2026-06-03)
- Added `Endpoint::getContentstackEndpoint()` for dynamic region-aware URL resolution
- Added `Utils::getContentstackEndpoint()` proxy for backward-compatible access
- Bundled `regions.json` is now downloaded at `composer install` / `composer update` via `post-install-cmd`; the file is not committed to the repository
- Added runtime fallback in `Endpoint::loadRegions()` — downloads `regions.json` on first use when the file is absent (e.g. when the package is installed as a dependency)
- Added `composer refresh-regions` script to manually pull the latest regions from Contentstack
- Supports 7 regions (AWS NA/EU/AU, Azure NA/EU, GCP NA/EU) and 18 service endpoint keys

## [1.2.0](https://github.com/contentstack/contentstack-utils-php/tree/v1.2.0) (2023-06-27)
- Support for the br tag and support for nested assets in the the image
## [1.1.0](https://github.com/contentstack/contentstack-utils-php/tree/v1.1.0) (2021-07-16)
Expand Down
239 changes: 239 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,243 @@ use Contentstack\Utils\Model\Option;
...
$render_html_text = GQL::jsonToHtml($entry->rich_text_content,, new Option());
...
```

---

## Endpoint Resolution

The SDK ships with a built-in endpoint resolver that returns the correct Contentstack API URL for any region and any service — no hardcoded URLs needed.

### How `regions.json` is provisioned

`regions.json` is **not committed** to your project. It is downloaded automatically:

| When | How |
|---|---|
| `composer install` or `composer update` | `post-install-cmd` runs `scripts/download-regions.php` |
| First call to `getContentstackEndpoint()` when file is missing | Runtime fallback downloads and caches the file |
| Manual refresh | `composer refresh-regions` |

```sh
# Refresh when Contentstack adds new regions or services
composer refresh-regions
```

---

### `getContentstackEndpoint()`

Available on both `Endpoint` and `Utils` (identical behaviour):

```php
use Contentstack\Utils\Endpoint;
use Contentstack\Utils\Utils;

Endpoint::getContentstackEndpoint(string $region, string $service, bool $omitHttps): string|array
Utils::getContentstackEndpoint(string $region, string $service, bool $omitHttps): string|array
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `$region` | `string` | `'us'` | Region ID or any accepted alias (see table below) |
| `$service` | `string` | `''` | Service key. When empty, all endpoints for the region are returned as an array |
| `$omitHttps` | `bool` | `false` | When `true`, strips `https://` from the returned URL(s) |

---

### Supported Regions

| Region | Canonical ID | Accepted Aliases |
|---|---|---|
| AWS North America | `na` | `us`, `aws-na`, `aws_na`, `NA`, `US`, `AWS-NA`, `AWS_NA` |
| AWS Europe | `eu` | `aws-eu`, `aws_eu`, `EU`, `AWS-EU`, `AWS_EU` |
| AWS Australia | `au` | `aws-au`, `aws_au`, `AU`, `AWS-AU`, `AWS_AU` |
| Azure North America | `azure-na` | `azure_na`, `AZURE-NA`, `AZURE_NA` |
| Azure Europe | `azure-eu` | `azure_eu`, `AZURE-EU`, `AZURE_EU` |
| GCP North America | `gcp-na` | `gcp_na`, `GCP-NA`, `GCP_NA` |
| GCP Europe | `gcp-eu` | `gcp_eu`, `GCP-EU`, `GCP_EU` |

Alias matching is **case-insensitive** and accepts both `-` and `_` separators.

---

### Available Service Keys

| Key | Description |
|---|---|
| `contentDelivery` | Content Delivery API (CDN) — for fetching published entries and assets |
| `contentManagement` | Content Management API — for creating and updating content |
| `graphqlDelivery` | GraphQL Delivery API |
| `graphqlPreview` | GraphQL Live Preview |
| `preview` | REST Live Preview |
| `auth` | Authentication API |
| `application` | Contentstack web application URL |
| `images` | Image Delivery |
| `assets` | Asset Delivery |
| `automate` | Workflow Automation API |
| `launch` | Contentstack Launch API |
| `developerHub` | Developer Hub API |
| `brandKit` | Brand Kit API |
| `genAI` | Generative AI / Knowledge Vault |
| `personalizeManagement` | Personalization Management API |
| `personalizeEdge` | Personalization Edge API |
| `composableStudio` | Composable Studio API |
| `assetManagement` | Asset Management API |

---

### Examples

#### Get a single service URL

```php
use Contentstack\Utils\Endpoint;

// AWS North America — Content Delivery
$url = Endpoint::getContentstackEndpoint('na', 'contentDelivery');
// → "https://cdn.contentstack.io"

// AWS Europe — Content Management
$url = Endpoint::getContentstackEndpoint('eu', 'contentManagement');
// → "https://eu-api.contentstack.com"

// Azure North America — GraphQL Delivery
$url = Endpoint::getContentstackEndpoint('azure-na', 'graphqlDelivery');
// → "https://azure-na-graphql.contentstack.com"

// GCP Europe — Auth
$url = Endpoint::getContentstackEndpoint('gcp-eu', 'auth');
// → "https://gcp-eu-auth-api.contentstack.com"
```

#### Use an alias instead of the canonical ID

```php
// All of these return the same NA content delivery URL
Endpoint::getContentstackEndpoint('us', 'contentDelivery'); // → https://cdn.contentstack.io
Endpoint::getContentstackEndpoint('na', 'contentDelivery'); // → https://cdn.contentstack.io
Endpoint::getContentstackEndpoint('aws-na', 'contentDelivery'); // → https://cdn.contentstack.io
Endpoint::getContentstackEndpoint('AWS_NA', 'contentDelivery'); // → https://cdn.contentstack.io
```

#### Get a URL without the `https://` scheme

Pass `true` as the third argument when you need just the hostname (e.g. for `Stack::setHost()`):

```php
$host = Endpoint::getContentstackEndpoint('eu', 'contentDelivery', true);
// → "eu-cdn.contentstack.com"
```

#### Get all endpoints for a region

Omit the `$service` argument to receive the full associative array:

```php
$endpoints = Endpoint::getContentstackEndpoint('au');
// → [
// 'contentDelivery' => 'https://au-cdn.contentstack.com',
// 'contentManagement' => 'https://au-api.contentstack.com',
// 'graphqlDelivery' => 'https://au-graphql.contentstack.com',
// 'auth' => 'https://au-auth-api.contentstack.com',
// ...17 more keys
// ]

// With omitHttps
$hosts = Endpoint::getContentstackEndpoint('au', '', true);
// → [
// 'contentDelivery' => 'au-cdn.contentstack.com',
// 'contentManagement' => 'au-api.contentstack.com',
// ...
// ]
```

#### Via `Utils` (same result, no import change needed)

```php
use Contentstack\Utils\Utils;

$url = Utils::getContentstackEndpoint('gcp-na', 'contentDelivery');
// → "https://gcp-na-cdn.contentstack.com"
```

---

### Integration with the PHP Delivery SDK

Use `getContentstackEndpoint()` to resolve the host dynamically, then pass it to `Stack::setHost()`:

```php
use Contentstack\Contentstack;
use Contentstack\Utils\Endpoint;

$region = 'eu'; // change this one value to switch regions

// Resolve the content delivery host for the chosen region
$host = Endpoint::getContentstackEndpoint($region, 'contentDelivery', true);
// → "eu-cdn.contentstack.com"

// Initialise the delivery SDK
$stack = Contentstack::Stack('<API_KEY>', '<DELIVERY_TOKEN>', '<ENVIRONMENT>');
$stack->setHost($host);

// Fetch entries — all requests now go to the EU CDN
$result = $stack->ContentType('<CONTENT_TYPE_UID>')->Query()->toJSON()->find();
```

#### Switching regions without changing any other code

```php
$regions = ['na', 'eu', 'au', 'azure-na', 'azure-eu', 'gcp-na', 'gcp-eu'];

foreach ($regions as $region) {
$host = Endpoint::getContentstackEndpoint($region, 'contentDelivery', true);
$stack = Contentstack::Stack('<API_KEY>', '<DELIVERY_TOKEN>', '<ENVIRONMENT>');
$stack->setHost($host);

$result = $stack->ContentType('<CONTENT_TYPE_UID>')->Query()->toJSON()->find();
echo "{$region}: " . count($result[0]) . " entries\n";
}
```

---

### Error Handling

```php
use Contentstack\Utils\Endpoint;

// Empty region
try {
Endpoint::getContentstackEndpoint('');
} catch (\InvalidArgumentException $e) {
echo $e->getMessage();
// → "Empty region provided. Please put valid region."
}

// Unknown region
try {
Endpoint::getContentstackEndpoint('unknown-region', 'contentDelivery');
} catch (\InvalidArgumentException $e) {
echo $e->getMessage();
// → "Invalid region: unknown-region"
}

// Unknown service
try {
Endpoint::getContentstackEndpoint('na', 'unknownService');
} catch (\InvalidArgumentException $e) {
echo $e->getMessage();
// → "Service "unknownService" not found for region "na""
}

// regions.json missing and no network access
try {
Endpoint::getContentstackEndpoint('na', 'contentDelivery');
} catch (\RuntimeException $e) {
echo $e->getMessage();
// → "contentstack/utils: regions.json not found and could not be downloaded.
// Run "composer install" or "composer refresh-regions" and ensure network access."
}
```
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@
}
},
"scripts": {
"post-install-cmd": ["@php scripts/download-regions.php"],
"post-update-cmd": ["@php scripts/download-regions.php"],
"test": "phpunit",
"check-style": "phpcs src tests",
"fix-style": "phpcbf src tests"
"fix-style": "phpcbf src tests",
"refresh-regions": "@php scripts/download-regions.php"
},
"extra": {
"branch-alias": {
Expand Down
79 changes: 79 additions & 0 deletions scripts/download-regions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/**
* Downloads the Contentstack regions registry from the official source and
* saves it to src/assets/regions.json.
*
* Invoked automatically by Composer on post-install-cmd and post-update-cmd,
* and manually via: composer refresh-regions
*
* Uses the PHP curl extension when available, falls back to file_get_contents.
*/

$url = 'https://artifacts.contentstack.com/regions.json';
$dest = dirname(__DIR__) . '/src/assets/regions.json';
$dir = dirname($dest);

if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
fwrite(STDERR, "contentstack/utils: Failed to create directory {$dir}\n");
exit(1);
}

$data = null;

// --- Attempt 1: PHP curl extension (preferred, respects SSL certs) ----------
if (extension_loaded('curl')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

if ($response !== false && $httpCode === 200) {
$data = $response;
} elseif ($curlError) {
fwrite(STDERR, "contentstack/utils: curl error: {$curlError}\n");
}
}

// --- Attempt 2: file_get_contents fallback ----------------------------------
if ($data === null) {
$ctx = stream_context_create([
'http' => [
'timeout' => 30,
'ignore_errors' => false,
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$data = @file_get_contents($url, false, $ctx);
}

// --- Validate and write ------------------------------------------------------
if ($data === false || $data === null) {
fwrite(STDERR, "contentstack/utils: Warning — could not download regions.json. " .
"The SDK will attempt to download it at runtime on first use.\n");
exit(0); // non-fatal: runtime fallback in Endpoint::loadRegions() handles it
}

$decoded = json_decode($data, true);
if (!is_array($decoded) || !isset($decoded['regions']) || !is_array($decoded['regions'])) {
fwrite(STDERR, "contentstack/utils: Warning — downloaded data is not a valid regions.json.\n");
exit(0);
}

if (file_put_contents($dest, $data) === false) {
fwrite(STDERR, "contentstack/utils: Warning — could not write regions.json to {$dest}.\n");
exit(0);
}

$regionCount = count($decoded['regions']);
echo "contentstack/utils: regions.json downloaded ({$regionCount} regions).\n";
Loading
Loading