Skip to content

Commit fb46255

Browse files
docs: add endpoint resolution documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e9169c4 commit fb46255

2 files changed

Lines changed: 1197 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
# Endpoint Feature — Integration Overview
2+
3+
## The Problem Being Solved
4+
5+
Before this feature, Contentstack hosts were either **hardcoded** in the delivery SDK (`cdn.contentstack.io`) or manually constructed with string concatenation (`$region.'-cdn.contentstack.com'`). There was no single authoritative source for all regions and all services. The endpoint feature solves this by providing one function to resolve any URL for any region/service.
6+
7+
---
8+
9+
## Part 1 — The Data Source (`regions.json`)
10+
11+
Contentstack maintains a live registry at:
12+
```
13+
https://artifacts.contentstack.com/regions.json
14+
```
15+
16+
Structure:
17+
```
18+
{
19+
"regions": [
20+
{
21+
"id": "na", ← canonical region ID
22+
"alias": ["us","aws-na","AWS-NA"...] ← all accepted aliases
23+
"isDefault": true,
24+
"endpoints": {
25+
"contentDelivery": "https://cdn.contentstack.io",
26+
"contentManagement": "https://api.contentstack.io",
27+
"graphqlDelivery": "https://graphql.contentstack.com",
28+
"auth": "https://auth-api.contentstack.com",
29+
"preview": "https://rest-preview.contentstack.com",
30+
... 18 services total
31+
}
32+
},
33+
{ "id": "eu", ... },
34+
{ "id": "au", ... },
35+
{ "id": "azure-na", ... },
36+
{ "id": "azure-eu", ... },
37+
{ "id": "gcp-na", ... },
38+
{ "id": "gcp-eu", ... } ← 7 regions total
39+
]
40+
}
41+
```
42+
43+
This file is **not committed** to the repository. It is downloaded automatically at install time and lives at `src/assets/regions.json`. No runtime HTTP calls in normal operation — works fully offline once downloaded.
44+
45+
---
46+
47+
## Part 2 — Keeping Regions Up To Date (`refresh-regions`)
48+
49+
Contentstack occasionally adds new regions or services. The workflow to update is:
50+
51+
```bash
52+
# Pull the latest registry from Contentstack and overwrite the local file
53+
composer refresh-regions
54+
55+
# What this runs internally:
56+
# php scripts/download-regions.php
57+
# → curl https://artifacts.contentstack.com/regions.json
58+
# → writes to src/assets/regions.json
59+
60+
# Since regions.json is in .gitignore, no commit is needed —
61+
# every developer and CI environment gets it fresh on composer install
62+
```
63+
64+
This mirrors exactly how the JS SDK handles it — except JS fetches at build/publish time via an npm `prebuild` script. PHP downloads it via a `post-install-cmd` composer hook, with a runtime fallback on the first API call.
65+
66+
---
67+
68+
## Part 3 — How `regions.json` Gets to Disk
69+
70+
Unlike the JS SDK (which bundles the file at publish time), the PHP SDK downloads it in three layers:
71+
72+
```
73+
Layer 1 — composer install / composer update (root package only)
74+
75+
└── post-install-cmd fires
76+
→ @php scripts/download-regions.php
77+
→ curl https://artifacts.contentstack.com/regions.json
78+
→ writes src/assets/regions.json
79+
→ "contentstack/utils: regions.json downloaded (7 regions)."
80+
81+
Layer 2 — Runtime fallback (when package is used as a dependency)
82+
83+
└── First call to Endpoint::getContentstackEndpoint()
84+
→ file_exists('src/assets/regions.json') === false
85+
→ Endpoint::downloadAndSave() runs silently
86+
→ writes src/assets/regions.json
87+
→ continues normally
88+
89+
Layer 3 — Static cache (fastest, zero I/O after first call)
90+
91+
└── $regionsData already set in memory
92+
→ return cached data immediately
93+
→ no disk reads, no network calls
94+
```
95+
96+
`src/assets/regions.json` is listed in `.gitignore` — it is never committed. Every environment provisions it independently.
97+
98+
---
99+
100+
## Part 4 — `Endpoint::getContentstackEndpoint()` — How Resolution Works
101+
102+
```
103+
Endpoint::getContentstackEndpoint('na', 'contentDelivery', false)
104+
│ │ │
105+
│ │ └── omitHttps: keep https://
106+
│ └── service: which URL to return
107+
└── region: ID or any alias
108+
```
109+
110+
Step-by-step inside [src/Endpoint.php](../src/Endpoint.php):
111+
112+
```
113+
Call arrives → getContentstackEndpoint('na', 'contentDelivery', false)
114+
115+
1. Guard check
116+
region === '' → throw InvalidArgumentException immediately
117+
118+
2. loadRegions() [runs only once per PHP process]
119+
First call: file_get_contents('src/assets/regions.json')
120+
json_decode() → store in static $regionsData
121+
Subsequent: return cached $regionsData ← no disk reads
122+
123+
3. Normalize input
124+
strtolower(trim('na')) → 'na'
125+
126+
4. findRegionByIdOrAlias()
127+
Pass 1 — match by id:
128+
regions[0]['id'] === 'na' ✓ → return this region row
129+
130+
Pass 2 — match by alias (only if Pass 1 fails):
131+
e.g. 'AWS-NA' → strtolower → 'aws-na'
132+
scan alias[] of each region until match found
133+
134+
No match → throw InvalidArgumentException('Invalid region: ...')
135+
136+
5. Service lookup
137+
service === 'contentDelivery'
138+
→ $regionRow['endpoints']['contentDelivery']
139+
→ "https://cdn.contentstack.io"
140+
Key missing → throw InvalidArgumentException('Service not found')
141+
142+
6. omitHttps check
143+
false → return "https://cdn.contentstack.io" ← full URL
144+
true → preg_replace('/^https?:\/\//', '') → "cdn.contentstack.io"
145+
146+
7. No service provided (service === '')
147+
→ return entire $regionRow['endpoints'] array
148+
→ with omitHttps: strip scheme from every value
149+
```
150+
151+
---
152+
153+
## Part 5 — `Utils::getContentstackEndpoint()` — Backward Compatibility
154+
155+
[src/Utils.php](../src/Utils.php) exposes the same function as a static proxy so existing code using `Utils::` doesn't need to change import paths:
156+
157+
```php
158+
// In Utils.php — just a thin pass-through, zero logic here
159+
public static function getContentstackEndpoint(
160+
string $region = 'us',
161+
string $service = '',
162+
bool $omitHttps = false
163+
) {
164+
return Endpoint::getContentstackEndpoint($region, $service, $omitHttps);
165+
}
166+
167+
// Both calls produce identical results:
168+
Endpoint::getContentstackEndpoint('na', 'contentDelivery');
169+
Utils::getContentstackEndpoint('na', 'contentDelivery');
170+
```
171+
172+
---
173+
174+
## Part 6 — Integration with the PHP Delivery SDK (what `test.php` does)
175+
176+
```
177+
test.php execution trace:
178+
─────────────────────────────────────────────────────────────────
179+
180+
1. Load autoloaders
181+
require vendor/autoload.php ← utils-php (has new Endpoint class)
182+
require contentstack-php/vendor/autoload.php ← delivery SDK
183+
(utils-php loads first → its Contentstack\Utils\* classes win)
184+
185+
2. Resolve endpoint
186+
Endpoint::getContentstackEndpoint('na', 'contentDelivery')
187+
→ "https://cdn.contentstack.io" [for display]
188+
189+
Endpoint::getContentstackEndpoint('na', 'contentDelivery', true)
190+
→ "cdn.contentstack.io" [host without scheme, for setHost()]
191+
192+
3. Create delivery SDK Stack
193+
Contentstack::Stack(API_KEY, DELIVERY_TOKEN, 'production')
194+
→ Stack object, host defaults to 'cdn.contentstack.io' (NA default)
195+
196+
4. Override host with endpoint-resolved value
197+
$stack->setHost('cdn.contentstack.io')
198+
→ host is now authoritative from regions.json, not hardcoded
199+
200+
5. Fetch entries
201+
$stack->ContentType('mega_menu')->Query()->toJSON()->find()
202+
→ HTTP GET https://cdn.contentstack.io/v3/content_types/mega_menu/entries
203+
?environment=production
204+
→ Returns 2 entries: "Region", "Topics Navigation"
205+
206+
Output:
207+
Total entries fetched: 2
208+
Entry #1 → bltc85890659eefc7c2 "Region"
209+
Entry #2 → blt3d9080b4eba8defa "Topics Navigation"
210+
```
211+
212+
The full `test.php` source is at [test.php](../test.php).
213+
214+
---
215+
216+
## Part 7 — Switching Regions in Practice
217+
218+
The key benefit: changing **one string** switches every URL automatically.
219+
220+
```php
221+
// NA (default)
222+
$host = Endpoint::getContentstackEndpoint('na', 'contentDelivery', true);
223+
// → cdn.contentstack.io
224+
225+
// EU
226+
$host = Endpoint::getContentstackEndpoint('eu', 'contentDelivery', true);
227+
// → eu-cdn.contentstack.com
228+
229+
// Azure EU
230+
$host = Endpoint::getContentstackEndpoint('azure-eu', 'contentDelivery', true);
231+
// → azure-eu-cdn.contentstack.com
232+
233+
// GCP NA
234+
$host = Endpoint::getContentstackEndpoint('gcp-na', 'contentDelivery', true);
235+
// → gcp-na-cdn.contentstack.com
236+
237+
// Then the same Stack setup works for any region:
238+
$stack = Contentstack::Stack($API_KEY, $DELIVERY_TOKEN, $ENV);
239+
$stack->setHost($host);
240+
```
241+
242+
Reading the region from an environment variable is the recommended pattern:
243+
244+
```php
245+
$region = getenv('CONTENTSTACK_REGION') ?: 'na';
246+
$host = Endpoint::getContentstackEndpoint($region, 'contentDelivery', true);
247+
248+
$stack = Contentstack::Stack(
249+
getenv('CONTENTSTACK_API_KEY'),
250+
getenv('CONTENTSTACK_DELIVERY_TOKEN'),
251+
getenv('CONTENTSTACK_ENVIRONMENT')
252+
);
253+
$stack->setHost($host);
254+
```
255+
256+
---
257+
258+
## Part 8 — Accepted Region Aliases
259+
260+
| You pass | Resolves to region |
261+
|---|---|
262+
| `na`, `us`, `aws-na`, `aws_na`, `NA`, `US`, `AWS-NA`, `AWS_NA` | `na` |
263+
| `eu`, `aws-eu`, `aws_eu`, `EU`, `AWS-EU`, `AWS_EU` | `eu` |
264+
| `au`, `aws-au`, `aws_au`, `AU`, `AWS-AU`, `AWS_AU` | `au` |
265+
| `azure-na`, `azure_na`, `AZURE-NA`, `AZURE_NA` | `azure-na` |
266+
| `azure-eu`, `azure_eu`, `AZURE-EU`, `AZURE_EU` | `azure-eu` |
267+
| `gcp-na`, `gcp_na`, `GCP-NA`, `GCP_NA` | `gcp-na` |
268+
| `gcp-eu`, `gcp_eu`, `GCP-EU`, `GCP_EU` | `gcp-eu` |
269+
270+
All matching is **case-insensitive** and accepts both `-` and `_` separators.
271+
272+
---
273+
274+
## Part 9 — Available Service Keys
275+
276+
| Service key | What it points to |
277+
|---|---|
278+
| `contentDelivery` | CDN for published content (used for entry/asset fetching) |
279+
| `contentManagement` | CMA for creating/updating content |
280+
| `graphqlDelivery` | GraphQL delivery API |
281+
| `graphqlPreview` | GraphQL live preview |
282+
| `preview` | REST live preview |
283+
| `auth` | Authentication API |
284+
| `application` | Web app URL |
285+
| `images` | Image delivery |
286+
| `assets` | Asset delivery |
287+
| `automate` | Workflow automation |
288+
| `launch` | Contentstack Launch |
289+
| `developerHub` | Developer Hub API |
290+
| `brandKit` | Brand Kit API |
291+
| `genAI` | Generative AI / Knowledge Vault |
292+
| `personalizeManagement` | Personalization management |
293+
| `personalizeEdge` | Personalization edge |
294+
| `composableStudio` | Composable Studio API |
295+
| `assetManagement` | Asset management API (NA only) |
296+
297+
---
298+
299+
## Part 10 — Error Handling
300+
301+
```php
302+
use Contentstack\Utils\Endpoint;
303+
304+
// Empty region string
305+
try {
306+
Endpoint::getContentstackEndpoint('');
307+
} catch (\InvalidArgumentException $e) {
308+
// "Empty region provided. Please put valid region."
309+
}
310+
311+
// Unknown region
312+
try {
313+
Endpoint::getContentstackEndpoint('asia-pacific', 'contentDelivery');
314+
} catch (\InvalidArgumentException $e) {
315+
// "Invalid region: asia-pacific"
316+
}
317+
318+
// Unknown service key
319+
try {
320+
Endpoint::getContentstackEndpoint('na', 'cms');
321+
} catch (\InvalidArgumentException $e) {
322+
// "Service "cms" not found for region "na""
323+
}
324+
325+
// regions.json missing and no network access
326+
try {
327+
Endpoint::getContentstackEndpoint('na', 'contentDelivery');
328+
} catch (\RuntimeException $e) {
329+
// "contentstack/utils: regions.json not found and could not be downloaded.
330+
// Run "composer install" or "composer refresh-regions" and ensure network access."
331+
}
332+
```
333+
334+
---
335+
336+
## Part 11 — Files Introduced by This Feature
337+
338+
```
339+
contentstack-utils-php/
340+
341+
├── src/
342+
│ ├── Endpoint.php ← core implementation (new)
343+
│ ├── Utils.php ← getContentstackEndpoint() proxy added
344+
│ └── assets/
345+
│ └── regions.json ← downloaded at install/runtime, NOT committed
346+
347+
├── scripts/
348+
│ └── download-regions.php ← called by composer hooks (new)
349+
350+
├── tests/
351+
│ └── EndpointTest.php ← 39 tests, 99 assertions (new)
352+
353+
├── docs/
354+
│ ├── endpoint-integration-overview.md ← this file (new)
355+
│ └── endpoint-resolution.md ← full API reference (new)
356+
357+
├── composer.json ← post-install-cmd, post-update-cmd, refresh-regions added
358+
└── .gitignore ← src/assets/regions.json added
359+
```

0 commit comments

Comments
 (0)