|
| 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