From 6c268c5de46ef4277336470474ca675dc9cb0378 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 8 Apr 2026 15:02:11 +0530 Subject: [PATCH 1/9] Add agent guide, skills overview, and development workflow instructions. --- .cursor/rules/README.md | 5 ++++ AGENTS.md | 48 ++++++++++++++++++++++++++++++ skills/README.md | 15 ++++++++++ skills/code-review/SKILL.md | 34 +++++++++++++++++++++ skills/contentstack-utils/SKILL.md | 42 ++++++++++++++++++++++++++ skills/dev-workflow/SKILL.md | 44 +++++++++++++++++++++++++++ skills/php-style/SKILL.md | 37 +++++++++++++++++++++++ skills/testing/SKILL.md | 40 +++++++++++++++++++++++++ 8 files changed, 265 insertions(+) create mode 100644 .cursor/rules/README.md create mode 100644 AGENTS.md create mode 100644 skills/README.md create mode 100644 skills/code-review/SKILL.md create mode 100644 skills/contentstack-utils/SKILL.md create mode 100644 skills/dev-workflow/SKILL.md create mode 100644 skills/php-style/SKILL.md create mode 100644 skills/testing/SKILL.md diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md new file mode 100644 index 0000000..b145041 --- /dev/null +++ b/.cursor/rules/README.md @@ -0,0 +1,5 @@ +# Cursor (optional) + +**Cursor** users: start at **[`AGENTS.md`](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**. + +This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d528bca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# Contentstack Utils PHP – Agent guide + +**Universal entry point** for contributors and AI agents. Detailed conventions live in **`skills/*/SKILL.md`**. + +## What this repo is + +| Field | Detail | +| --- | --- | +| **Name:** | [contentstack/utils](https://packagist.org/packages/contentstack/utils) · [GitHub](https://github.com/contentstack/contentstack-utils-php) | +| **Purpose:** | PHP library for rendering Contentstack Rich Text (RTE) content and GraphQL-shaped JSON to HTML, including embedded entries and custom render options. | +| **Out of scope (if any):** | Not an HTTP client or full Contentstack Delivery SDK; it focuses on parsing/rendering utilities used alongside other Contentstack PHP packages. | + +## Tech stack (at a glance) + +| Area | Details | +| --- | --- | +| Language | PHP `>=7.2` (Composer); CI runs on PHP 8.3. `declare(strict_types=1);` in source files. | +| Build | [Composer](https://getcomposer.org/) — `composer.json`, `composer.lock`. No compile step; autoload PSR-4 `Contentstack\Utils\` → `src/`. | +| Tests | PHPUnit 9.x via `composer test` / `phpunit`; suite in `tests/`, config `phpunit.xml`. | +| Lint / coverage | PHP_CodeSniffer (PSR-2 ruleset) — `composer check-style` / `composer fix-style`; config `phpcs.xml.dist`. Coverage/logging paths under `build/` when generated. | +| Other | Dev dependency `marc-mabe/php-enum` for enums. | + +## Commands (quick reference) + +| Command type | Command | +| --- | --- | +| Install deps | `composer install` | +| Test | `composer test` | +| Lint | `composer check-style` | +| Format (fix) | `composer fix-style` | + +**CI:** [`.github/workflows/ci.yml`](.github/workflows/ci.yml) — validates `composer.json` / lockfile, installs dependencies, runs `composer run-script test`. Other workflows: `sca-scan.yml`, `policy-scan.yml`, `issues-jira.yml`, `check-branch.yml`. + +## Where the documentation lives: skills + +| Skill | Path | What it covers | +| --- | --- | --- | +| Dev workflow | [`skills/dev-workflow/SKILL.md`](skills/dev-workflow/SKILL.md) | Branches, CI, Composer scripts, PR expectations | +| Contentstack Utils API | [`skills/contentstack-utils/SKILL.md`](skills/contentstack-utils/SKILL.md) | Public API, namespaces, extension points (`Option`, `RenderableInterface`) | +| PHP style & layout | [`skills/php-style/SKILL.md`](skills/php-style/SKILL.md) | PSR-2, PHPCS, file layout under `src/` | +| Testing | [`skills/testing/SKILL.md`](skills/testing/SKILL.md) | PHPUnit layout, mocks, coverage output | +| Code review | [`skills/code-review/SKILL.md`](skills/code-review/SKILL.md) | PR checklist aligned with this repo | + +An index with “when to use” hints is in [`skills/README.md`](skills/README.md). + +## Using Cursor (optional) + +If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—same docs as everyone else. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..f54ffac --- /dev/null +++ b/skills/README.md @@ -0,0 +1,15 @@ +# Skills – Contentstack Utils PHP + +Source of truth for detailed guidance. Read [`AGENTS.md`](../AGENTS.md) first, then open the skill that matches your task. + +## When to use which skill + +| Skill folder | Use when | +| --- | --- | +| [`dev-workflow`](dev-workflow/SKILL.md) | Running installs/tests, understanding CI, branch rules for PRs to `master` | +| [`contentstack-utils`](contentstack-utils/SKILL.md) | Changing public APIs, `Utils` / `GQL`, parsers, models, or embed rendering | +| [`php-style`](php-style/SKILL.md) | Code style, PHPCS/PSR-2, namespaces, or `src/` layout | +| [`testing`](testing/SKILL.md) | Adding or changing PHPUnit tests, mocks, or coverage | +| [`code-review`](code-review/SKILL.md) | Preparing or reviewing a PR for this repository | + +Each folder contains `SKILL.md` with YAML frontmatter (`name`, `description`). diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md new file mode 100644 index 0000000..811ad3a --- /dev/null +++ b/skills/code-review/SKILL.md @@ -0,0 +1,34 @@ +--- +name: code-review +description: Use when reviewing or preparing a PR — tests, style, SemVer, docs, and branch rules for this repository. +--- + +# Code review – Contentstack Utils PHP + +## When to use + +- Authoring a pull request into this repo +- Reviewing someone else’s PR for completeness and repo policy + +## Instructions + +### Checklist + +- **Tests:** New or changed behavior has PHPUnit coverage; `composer test` passes. +- **Style:** `composer check-style` passes (PSR-2 via PHPCS). +- **Docs:** User-visible behavior changes reflected in [`README.md`](../../README.md) or other maintainer-facing docs as appropriate. +- **API / SemVer:** Public signatures and documented behavior follow semantic versioning expectations per [`CONTRIBUTING.md`](../../CONTRIBUTING.md). +- **Branch policy:** PRs targeting **`master`** must come from the **`next`** branch per [`.github/workflows/check-branch.yml`](../../.github/workflows/check-branch.yml); confirm with the team if process changes. +- **Commits:** Prefer coherent history; squash noisy WIP commits when requested. + +### Severity (optional labels) + +- **Blocker:** Breaks tests, CI, or documented security/policy requirements. +- **Major:** Missing tests for non-trivial logic, public API breakage without version strategy, or policy violations (e.g. wrong base branch). +- **Minor:** Style nits, naming, or internal refactors with no contract change. + +## References + +- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) +- [`PULL_REQUEST_TEMPLATE.md`](../../PULL_REQUEST_TEMPLATE.md) +- [`dev-workflow/SKILL.md`](../dev-workflow/SKILL.md) diff --git a/skills/contentstack-utils/SKILL.md b/skills/contentstack-utils/SKILL.md new file mode 100644 index 0000000..234e472 --- /dev/null +++ b/skills/contentstack-utils/SKILL.md @@ -0,0 +1,42 @@ +--- +name: contentstack-utils +description: Use when changing Utils, GQL, BaseParser, models, enums, or embed/render behavior — the library’s public surface and integration boundaries. +--- + +# Contentstack Utils API – Contentstack Utils PHP + +## When to use + +- Adding or changing HTML/RTE rendering or GraphQL JSON → HTML conversion +- Extending or implementing render callbacks (`Option`, `RenderableInterface`) +- Working with metadata, embedded items, or enum-backed node/mark types + +## Instructions + +### Package identity + +- Composer package: `contentstack/utils` ([Packagist](https://packagist.org/packages/contentstack/utils)); PSR-4 root namespace `Contentstack\Utils\` maps to `src/`. + +### Main entry points (illustrative) + +- **`Contentstack\Utils\Utils`** ([`src/Utils.php`](../../src/Utils.php)) — static helpers such as `renderContent` / `jsonToHtml` for RTE strings and JSON payloads; extends `BaseParser`. +- **`Contentstack\Utils\GQL`** ([`src/GQL.php`](../../src/GQL.php)) — `jsonToHtml` and related parsing for GraphQL-shaped content; extends `BaseParser`. +- **`Contentstack\Utils\BaseParser`** ([`src/BaseParser.php`](../../src/BaseParser.php)) — shared parsing and embedded-object resolution logic. + +### Extension / integration + +- **`Contentstack\Utils\Model\Option`** — configure rendering; subclasses override `renderMark`, `renderNode`, `renderOptions`, etc. +- **`Contentstack\Utils\Resource\RenderableInterface`** — contract for types that participate in rendering. +- **`Contentstack\Utils\Model\Metadata`** — embed metadata (e.g. style type, attributes). +- **Enums** under `Contentstack\Utils\Enum\` (`NodeType`, `MarkType`, `StyleType`, `EmbedItemType`) use `marc-mabe/php-enum`. + +### Boundaries + +- This package does not ship HTTP calls; consumers fetch data with the Contentstack PHP SDK or other clients and pass strings/objects into `Utils` / `GQL`. +- Prefer backward-compatible changes to public method signatures; follow SemVer. + +## References + +- [`testing/SKILL.md`](../testing/SKILL.md) +- [`php-style/SKILL.md`](../php-style/SKILL.md) +- [Product README — usage](../../README.md) diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md new file mode 100644 index 0000000..e772790 --- /dev/null +++ b/skills/dev-workflow/SKILL.md @@ -0,0 +1,44 @@ +--- +name: dev-workflow +description: Use when running Composer, CI, or PRs to master/next — installs, tests, and branch policy for this repo. +--- + +# Dev workflow – Contentstack Utils PHP + +## When to use + +- Installing dependencies or running the test suite locally +- Understanding what GitHub Actions run on push/PR +- Opening or reviewing a PR and matching branch expectations + +## Instructions + +### Local setup + +- From the repo root: `composer install` (use `composer install --ignore-platform-reqs` only if you must match CI’s relaxed install; see `.github/workflows/ci.yml`). +- Run tests: `composer test` (runs PHPUnit as defined in `composer.json`). + +### Lint + +- Check: `composer check-style` (PHPCS on `src` and `tests`). +- Autofix where supported: `composer fix-style` (PHPCBF). + +### CI + +- [`ci.yml`](../../.github/workflows/ci.yml): `composer validate`, cache + `composer install`, then `composer run-script test`. +- Lint is not currently enforced in that workflow; still run `check-style` before pushing. + +### Branch / PR policy + +- [`check-branch.yml`](../../.github/workflows/check-branch.yml): PRs **into `master`** must use branch **`next`** as the head branch (automation comments and fails otherwise). +- Prefer feature branches (not your fork’s default branch name as the only source) per [`CONTRIBUTING.md`](../../CONTRIBUTING.md). + +### Releases / versioning + +- Project follows SemVer per [`CONTRIBUTING.md`](../../CONTRIBUTING.md); public API changes belong in changelog/release notes as the maintainers require. + +## References + +- [`AGENTS.md`](../../AGENTS.md) +- [`testing/SKILL.md`](../testing/SKILL.md) +- [`php-style/SKILL.md`](../php-style/SKILL.md) diff --git a/skills/php-style/SKILL.md b/skills/php-style/SKILL.md new file mode 100644 index 0000000..cf4cf18 --- /dev/null +++ b/skills/php-style/SKILL.md @@ -0,0 +1,37 @@ +--- +name: php-style +description: Use when applying PSR-2, running PHPCS, or matching src/tests layout and strict_types in this repo. +--- + +# PHP style & layout – Contentstack Utils PHP + +## When to use + +- Fixing or avoiding PHPCS violations +- Adding new classes under `src/` or tests under `tests/` +- Aligning with existing `declare(strict_types=1);` usage + +## Instructions + +### Coding standard + +- **PHPCS** ruleset: [`phpcs.xml.dist`](../../phpcs.xml.dist) extends **PSR-2** (``). +- Check: `composer check-style`; fix: `composer fix-style` (both target `src` and `tests` per `composer.json` scripts). + +### File layout + +- **Source:** `src/` — namespaces mirror directories (e.g. `Contentstack\Utils\Model\Option` → `src/Model/Option.php`). +- **Tests:** `tests/` — namespace `Contentstack\Tests\Utils\` per `composer.json` `autoload-dev`. + +### PHP version + +- `composer.json` requires `php: >=7.2`; avoid language features that require newer versions unless the project explicitly raises the minimum. + +### Strict typing + +- New PHP files in `src/` should use `declare(strict_types=1);` consistently with existing classes. + +## References + +- [`dev-workflow/SKILL.md`](../dev-workflow/SKILL.md) +- [PSR-2 coding style guide](https://www.php-fig.org/psr/psr-2/) diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md new file mode 100644 index 0000000..d041a38 --- /dev/null +++ b/skills/testing/SKILL.md @@ -0,0 +1,40 @@ +--- +name: testing +description: Use when writing PHPUnit tests, mocks under tests/Mock, or coverage/output under build/ for this package. +--- + +# Testing – Contentstack Utils PHP + +## When to use + +- Adding or updating unit tests for `Utils`, `GQL`, models, or helpers +- Extending mocks/fixtures under `tests/Mock` or `tests/Helpers` +- Interpreting PHPUnit config, coverage, or CI test runs + +## Instructions + +### Runner and config + +- **Command:** `composer test` → PHPUnit (`phpunit/phpunit` ^9.3). +- **Config:** [`phpunit.xml`](../../phpunit.xml) — bootstrap `vendor/autoload.php`, suite directory `tests/`, whitelist coverage on `src/`. + +### Layout + +- Test classes live directly under `tests/` (e.g. `UtilsTest.php`, `GQLTest.php`, `MetadataTest.php`) plus **`tests/Mock/`** (`JsonMock.php`, `EmbedObjectMock.php`, etc.) and **`tests/Helpers/`** (`Utility.php`). + +### Coverage and reports + +- `phpunit.xml` logs JUnit, coverage (HTML, text, Clover), and testdox output under **`build/`** (e.g. `build/coverage/`, `build/logs/`). Add `build/` to `.gitignore` if generating locally; do not commit generated artifacts unless the project asks for them. + +### Expectations for contributions + +- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) requires tests for changes; new behavior should include PHPUnit coverage in the same PR where feasible. + +### Credentials + +- Tests use mocks and fixtures; do not add real API keys or stack secrets to the repo. + +## References + +- [`dev-workflow/SKILL.md`](../dev-workflow/SKILL.md) +- [`contentstack-utils/SKILL.md`](../contentstack-utils/SKILL.md) From da0e4759ab1c1eb3669d52780bad7a044abf6773 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 8 Apr 2026 15:04:21 +0530 Subject: [PATCH 2/9] Update branch restriction messages in check-branch.yml to require pull requests from the development branch instead of the next branch. --- .github/workflows/check-branch.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml index 1e2d24a..421c253 100644 --- a/.github/workflows/check-branch.yml +++ b/.github/workflows/check-branch.yml @@ -8,13 +8,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Comment PR - if: github.base_ref == 'master' && github.head_ref != 'next' + if: github.base_ref == 'master' && github.head_ref != 'development' uses: thollander/actions-comment-pull-request@v2 with: message: | - We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the next branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch. + We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the development branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch. - name: Check branch - if: github.base_ref == 'master' && github.head_ref != 'next' + if: github.base_ref == 'master' && github.head_ref != 'development' run: | - echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the next branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch." + echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the development branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch." exit 1 \ No newline at end of file From 9cdd7617594dcc580612d5bbed549e9916ae29a3 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 8 Apr 2026 15:05:32 +0530 Subject: [PATCH 3/9] Update copyright year in LICENSE.md to 2026 --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 1e02d05..d7e40d6 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ The MIT License (MIT) -Copyright (c) 2016-2024 Contentstack +Copyright (c) 2016-2026 Contentstack > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal From 7997c06059777a6cbd3a836f11c2cfc015c80c16 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Fri, 10 Apr 2026 13:25:47 +0530 Subject: [PATCH 4/9] Remove references section from multiple SKILL.md files to streamline documentation. --- skills/code-review/SKILL.md | 6 ------ skills/contentstack-utils/SKILL.md | 6 ------ skills/dev-workflow/SKILL.md | 6 ------ skills/php-style/SKILL.md | 5 ----- skills/testing/SKILL.md | 5 ----- 5 files changed, 28 deletions(-) diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md index 811ad3a..1ddf02a 100644 --- a/skills/code-review/SKILL.md +++ b/skills/code-review/SKILL.md @@ -26,9 +26,3 @@ description: Use when reviewing or preparing a PR — tests, style, SemVer, docs - **Blocker:** Breaks tests, CI, or documented security/policy requirements. - **Major:** Missing tests for non-trivial logic, public API breakage without version strategy, or policy violations (e.g. wrong base branch). - **Minor:** Style nits, naming, or internal refactors with no contract change. - -## References - -- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) -- [`PULL_REQUEST_TEMPLATE.md`](../../PULL_REQUEST_TEMPLATE.md) -- [`dev-workflow/SKILL.md`](../dev-workflow/SKILL.md) diff --git a/skills/contentstack-utils/SKILL.md b/skills/contentstack-utils/SKILL.md index 234e472..9e3a0db 100644 --- a/skills/contentstack-utils/SKILL.md +++ b/skills/contentstack-utils/SKILL.md @@ -34,9 +34,3 @@ description: Use when changing Utils, GQL, BaseParser, models, enums, or embed/r - This package does not ship HTTP calls; consumers fetch data with the Contentstack PHP SDK or other clients and pass strings/objects into `Utils` / `GQL`. - Prefer backward-compatible changes to public method signatures; follow SemVer. - -## References - -- [`testing/SKILL.md`](../testing/SKILL.md) -- [`php-style/SKILL.md`](../php-style/SKILL.md) -- [Product README — usage](../../README.md) diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md index e772790..f922a99 100644 --- a/skills/dev-workflow/SKILL.md +++ b/skills/dev-workflow/SKILL.md @@ -36,9 +36,3 @@ description: Use when running Composer, CI, or PRs to master/next — installs, ### Releases / versioning - Project follows SemVer per [`CONTRIBUTING.md`](../../CONTRIBUTING.md); public API changes belong in changelog/release notes as the maintainers require. - -## References - -- [`AGENTS.md`](../../AGENTS.md) -- [`testing/SKILL.md`](../testing/SKILL.md) -- [`php-style/SKILL.md`](../php-style/SKILL.md) diff --git a/skills/php-style/SKILL.md b/skills/php-style/SKILL.md index cf4cf18..41f9d90 100644 --- a/skills/php-style/SKILL.md +++ b/skills/php-style/SKILL.md @@ -30,8 +30,3 @@ description: Use when applying PSR-2, running PHPCS, or matching src/tests layou ### Strict typing - New PHP files in `src/` should use `declare(strict_types=1);` consistently with existing classes. - -## References - -- [`dev-workflow/SKILL.md`](../dev-workflow/SKILL.md) -- [PSR-2 coding style guide](https://www.php-fig.org/psr/psr-2/) diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md index d041a38..ca2a69e 100644 --- a/skills/testing/SKILL.md +++ b/skills/testing/SKILL.md @@ -33,8 +33,3 @@ description: Use when writing PHPUnit tests, mocks under tests/Mock, or coverage ### Credentials - Tests use mocks and fixtures; do not add real API keys or stack secrets to the repo. - -## References - -- [`dev-workflow/SKILL.md`](../dev-workflow/SKILL.md) -- [`contentstack-utils/SKILL.md`](../contentstack-utils/SKILL.md) From 1c81513f386d3b6e93dc52fc60752b445cba3b28 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 29 Apr 2026 08:12:55 +0530 Subject: [PATCH 5/9] chore: align release workflows with new development-to-main process --- .github/workflows/back-merge-pr.yml | 54 +++++++++++++++ .github/workflows/check-branch.yml | 20 ------ .github/workflows/check-version-bump.yml | 86 ++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/back-merge-pr.yml delete mode 100644 .github/workflows/check-branch.yml create mode 100644 .github/workflows/check-version-bump.yml diff --git a/.github/workflows/back-merge-pr.yml b/.github/workflows/back-merge-pr.yml new file mode 100644 index 0000000..02b378c --- /dev/null +++ b/.github/workflows/back-merge-pr.yml @@ -0,0 +1,54 @@ +name: Back-merge master to development + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + open-back-merge-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Open back-merge PR if needed + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + BASE_BRANCH="development" + SOURCE_BRANCH="master" + + git fetch origin "$BASE_BRANCH" "$SOURCE_BRANCH" + + if ! git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then + echo "Base branch '$BASE_BRANCH' does not exist on origin; skipping." + exit 0 + fi + + SOURCE_SHA=$(git rev-parse "origin/$SOURCE_BRANCH") + BASE_SHA=$(git rev-parse "origin/$BASE_BRANCH") + + if [ "$SOURCE_SHA" = "$BASE_SHA" ]; then + echo "$SOURCE_BRANCH and $BASE_BRANCH are at the same commit; nothing to back-merge." + exit 0 + fi + + EXISTING=$(gh pr list --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --state open --json number --jq 'length') + + if [ "$EXISTING" -gt 0 ]; then + echo "An open PR from $SOURCE_BRANCH to $BASE_BRANCH already exists; skipping." + exit 0 + fi + + gh pr create --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --title "chore: back-merge $SOURCE_BRANCH into $BASE_BRANCH" --body "Automated back-merge after changes landed on \\`$SOURCE_BRANCH\\`. Review and merge to keep \\`$BASE_BRANCH\\` in sync." + + echo "Created back-merge PR $SOURCE_BRANCH -> $BASE_BRANCH." diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml deleted file mode 100644 index 421c253..0000000 --- a/.github/workflows/check-branch.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Check Branch' - -on: - pull_request: - -jobs: - check_branch: - runs-on: ubuntu-latest - steps: - - name: Comment PR - if: github.base_ref == 'master' && github.head_ref != 'development' - uses: thollander/actions-comment-pull-request@v2 - with: - message: | - We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the development branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch. - - name: Check branch - if: github.base_ref == 'master' && github.head_ref != 'development' - run: | - echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the development branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch." - exit 1 \ No newline at end of file diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml new file mode 100644 index 0000000..8e71000 --- /dev/null +++ b/.github/workflows/check-version-bump.yml @@ -0,0 +1,86 @@ +name: Check Version Bump + +on: + pull_request: + +jobs: + version-bump: + name: Version & Changelog bump + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect changed files and version bump + id: detect + run: | + if git rev-parse HEAD^2 >/dev/null 2>&1; then + FILES=$(git diff --name-only HEAD^1 HEAD^2) + else + FILES=$(git diff --name-only HEAD~1 HEAD) + fi + VERSION_FILES_CHANGED=false + echo "$FILES" | grep -qx 'package.json' && VERSION_FILES_CHANGED=true + echo "$FILES" | grep -qx 'CHANGELOG.md' && VERSION_FILES_CHANGED=true + echo "version_files_changed=$VERSION_FILES_CHANGED" >> $GITHUB_OUTPUT + # Only lib/, webpack/, dist/, package.json count as release-affecting; .github/ and test/ do not + CODE_CHANGED=false + echo "$FILES" | grep -qE '^lib/|^webpack/|^dist/' && CODE_CHANGED=true + echo "$FILES" | grep -qx 'package.json' && CODE_CHANGED=true + echo "code_changed=$CODE_CHANGED" >> $GITHUB_OUTPUT + + - name: Skip when only test/docs/.github changed + if: steps.detect.outputs.code_changed != 'true' + run: | + echo "No release-affecting files changed (e.g. only test/docs/.github). Skipping version-bump check." + exit 0 + + - name: Fail when version bump was missed + if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed != 'true' + run: | + echo "::error::This PR has code changes but no version bump. Please bump the version in package.json and add an entry in CHANGELOG.md." + exit 1 + + - name: Setup Node + if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed == 'true' + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Check version bump + if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed == 'true' + run: | + set -e + PKG_VERSION=$(node -p "require('./package.json').version.replace(/^v/, '')") + if [ -z "$PKG_VERSION" ]; then + echo "::error::Could not read version from package.json" + exit 1 + fi + git fetch --tags --force 2>/dev/null || true + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true) + if [ -z "$LATEST_TAG" ]; then + echo "No existing tags found. Skipping version-bump check (first release)." + exit 0 + fi + LATEST_VERSION="${LATEST_TAG#v}" + LATEST_VERSION="${LATEST_VERSION%%-*}" + if [ "$(printf '%s\n' "$LATEST_VERSION" "$PKG_VERSION" | sort -V | tail -1)" != "$PKG_VERSION" ]; then + echo "::error::Version bump required: package.json version ($PKG_VERSION) is not greater than latest tag ($LATEST_TAG). Please bump the version in package.json." + exit 1 + fi + if [ "$PKG_VERSION" = "$LATEST_VERSION" ]; then + echo "::error::Version bump required: package.json version ($PKG_VERSION) equals latest tag ($LATEST_TAG). Please bump the version in package.json." + exit 1 + fi + CHANGELOG_VERSION=$(sed -nE 's/^## \[v?([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' CHANGELOG.md | head -1) + if [ -z "$CHANGELOG_VERSION" ]; then + echo "::error::Could not find a version entry in CHANGELOG.md (expected line like '## [v1.0.0](...)')." + exit 1 + fi + if [ "$CHANGELOG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::CHANGELOG version mismatch: CHANGELOG.md top version ($CHANGELOG_VERSION) does not match package.json version ($PKG_VERSION). Please add or update the CHANGELOG entry for $PKG_VERSION." + exit 1 + fi + echo "Version bump check passed: package.json and CHANGELOG.md are at $PKG_VERSION (latest tag: $LATEST_TAG)." From e9169c45b636d5c7ae650021ce6c0376e3c51a9d Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:01:27 +0530 Subject: [PATCH 6/9] Added endpoint changes --- .gitignore | 3 +- CHANGELOG.md | 10 +- README.md | 239 +++++++++++++++++++++++++++++++++ composer.json | 5 +- scripts/download-regions.php | 79 +++++++++++ src/Endpoint.php | 213 +++++++++++++++++++++++++++++ src/Utils.php | 18 +++ tests/EndpointTest.php | 253 +++++++++++++++++++++++++++++++++++ 8 files changed, 816 insertions(+), 4 deletions(-) create mode 100644 scripts/download-regions.php create mode 100644 src/Endpoint.php create mode 100644 tests/EndpointTest.php diff --git a/.gitignore b/.gitignore index 8c52a76..4b8402a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ build vendor phpcs.xml .phpunit.result.cache -*/.DS_Store \ No newline at end of file +*/.DS_Store +src/assets/regions.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef7307..af262b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index dc552b5..1cc820a 100644 --- a/README.md +++ b/README.md @@ -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('', '', ''); +$stack->setHost($host); + +// Fetch entries — all requests now go to the EU CDN +$result = $stack->ContentType('')->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('', '', ''); + $stack->setHost($host); + + $result = $stack->ContentType('')->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." +} ``` \ No newline at end of file diff --git a/composer.json b/composer.json index 42aa65e..d95db1a 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/scripts/download-regions.php b/scripts/download-regions.php new file mode 100644 index 0000000..23404bd --- /dev/null +++ b/scripts/download-regions.php @@ -0,0 +1,79 @@ + 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"; diff --git a/src/Endpoint.php b/src/Endpoint.php new file mode 100644 index 0000000..cf00c4e --- /dev/null +++ b/src/Endpoint.php @@ -0,0 +1,213 @@ +|null */ + private static $regionsData = null; + + /** + * Resolve a Contentstack service endpoint URL for a given region. + * + * @param string $region Region ID or alias (e.g. 'us', 'eu', 'azure-na', 'gcp-eu'). + * Defaults to 'us' (AWS North America). + * @param string $service Optional service key (e.g. 'contentDelivery', 'contentManagement', + * 'auth', 'graphqlDelivery'). When empty, all endpoints are returned. + * @param bool $omitHttps When true, strips the 'https://' prefix from every URL. + * + * @return string|array Single URL string when $service is provided, + * associative array of all service URLs otherwise. + * + * @throws \InvalidArgumentException When region is empty, unknown, or service is not found. + * @throws \RuntimeException When the bundled regions.json cannot be read or parsed. + */ + public static function getContentstackEndpoint( + string $region = 'us', + string $service = '', + bool $omitHttps = false + ) { + if ($region === '') { + throw new \InvalidArgumentException( + 'Empty region provided. Please put valid region.' + ); + } + + $data = self::loadRegions(); + $normalized = strtolower(trim($region)); + $regionRow = self::findRegionByIdOrAlias($data['regions'], $normalized); + + if ($regionRow === null) { + throw new \InvalidArgumentException("Invalid region: {$region}"); + } + + if ($service !== '') { + if (!array_key_exists($service, $regionRow['endpoints'])) { + throw new \InvalidArgumentException( + "Service \"{$service}\" not found for region \"{$regionRow['id']}\"" + ); + } + $url = $regionRow['endpoints'][$service]; + return $omitHttps ? self::stripHttps($url) : $url; + } + + $endpoints = $regionRow['endpoints']; + return $omitHttps ? self::stripHttpsFromMap($endpoints) : $endpoints; + } + + /** @var string */ + const REGIONS_URL = 'https://artifacts.contentstack.com/regions.json'; + + /** + * Load and cache regions.json. + * + * Resolution order: + * 1. In-memory static cache (fastest, zero I/O after first call) + * 2. src/assets/regions.json on disk (written by composer install script) + * 3. Live download from artifacts.contentstack.com (fallback when the + * package is used as a dependency and the file was not yet created) + * + * @return array + */ + private static function loadRegions(): array + { + if (self::$regionsData !== null) { + return self::$regionsData; + } + + $path = __DIR__ . '/assets/regions.json'; + + if (!file_exists($path)) { + self::downloadAndSave($path); + } + + if (!file_exists($path)) { + throw new \RuntimeException( + 'contentstack/utils: regions.json not found and could not be downloaded. ' . + 'Run "composer install" or "composer refresh-regions" and ensure network access.' + ); + } + + $raw = file_get_contents($path); + if ($raw === false) { + throw new \RuntimeException( + 'contentstack/utils: Could not read regions.json.' + ); + } + + $decoded = json_decode($raw, true); + if (!is_array($decoded) || !isset($decoded['regions'])) { + throw new \RuntimeException( + 'contentstack/utils: regions.json is corrupt. ' . + 'Run "composer refresh-regions" to re-download it.' + ); + } + + self::$regionsData = $decoded; + return self::$regionsData; + } + + /** + * Download regions.json from the Contentstack CDN and write it to disk. + * Tries the PHP curl extension first, falls back to file_get_contents. + * Silent on failure — caller decides whether the missing file is fatal. + * + * @param string $dest Absolute path to write the file to. + */ + private static function downloadAndSave(string $dest): void + { + $dir = dirname($dest); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $data = null; + + if (extension_loaded('curl')) { + $ch = curl_init(self::REGIONS_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); + curl_close($ch); + if ($response !== false && $httpCode === 200) { + $data = $response; + } + } + + if ($data === null) { + $ctx = stream_context_create(['http' => ['timeout' => 30]]); + $data = @file_get_contents(self::REGIONS_URL, false, $ctx); + } + + if (!$data) { + return; + } + + $decoded = json_decode($data, true); + if (is_array($decoded) && isset($decoded['regions'])) { + file_put_contents($dest, $data); + } + } + + /** + * Find a region entry by its id or any of its aliases (case-insensitive). + * + * @param array> $regions + * @param string $input Already lowercased input. + * @return array|null + */ + private static function findRegionByIdOrAlias(array $regions, string $input): ?array + { + foreach ($regions as $row) { + if ($row['id'] === $input) { + return $row; + } + } + foreach ($regions as $row) { + foreach ($row['alias'] as $alias) { + if (strtolower($alias) === $input) { + return $row; + } + } + } + return null; + } + + /** + * Strip the https:// (or http://) scheme from a URL string. + */ + private static function stripHttps(string $url): string + { + return (string) preg_replace('/^https?:\/\//', '', $url); + } + + /** + * Strip https:// from every value in an endpoint map. + * + * @param array $endpoints + * @return array + */ + private static function stripHttpsFromMap(array $endpoints): array + { + $result = []; + foreach ($endpoints as $key => $url) { + $result[$key] = self::stripHttps($url); + } + return $result; + } + + /** + * Reset the internal region cache (intended for testing only). + */ + public static function resetCache(): void + { + self::$regionsData = null; + } +} diff --git a/src/Utils.php b/src/Utils.php index d18479d..9d377fe 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -84,6 +84,24 @@ public static function jsonToHtml(object $content, Option $option): string { return $resultHtml; } + /** + * Resolve a Contentstack service endpoint URL for a given region. + * + * @param string $region Region ID or alias (e.g. 'us', 'eu', 'azure-na', 'gcp-eu'). + * @param string $service Optional service key (e.g. 'contentDelivery', 'contentManagement'). + * When empty, all endpoints for the region are returned as an array. + * @param bool $omitHttps When true, strips the 'https://' prefix from returned URL(s). + * + * @return string|array + */ + public static function getContentstackEndpoint( + string $region = 'us', + string $service = '', + bool $omitHttps = false + ) { + return Endpoint::getContentstackEndpoint($region, $service, $omitHttps); + } + protected static function findObject(Metadata $metadata, array $entry): array { if (array_key_exists('_embedded_items', $entry)) diff --git a/tests/EndpointTest.php b/tests/EndpointTest.php new file mode 100644 index 0000000..5885311 --- /dev/null +++ b/tests/EndpointTest.php @@ -0,0 +1,253 @@ +assertIsArray($endpoints); + $this->assertArrayHasKey('contentDelivery', $endpoints); + $this->assertArrayHasKey('contentManagement', $endpoints); + } + + public function testDefaultRegionContentDelivery(): void + { + $url = Endpoint::getContentstackEndpoint('us', 'contentDelivery'); + $this->assertSame('https://cdn.contentstack.io', $url); + } + + public function testDefaultRegionContentManagement(): void + { + $url = Endpoint::getContentstackEndpoint('us', 'contentManagement'); + $this->assertSame('https://api.contentstack.io', $url); + } + + // ------------------------------------------------------------------------- + // Region aliases resolve to the same region + // ------------------------------------------------------------------------- + + /** + * @dataProvider naAliasProvider + */ + public function testNaRegionAliasesResolveToSameEndpoint(string $alias): void + { + $url = Endpoint::getContentstackEndpoint($alias, 'contentDelivery'); + $this->assertSame('https://cdn.contentstack.io', $url); + } + + public function naAliasProvider(): array + { + return [ + 'id na' => ['na'], + 'alias us' => ['us'], + 'alias aws-na' => ['aws-na'], + 'alias aws_na' => ['aws_na'], + 'upper NA' => ['NA'], + 'upper US' => ['US'], + ]; + } + + // ------------------------------------------------------------------------- + // All seven regions – contentDelivery spot-checks + // ------------------------------------------------------------------------- + + /** + * @dataProvider regionContentDeliveryProvider + */ + public function testContentDeliveryUrlByRegion(string $region, string $expected): void + { + $url = Endpoint::getContentstackEndpoint($region, 'contentDelivery'); + $this->assertSame($expected, $url); + } + + public function regionContentDeliveryProvider(): array + { + return [ + 'na' => ['na', 'https://cdn.contentstack.io'], + 'eu' => ['eu', 'https://eu-cdn.contentstack.com'], + 'au' => ['au', 'https://au-cdn.contentstack.com'], + 'azure-na' => ['azure-na', 'https://azure-na-cdn.contentstack.com'], + 'azure-eu' => ['azure-eu', 'https://azure-eu-cdn.contentstack.com'], + 'gcp-na' => ['gcp-na', 'https://gcp-na-cdn.contentstack.com'], + 'gcp-eu' => ['gcp-eu', 'https://gcp-eu-cdn.contentstack.com'], + ]; + } + + /** + * @dataProvider regionContentManagementProvider + */ + public function testContentManagementUrlByRegion(string $region, string $expected): void + { + $url = Endpoint::getContentstackEndpoint($region, 'contentManagement'); + $this->assertSame($expected, $url); + } + + public function regionContentManagementProvider(): array + { + return [ + 'na' => ['na', 'https://api.contentstack.io'], + 'eu' => ['eu', 'https://eu-api.contentstack.com'], + 'au' => ['au', 'https://au-api.contentstack.com'], + 'azure-na' => ['azure-na', 'https://azure-na-api.contentstack.com'], + 'azure-eu' => ['azure-eu', 'https://azure-eu-api.contentstack.com'], + 'gcp-na' => ['gcp-na', 'https://gcp-na-api.contentstack.com'], + 'gcp-eu' => ['gcp-eu', 'https://gcp-eu-api.contentstack.com'], + ]; + } + + // ------------------------------------------------------------------------- + // All service keys present for a region + // ------------------------------------------------------------------------- + + public function testAllServiceKeysPresent(): void + { + $expected = [ + 'application', 'contentDelivery', 'contentManagement', 'auth', + 'graphqlDelivery', 'preview', 'graphqlPreview', 'images', 'assets', + 'automate', 'launch', 'developerHub', 'brandKit', 'genAI', + 'personalizeManagement', 'personalizeEdge', 'composableStudio', + ]; + $endpoints = Endpoint::getContentstackEndpoint('eu'); + foreach ($expected as $key) { + $this->assertArrayHasKey($key, $endpoints, "Missing service key: {$key}"); + } + } + + // ------------------------------------------------------------------------- + // omitHttps flag + // ------------------------------------------------------------------------- + + public function testOmitHttpsStripsSchemeFromSingleService(): void + { + $url = Endpoint::getContentstackEndpoint('eu', 'contentDelivery', true); + $this->assertSame('eu-cdn.contentstack.com', $url); + } + + public function testOmitHttpsStripsSchemeFromAllServices(): void + { + $endpoints = Endpoint::getContentstackEndpoint('na', '', true); + $this->assertIsArray($endpoints); + foreach ($endpoints as $key => $url) { + $this->assertStringNotContainsString('https://', $url, "Service {$key} still has https://"); + $this->assertStringNotContainsString('http://', $url, "Service {$key} still has http://"); + } + } + + public function testOmitHttpsFalseRetainsScheme(): void + { + $url = Endpoint::getContentstackEndpoint('na', 'contentManagement', false); + $this->assertStringStartsWith('https://', $url); + } + + // ------------------------------------------------------------------------- + // Case-insensitive alias matching + // ------------------------------------------------------------------------- + + public function testUppercaseAliasResolves(): void + { + $url = Endpoint::getContentstackEndpoint('AWS-NA', 'contentDelivery'); + $this->assertSame('https://cdn.contentstack.io', $url); + } + + public function testUnderscoreAliasResolves(): void + { + $url = Endpoint::getContentstackEndpoint('azure_na', 'contentDelivery'); + $this->assertSame('https://azure-na-cdn.contentstack.com', $url); + } + + public function testGcpUnderscoreAliasResolves(): void + { + $url = Endpoint::getContentstackEndpoint('gcp_eu', 'contentManagement'); + $this->assertSame('https://gcp-eu-api.contentstack.com', $url); + } + + // ------------------------------------------------------------------------- + // Return-all-endpoints (no service) + // ------------------------------------------------------------------------- + + public function testNoServiceReturnsArray(): void + { + $result = Endpoint::getContentstackEndpoint('au'); + $this->assertIsArray($result); + $this->assertGreaterThan(1, count($result)); + } + + public function testNoServiceContainsCorrectUrls(): void + { + $endpoints = Endpoint::getContentstackEndpoint('au'); + $this->assertSame('https://au-cdn.contentstack.com', $endpoints['contentDelivery']); + $this->assertSame('https://au-api.contentstack.com', $endpoints['contentManagement']); + } + + // ------------------------------------------------------------------------- + // Error cases + // ------------------------------------------------------------------------- + + public function testEmptyRegionThrowsInvalidArgument(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Empty region provided'); + Endpoint::getContentstackEndpoint(''); + } + + public function testUnknownRegionThrowsInvalidArgument(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid region: invalid-region'); + Endpoint::getContentstackEndpoint('invalid-region'); + } + + public function testUnknownServiceThrowsInvalidArgument(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Service "unknownService" not found'); + Endpoint::getContentstackEndpoint('na', 'unknownService'); + } + + // ------------------------------------------------------------------------- + // Utils::getContentstackEndpoint() proxy + // ------------------------------------------------------------------------- + + public function testUtilsProxyReturnsSameResultAsEndpointClass(): void + { + $viaEndpoint = Endpoint::getContentstackEndpoint('eu', 'contentDelivery'); + $viaUtils = Utils::getContentstackEndpoint('eu', 'contentDelivery'); + $this->assertSame($viaEndpoint, $viaUtils); + } + + public function testUtilsProxyDefaultRegion(): void + { + $url = Utils::getContentstackEndpoint('us', 'contentManagement'); + $this->assertSame('https://api.contentstack.io', $url); + } + + public function testUtilsProxyOmitHttps(): void + { + $url = Utils::getContentstackEndpoint('gcp-na', 'contentDelivery', true); + $this->assertSame('gcp-na-cdn.contentstack.com', $url); + } + + public function testUtilsProxyAllEndpoints(): void + { + $endpoints = Utils::getContentstackEndpoint('azure-eu'); + $this->assertIsArray($endpoints); + $this->assertArrayHasKey('contentDelivery', $endpoints); + } +} From fb46255b6d65c868906507dc1d51b436154d40e8 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:34:06 +0530 Subject: [PATCH 7/9] docs: add endpoint resolution documentation Co-Authored-By: Claude Sonnet 4.6 --- docs/endpoint-integration-overview.md | 359 +++++++++++ docs/endpoint-resolution.md | 838 ++++++++++++++++++++++++++ 2 files changed, 1197 insertions(+) create mode 100644 docs/endpoint-integration-overview.md create mode 100644 docs/endpoint-resolution.md diff --git a/docs/endpoint-integration-overview.md b/docs/endpoint-integration-overview.md new file mode 100644 index 0000000..2674edc --- /dev/null +++ b/docs/endpoint-integration-overview.md @@ -0,0 +1,359 @@ +# Endpoint Feature — Integration Overview + +## The Problem Being Solved + +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. + +--- + +## Part 1 — The Data Source (`regions.json`) + +Contentstack maintains a live registry at: +``` +https://artifacts.contentstack.com/regions.json +``` + +Structure: +``` +{ + "regions": [ + { + "id": "na", ← canonical region ID + "alias": ["us","aws-na","AWS-NA"...] ← all accepted aliases + "isDefault": true, + "endpoints": { + "contentDelivery": "https://cdn.contentstack.io", + "contentManagement": "https://api.contentstack.io", + "graphqlDelivery": "https://graphql.contentstack.com", + "auth": "https://auth-api.contentstack.com", + "preview": "https://rest-preview.contentstack.com", + ... 18 services total + } + }, + { "id": "eu", ... }, + { "id": "au", ... }, + { "id": "azure-na", ... }, + { "id": "azure-eu", ... }, + { "id": "gcp-na", ... }, + { "id": "gcp-eu", ... } ← 7 regions total + ] +} +``` + +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. + +--- + +## Part 2 — Keeping Regions Up To Date (`refresh-regions`) + +Contentstack occasionally adds new regions or services. The workflow to update is: + +```bash +# Pull the latest registry from Contentstack and overwrite the local file +composer refresh-regions + +# What this runs internally: +# php scripts/download-regions.php +# → curl https://artifacts.contentstack.com/regions.json +# → writes to src/assets/regions.json + +# Since regions.json is in .gitignore, no commit is needed — +# every developer and CI environment gets it fresh on composer install +``` + +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. + +--- + +## Part 3 — How `regions.json` Gets to Disk + +Unlike the JS SDK (which bundles the file at publish time), the PHP SDK downloads it in three layers: + +``` +Layer 1 — composer install / composer update (root package only) + │ + └── post-install-cmd fires + → @php scripts/download-regions.php + → curl https://artifacts.contentstack.com/regions.json + → writes src/assets/regions.json + → "contentstack/utils: regions.json downloaded (7 regions)." + +Layer 2 — Runtime fallback (when package is used as a dependency) + │ + └── First call to Endpoint::getContentstackEndpoint() + → file_exists('src/assets/regions.json') === false + → Endpoint::downloadAndSave() runs silently + → writes src/assets/regions.json + → continues normally + +Layer 3 — Static cache (fastest, zero I/O after first call) + │ + └── $regionsData already set in memory + → return cached data immediately + → no disk reads, no network calls +``` + +`src/assets/regions.json` is listed in `.gitignore` — it is never committed. Every environment provisions it independently. + +--- + +## Part 4 — `Endpoint::getContentstackEndpoint()` — How Resolution Works + +``` +Endpoint::getContentstackEndpoint('na', 'contentDelivery', false) + │ │ │ + │ │ └── omitHttps: keep https:// + │ └── service: which URL to return + └── region: ID or any alias +``` + +Step-by-step inside [src/Endpoint.php](../src/Endpoint.php): + +``` +Call arrives → getContentstackEndpoint('na', 'contentDelivery', false) + +1. Guard check + region === '' → throw InvalidArgumentException immediately + +2. loadRegions() [runs only once per PHP process] + First call: file_get_contents('src/assets/regions.json') + json_decode() → store in static $regionsData + Subsequent: return cached $regionsData ← no disk reads + +3. Normalize input + strtolower(trim('na')) → 'na' + +4. findRegionByIdOrAlias() + Pass 1 — match by id: + regions[0]['id'] === 'na' ✓ → return this region row + + Pass 2 — match by alias (only if Pass 1 fails): + e.g. 'AWS-NA' → strtolower → 'aws-na' + scan alias[] of each region until match found + + No match → throw InvalidArgumentException('Invalid region: ...') + +5. Service lookup + service === 'contentDelivery' + → $regionRow['endpoints']['contentDelivery'] + → "https://cdn.contentstack.io" + Key missing → throw InvalidArgumentException('Service not found') + +6. omitHttps check + false → return "https://cdn.contentstack.io" ← full URL + true → preg_replace('/^https?:\/\//', '') → "cdn.contentstack.io" + +7. No service provided (service === '') + → return entire $regionRow['endpoints'] array + → with omitHttps: strip scheme from every value +``` + +--- + +## Part 5 — `Utils::getContentstackEndpoint()` — Backward Compatibility + +[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: + +```php +// In Utils.php — just a thin pass-through, zero logic here +public static function getContentstackEndpoint( + string $region = 'us', + string $service = '', + bool $omitHttps = false +) { + return Endpoint::getContentstackEndpoint($region, $service, $omitHttps); +} + +// Both calls produce identical results: +Endpoint::getContentstackEndpoint('na', 'contentDelivery'); +Utils::getContentstackEndpoint('na', 'contentDelivery'); +``` + +--- + +## Part 6 — Integration with the PHP Delivery SDK (what `test.php` does) + +``` +test.php execution trace: +───────────────────────────────────────────────────────────────── + +1. Load autoloaders + require vendor/autoload.php ← utils-php (has new Endpoint class) + require contentstack-php/vendor/autoload.php ← delivery SDK + (utils-php loads first → its Contentstack\Utils\* classes win) + +2. Resolve endpoint + Endpoint::getContentstackEndpoint('na', 'contentDelivery') + → "https://cdn.contentstack.io" [for display] + + Endpoint::getContentstackEndpoint('na', 'contentDelivery', true) + → "cdn.contentstack.io" [host without scheme, for setHost()] + +3. Create delivery SDK Stack + Contentstack::Stack(API_KEY, DELIVERY_TOKEN, 'production') + → Stack object, host defaults to 'cdn.contentstack.io' (NA default) + +4. Override host with endpoint-resolved value + $stack->setHost('cdn.contentstack.io') + → host is now authoritative from regions.json, not hardcoded + +5. Fetch entries + $stack->ContentType('mega_menu')->Query()->toJSON()->find() + → HTTP GET https://cdn.contentstack.io/v3/content_types/mega_menu/entries + ?environment=production + → Returns 2 entries: "Region", "Topics Navigation" + +Output: + Total entries fetched: 2 + Entry #1 → bltc85890659eefc7c2 "Region" + Entry #2 → blt3d9080b4eba8defa "Topics Navigation" +``` + +The full `test.php` source is at [test.php](../test.php). + +--- + +## Part 7 — Switching Regions in Practice + +The key benefit: changing **one string** switches every URL automatically. + +```php +// NA (default) +$host = Endpoint::getContentstackEndpoint('na', 'contentDelivery', true); +// → cdn.contentstack.io + +// EU +$host = Endpoint::getContentstackEndpoint('eu', 'contentDelivery', true); +// → eu-cdn.contentstack.com + +// Azure EU +$host = Endpoint::getContentstackEndpoint('azure-eu', 'contentDelivery', true); +// → azure-eu-cdn.contentstack.com + +// GCP NA +$host = Endpoint::getContentstackEndpoint('gcp-na', 'contentDelivery', true); +// → gcp-na-cdn.contentstack.com + +// Then the same Stack setup works for any region: +$stack = Contentstack::Stack($API_KEY, $DELIVERY_TOKEN, $ENV); +$stack->setHost($host); +``` + +Reading the region from an environment variable is the recommended pattern: + +```php +$region = getenv('CONTENTSTACK_REGION') ?: 'na'; +$host = Endpoint::getContentstackEndpoint($region, 'contentDelivery', true); + +$stack = Contentstack::Stack( + getenv('CONTENTSTACK_API_KEY'), + getenv('CONTENTSTACK_DELIVERY_TOKEN'), + getenv('CONTENTSTACK_ENVIRONMENT') +); +$stack->setHost($host); +``` + +--- + +## Part 8 — Accepted Region Aliases + +| You pass | Resolves to region | +|---|---| +| `na`, `us`, `aws-na`, `aws_na`, `NA`, `US`, `AWS-NA`, `AWS_NA` | `na` | +| `eu`, `aws-eu`, `aws_eu`, `EU`, `AWS-EU`, `AWS_EU` | `eu` | +| `au`, `aws-au`, `aws_au`, `AU`, `AWS-AU`, `AWS_AU` | `au` | +| `azure-na`, `azure_na`, `AZURE-NA`, `AZURE_NA` | `azure-na` | +| `azure-eu`, `azure_eu`, `AZURE-EU`, `AZURE_EU` | `azure-eu` | +| `gcp-na`, `gcp_na`, `GCP-NA`, `GCP_NA` | `gcp-na` | +| `gcp-eu`, `gcp_eu`, `GCP-EU`, `GCP_EU` | `gcp-eu` | + +All matching is **case-insensitive** and accepts both `-` and `_` separators. + +--- + +## Part 9 — Available Service Keys + +| Service key | What it points to | +|---|---| +| `contentDelivery` | CDN for published content (used for entry/asset fetching) | +| `contentManagement` | CMA for creating/updating content | +| `graphqlDelivery` | GraphQL delivery API | +| `graphqlPreview` | GraphQL live preview | +| `preview` | REST live preview | +| `auth` | Authentication API | +| `application` | Web app URL | +| `images` | Image delivery | +| `assets` | Asset delivery | +| `automate` | Workflow automation | +| `launch` | Contentstack Launch | +| `developerHub` | Developer Hub API | +| `brandKit` | Brand Kit API | +| `genAI` | Generative AI / Knowledge Vault | +| `personalizeManagement` | Personalization management | +| `personalizeEdge` | Personalization edge | +| `composableStudio` | Composable Studio API | +| `assetManagement` | Asset management API (NA only) | + +--- + +## Part 10 — Error Handling + +```php +use Contentstack\Utils\Endpoint; + +// Empty region string +try { + Endpoint::getContentstackEndpoint(''); +} catch (\InvalidArgumentException $e) { + // "Empty region provided. Please put valid region." +} + +// Unknown region +try { + Endpoint::getContentstackEndpoint('asia-pacific', 'contentDelivery'); +} catch (\InvalidArgumentException $e) { + // "Invalid region: asia-pacific" +} + +// Unknown service key +try { + Endpoint::getContentstackEndpoint('na', 'cms'); +} catch (\InvalidArgumentException $e) { + // "Service "cms" not found for region "na"" +} + +// regions.json missing and no network access +try { + Endpoint::getContentstackEndpoint('na', 'contentDelivery'); +} catch (\RuntimeException $e) { + // "contentstack/utils: regions.json not found and could not be downloaded. + // Run "composer install" or "composer refresh-regions" and ensure network access." +} +``` + +--- + +## Part 11 — Files Introduced by This Feature + +``` +contentstack-utils-php/ +│ +├── src/ +│ ├── Endpoint.php ← core implementation (new) +│ ├── Utils.php ← getContentstackEndpoint() proxy added +│ └── assets/ +│ └── regions.json ← downloaded at install/runtime, NOT committed +│ +├── scripts/ +│ └── download-regions.php ← called by composer hooks (new) +│ +├── tests/ +│ └── EndpointTest.php ← 39 tests, 99 assertions (new) +│ +├── docs/ +│ ├── endpoint-integration-overview.md ← this file (new) +│ └── endpoint-resolution.md ← full API reference (new) +│ +├── composer.json ← post-install-cmd, post-update-cmd, refresh-regions added +└── .gitignore ← src/assets/regions.json added +``` diff --git a/docs/endpoint-resolution.md b/docs/endpoint-resolution.md new file mode 100644 index 0000000..53fe02f --- /dev/null +++ b/docs/endpoint-resolution.md @@ -0,0 +1,838 @@ +# Endpoint Resolution — Contentstack PHP Utils SDK + +## Overview + +The endpoint resolution feature provides a single function — `getContentstackEndpoint()` — that returns the correct Contentstack API URL for any **region** and any **service**, without hardcoding host strings in your application. + +It mirrors the JavaScript utils SDK implementation and is backed by the official Contentstack regions registry at `https://artifacts.contentstack.com/regions.json`. + +--- + +## Table of Contents + +1. [How It Works](#1-how-it-works) +2. [Setup — regions.json](#2-setup--regionsjson) +3. [API Reference](#3-api-reference) +4. [Supported Regions](#4-supported-regions) +5. [Available Service Keys](#5-available-service-keys) +6. [Complete URL Reference](#6-complete-url-reference) +7. [Usage Examples](#7-usage-examples) +8. [Integration with PHP Delivery SDK](#8-integration-with-php-delivery-sdk) +9. [Error Handling](#9-error-handling) +10. [Keeping regions.json Up to Date](#10-keeping-regionsjson-up-to-date) +11. [Architecture](#11-architecture) + +--- + +## 1. How It Works + +``` +Your Code + │ + └── Endpoint::getContentstackEndpoint('eu', 'contentDelivery') + │ + ├── 1. Check static cache (zero I/O after first call) + ├── 2. Read src/assets/regions.json from disk + ├── 3. Download from artifacts.contentstack.com (fallback) + │ + ├── Normalize region → lowercase + trim + ├── Match by canonical ID → match by alias + ├── Look up service key in endpoints map + └── Return "https://eu-cdn.contentstack.com" +``` + +**Key design principles:** + +- **No hardcoded URLs** — all hosts come from `regions.json` +- **No runtime HTTP calls** in normal operation — file is read from disk +- **Zero production dependencies** — uses only PHP built-ins +- **Single static cache** — `regions.json` is parsed once per PHP process +- **Backward compatible** — available on both `Endpoint` and `Utils` classes + +--- + +## 2. Setup — regions.json + +`regions.json` is **not committed** to the repository. It is downloaded automatically and lives only on disk at `src/assets/regions.json`. + +### Automatic download on `composer install` + +```bash +composer install +# Output: +# > @php scripts/download-regions.php +# contentstack/utils: regions.json downloaded (7 regions). +``` + +This also fires on `composer update`. + +### When the package is used as a dependency + +When another project runs `composer require contentstack/utils`, Composer does not run the library's `post-install-cmd` scripts. In that case, `Endpoint::loadRegions()` detects the missing file and **downloads it automatically on the first call** — no manual step needed. + +### Manual refresh + +```bash +composer refresh-regions +# contentstack/utils: regions.json downloaded (7 regions). +``` + +Run this whenever Contentstack announces new regions or services. + +### Resolution priority + +``` +1. Static cache → fastest, zero I/O, lives for the PHP process lifetime +2. src/assets/regions.json on disk → written by composer install script +3. Live download → fallback when file is absent (e.g. fresh dependency install) +``` + +--- + +## 3. API Reference + +### `Endpoint::getContentstackEndpoint()` + +```php +namespace Contentstack\Utils; + +public static function getContentstackEndpoint( + string $region = 'us', // Region ID or alias + string $service = '', // Service key. Empty = return all endpoints + bool $omitHttps = false // true = strip https:// from result +): string|array +``` + +Also available as a proxy on `Utils`: + +```php +use Contentstack\Utils\Utils; + +Utils::getContentstackEndpoint(string $region, string $service, bool $omitHttps): string|array +``` + +Both calls produce identical results. + +### Parameters + +| Parameter | Type | Default | Required | Description | +|---|---|---|---|---| +| `$region` | `string` | `'us'` | No | Region ID (`na`, `eu`, `au`, `azure-na`, `azure-eu`, `gcp-na`, `gcp-eu`) or any accepted alias. Case-insensitive. | +| `$service` | `string` | `''` | No | Service key (e.g. `contentDelivery`, `contentManagement`). When empty, the full endpoint map for the region is returned as an associative array. | +| `$omitHttps` | `bool` | `false` | No | When `true`, strips `https://` from every returned URL. Useful when passing the host to `Stack::setHost()`. | + +### Return values + +| `$service` | `$omitHttps` | Return type | Example | +|---|---|---|---| +| `'contentDelivery'` | `false` | `string` | `"https://eu-cdn.contentstack.com"` | +| `'contentDelivery'` | `true` | `string` | `"eu-cdn.contentstack.com"` | +| `''` (empty) | `false` | `array` | `['contentDelivery' => 'https://...', ...]` | +| `''` (empty) | `true` | `array` | `['contentDelivery' => 'eu-cdn...', ...]` | + +### Exceptions + +| Exception | Thrown when | +|---|---| +| `\InvalidArgumentException` | `$region` is an empty string | +| `\InvalidArgumentException` | `$region` does not match any known ID or alias | +| `\InvalidArgumentException` | `$service` is not found in the region's endpoint map | +| `\RuntimeException` | `regions.json` is missing and cannot be downloaded | +| `\RuntimeException` | `regions.json` exists but contains invalid JSON | + +--- + +## 4. Supported Regions + +| Canonical ID | Cloud | Location | Default | Accepted Aliases | +|---|---|---|---|---| +| `na` | AWS | North America | ✓ | `us`, `aws-na`, `aws_na`, `NA`, `US`, `AWS-NA`, `AWS_NA` | +| `eu` | AWS | Europe | | `aws-eu`, `aws_eu`, `EU`, `AWS-EU`, `AWS_EU` | +| `au` | AWS | Australia | | `aws-au`, `aws_au`, `AU`, `AWS-AU`, `AWS_AU` | +| `azure-na` | Azure | North America | | `azure_na`, `AZURE-NA`, `AZURE_NA` | +| `azure-eu` | Azure | Europe | | `azure_eu`, `AZURE-EU`, `AZURE_EU` | +| `gcp-na` | GCP | North America | | `gcp_na`, `GCP-NA`, `GCP_NA` | +| `gcp-eu` | GCP | Europe | | `gcp_eu`, `GCP-EU`, `GCP_EU` | + +**Alias matching rules:** +- Case-insensitive — `EU`, `eu`, `Eu` all resolve to the same region +- Both `-` and `_` separators are accepted — `azure-na` and `azure_na` are equivalent +- Leading/trailing whitespace is stripped automatically + +--- + +## 5. Available Service Keys + +| Key | Description | +|---|---| +| `contentDelivery` | Content Delivery API (CDN) — for fetching published entries and assets | +| `contentManagement` | Content Management API — for creating, updating, and deleting content | +| `graphqlDelivery` | GraphQL Delivery API | +| `graphqlPreview` | GraphQL Live Preview API | +| `preview` | REST Live Preview API | +| `auth` | Authentication API | +| `application` | Contentstack web application | +| `images` | Image Delivery — optimised image serving | +| `assets` | Asset Delivery — non-image file storage | +| `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 (NA only) | + +> **Note:** Not all service keys are present in every region. If you request a service that does not exist for a given region, an `InvalidArgumentException` is thrown. + +--- + +## 6. Complete URL Reference + +### AWS North America (`na`) + +| Service | URL | +|---|---| +| `application` | `https://app.contentstack.com` | +| `contentDelivery` | `https://cdn.contentstack.io` | +| `contentManagement` | `https://api.contentstack.io` | +| `auth` | `https://auth-api.contentstack.com` | +| `graphqlDelivery` | `https://graphql.contentstack.com` | +| `preview` | `https://rest-preview.contentstack.com` | +| `graphqlPreview` | `https://graphql-preview.contentstack.com` | +| `images` | `https://images.contentstack.io` | +| `assets` | `https://assets.contentstack.io` | +| `automate` | `https://automations-api.contentstack.com` | +| `launch` | `https://launch-api.contentstack.com` | +| `developerHub` | `https://developerhub-api.contentstack.com` | +| `brandKit` | `https://brand-kits-api.contentstack.com` | +| `genAI` | `https://ai.contentstack.com/brand-kits` | +| `personalizeManagement` | `https://personalize-api.contentstack.com` | +| `personalizeEdge` | `https://personalize-edge.contentstack.com` | +| `composableStudio` | `https://composable-studio-api.contentstack.com` | +| `assetManagement` | `https://am-api.contentstack.com` | + +### AWS Europe (`eu`) + +| Service | URL | +|---|---| +| `application` | `https://eu-app.contentstack.com` | +| `contentDelivery` | `https://eu-cdn.contentstack.com` | +| `contentManagement` | `https://eu-api.contentstack.com` | +| `auth` | `https://eu-auth-api.contentstack.com` | +| `graphqlDelivery` | `https://eu-graphql.contentstack.com` | +| `preview` | `https://eu-rest-preview.contentstack.com` | +| `graphqlPreview` | `https://eu-graphql-preview.contentstack.com` | +| `images` | `https://eu-images.contentstack.com` | +| `assets` | `https://eu-assets.contentstack.com` | +| `automate` | `https://eu-prod-automations-api.contentstack.com` | +| `launch` | `https://eu-launch-api.contentstack.com` | +| `developerHub` | `https://eu-developerhub-api.contentstack.com` | +| `brandKit` | `https://eu-brand-kits-api.contentstack.com` | +| `genAI` | `https://eu-ai.contentstack.com/brand-kits` | +| `personalizeManagement` | `https://eu-personalize-api.contentstack.com` | +| `personalizeEdge` | `https://eu-personalize-edge.contentstack.com` | +| `composableStudio` | `https://eu-composable-studio-api.contentstack.com` | + +### AWS Australia (`au`) + +| Service | URL | +|---|---| +| `contentDelivery` | `https://au-cdn.contentstack.com` | +| `contentManagement` | `https://au-api.contentstack.com` | +| `auth` | `https://au-auth-api.contentstack.com` | +| `graphqlDelivery` | `https://au-graphql.contentstack.com` | +| `preview` | `https://au-rest-preview.contentstack.com` | +| `graphqlPreview` | `https://au-graphql-preview.contentstack.com` | +| `images` | `https://au-images.contentstack.com` | +| `assets` | `https://au-assets.contentstack.com` | + +### Azure North America (`azure-na`) + +| Service | URL | +|---|---| +| `contentDelivery` | `https://azure-na-cdn.contentstack.com` | +| `contentManagement` | `https://azure-na-api.contentstack.com` | +| `auth` | `https://azure-na-auth-api.contentstack.com` | +| `graphqlDelivery` | `https://azure-na-graphql.contentstack.com` | +| `preview` | `https://azure-na-rest-preview.contentstack.com` | +| `graphqlPreview` | `https://azure-na-graphql-preview.contentstack.com` | +| `images` | `https://azure-na-images.contentstack.com` | +| `assets` | `https://azure-na-assets.contentstack.com` | + +### Azure Europe (`azure-eu`) + +| Service | URL | +|---|---| +| `contentDelivery` | `https://azure-eu-cdn.contentstack.com` | +| `contentManagement` | `https://azure-eu-api.contentstack.com` | +| `auth` | `https://azure-eu-auth-api.contentstack.com` | +| `graphqlDelivery` | `https://azure-eu-graphql.contentstack.com` | + +### GCP North America (`gcp-na`) + +| Service | URL | +|---|---| +| `contentDelivery` | `https://gcp-na-cdn.contentstack.com` | +| `contentManagement` | `https://gcp-na-api.contentstack.com` | +| `auth` | `https://gcp-na-auth-api.contentstack.com` | +| `graphqlDelivery` | `https://gcp-na-graphql.contentstack.com` | + +### GCP Europe (`gcp-eu`) + +| Service | URL | +|---|---| +| `contentDelivery` | `https://gcp-eu-cdn.contentstack.com` | +| `contentManagement` | `https://gcp-eu-api.contentstack.com` | +| `auth` | `https://gcp-eu-auth-api.contentstack.com` | +| `graphqlDelivery` | `https://gcp-eu-graphql.contentstack.com` | + +--- + +## 7. Usage Examples + +### Basic — get a single URL + +```php +use Contentstack\Utils\Endpoint; + +// Full URL with https:// +$url = Endpoint::getContentstackEndpoint('na', 'contentDelivery'); +// → "https://cdn.contentstack.io" + +$url = Endpoint::getContentstackEndpoint('eu', 'contentManagement'); +// → "https://eu-api.contentstack.com" + +$url = Endpoint::getContentstackEndpoint('au', 'graphqlDelivery'); +// → "https://au-graphql.contentstack.com" + +$url = Endpoint::getContentstackEndpoint('azure-na', 'auth'); +// → "https://azure-na-auth-api.contentstack.com" + +$url = Endpoint::getContentstackEndpoint('gcp-eu', 'preview'); +// → "https://gcp-eu-rest-preview.contentstack.com" +``` + +### Using region aliases + +All aliases resolve to the same canonical region — use whichever form suits your config: + +```php +// All four return "https://cdn.contentstack.io" +Endpoint::getContentstackEndpoint('na', 'contentDelivery'); +Endpoint::getContentstackEndpoint('us', 'contentDelivery'); +Endpoint::getContentstackEndpoint('aws-na', 'contentDelivery'); +Endpoint::getContentstackEndpoint('AWS_NA', 'contentDelivery'); + +// All three return "https://eu-cdn.contentstack.com" +Endpoint::getContentstackEndpoint('eu', 'contentDelivery'); +Endpoint::getContentstackEndpoint('EU', 'contentDelivery'); +Endpoint::getContentstackEndpoint('aws_eu', 'contentDelivery'); + +// Both return "https://azure-na-cdn.contentstack.com" +Endpoint::getContentstackEndpoint('azure-na', 'contentDelivery'); +Endpoint::getContentstackEndpoint('AZURE_NA', 'contentDelivery'); +``` + +### Strip `https://` for use as a hostname + +Pass `true` as the third argument when you need just the host: + +```php +$host = Endpoint::getContentstackEndpoint('eu', 'contentDelivery', true); +// → "eu-cdn.contentstack.com" + +$host = Endpoint::getContentstackEndpoint('gcp-na', 'contentManagement', true); +// → "gcp-na-api.contentstack.com" + +$host = Endpoint::getContentstackEndpoint('azure-eu', 'auth', true); +// → "azure-eu-auth-api.contentstack.com" +``` + +### Get all endpoints for a region + +Omit `$service` to receive the complete associative array: + +```php +$endpoints = Endpoint::getContentstackEndpoint('eu'); +// → [ +// 'application' => 'https://eu-app.contentstack.com', +// 'contentDelivery' => 'https://eu-cdn.contentstack.com', +// 'contentManagement' => 'https://eu-api.contentstack.com', +// 'auth' => 'https://eu-auth-api.contentstack.com', +// 'graphqlDelivery' => 'https://eu-graphql.contentstack.com', +// 'preview' => 'https://eu-rest-preview.contentstack.com', +// 'graphqlPreview' => 'https://eu-graphql-preview.contentstack.com', +// 'images' => 'https://eu-images.contentstack.com', +// 'assets' => 'https://eu-assets.contentstack.com', +// 'automate' => 'https://eu-prod-automations-api.contentstack.com', +// 'launch' => 'https://eu-launch-api.contentstack.com', +// 'developerHub' => 'https://eu-developerhub-api.contentstack.com', +// 'brandKit' => 'https://eu-brand-kits-api.contentstack.com', +// 'genAI' => 'https://eu-ai.contentstack.com/brand-kits', +// 'personalizeManagement'=> 'https://eu-personalize-api.contentstack.com', +// 'personalizeEdge' => 'https://eu-personalize-edge.contentstack.com', +// 'composableStudio' => 'https://eu-composable-studio-api.contentstack.com', +// ] + +// With omitHttps — all schemes stripped +$hosts = Endpoint::getContentstackEndpoint('eu', '', true); +// → [ +// 'contentDelivery' => 'eu-cdn.contentstack.com', +// 'contentManagement' => 'eu-api.contentstack.com', +// ... +// ] + +echo $endpoints['contentDelivery']; // https://eu-cdn.contentstack.com +echo $hosts['contentManagement']; // eu-api.contentstack.com +``` + +### Reading region from an environment variable + +```php +use Contentstack\Utils\Endpoint; + +// Set in your .env or server config: CONTENTSTACK_REGION=eu +$region = getenv('CONTENTSTACK_REGION') ?: 'na'; + +$cdnUrl = Endpoint::getContentstackEndpoint($region, 'contentDelivery'); +$apiUrl = Endpoint::getContentstackEndpoint($region, 'contentManagement'); + +echo $cdnUrl; // https://eu-cdn.contentstack.com +echo $apiUrl; // https://eu-api.contentstack.com +``` + +### Accessing specific services from the full map + +```php +$endpoints = Endpoint::getContentstackEndpoint('azure-na'); + +$cdnUrl = $endpoints['contentDelivery']; +$apiUrl = $endpoints['contentManagement']; +$graphqlUrl = $endpoints['graphqlDelivery']; +$previewUrl = $endpoints['preview']; +$authUrl = $endpoints['auth']; + +// Use in your app config +$config = [ + 'cdn' => $cdnUrl, + 'api' => $apiUrl, + 'graphql' => $graphqlUrl, +]; +``` + +### Via `Utils` — backward-compatible shorthand + +```php +use Contentstack\Utils\Utils; + +// Identical to Endpoint::getContentstackEndpoint() +$url = Utils::getContentstackEndpoint('eu', 'contentDelivery'); +// → "https://eu-cdn.contentstack.com" + +$host = Utils::getContentstackEndpoint('gcp-na', 'contentManagement', true); +// → "gcp-na-api.contentstack.com" + +$all = Utils::getContentstackEndpoint('au'); +// → associative array of all AU endpoints +``` + +--- + +## 8. Integration with PHP Delivery SDK + +The `contentDelivery` host resolved from `getContentstackEndpoint()` maps directly to the host the PHP delivery SDK uses for entry and asset fetching. + +### Basic setup + +```php +use Contentstack\Contentstack; +use Contentstack\Utils\Endpoint; + +$region = 'eu'; // switch this one value to target any region + +// 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( + '', + '', + '' +); + +// Wire the resolved host into the stack +$stack->setHost($host); + +// All subsequent requests go to the EU CDN +$result = $stack + ->ContentType('') + ->Query() + ->toJSON() + ->find(); + +foreach ($result[0] as $entry) { + echo $entry['title'] . "\n"; +} +``` + +### Reading region from environment variable (recommended) + +```php +use Contentstack\Contentstack; +use Contentstack\Utils\Endpoint; + +$region = getenv('CONTENTSTACK_REGION') ?: 'na'; + +$stack = Contentstack::Stack( + getenv('CONTENTSTACK_API_KEY'), + getenv('CONTENTSTACK_DELIVERY_TOKEN'), + getenv('CONTENTSTACK_ENVIRONMENT') +); +$stack->setHost(Endpoint::getContentstackEndpoint($region, 'contentDelivery', true)); +``` + +`.env` file: +```dotenv +CONTENTSTACK_REGION=eu +CONTENTSTACK_API_KEY=blt... +CONTENTSTACK_DELIVERY_TOKEN=cs... +CONTENTSTACK_ENVIRONMENT=production +``` + +### Fetching a single entry + +```php +use Contentstack\Contentstack; +use Contentstack\Utils\Endpoint; + +$region = 'azure-na'; +$host = Endpoint::getContentstackEndpoint($region, 'contentDelivery', true); + +$stack = Contentstack::Stack('', '', ''); +$stack->setHost($host); + +$entry = $stack + ->ContentType('') + ->Entry('') + ->toJSON() + ->fetch(); + +echo $entry['title']; +``` + +### Querying entries with filters + +```php +use Contentstack\Contentstack; +use Contentstack\Utils\Endpoint; + +$host = Endpoint::getContentstackEndpoint('gcp-eu', 'contentDelivery', true); +$stack = Contentstack::Stack('', '', ''); +$stack->setHost($host); + +$result = $stack + ->ContentType('blog_post') + ->Query() + ->where('category', 'technology') + ->limit(10) + ->toJSON() + ->find(); + +$entries = $result[0]; +echo "Found: " . count($entries) . " entries\n"; +``` + +### Fetching assets + +```php +use Contentstack\Contentstack; +use Contentstack\Utils\Endpoint; + +$host = Endpoint::getContentstackEndpoint('eu', 'contentDelivery', true); +$stack = Contentstack::Stack('', '', ''); +$stack->setHost($host); + +$asset = $stack->Assets('')->toJSON()->fetch(); +echo $asset['url']; +``` + +### Querying with embedded items (JSON RTE) + +```php +use Contentstack\Contentstack; +use Contentstack\Utils\Endpoint; +use Contentstack\Utils\Model\Option; + +$host = Endpoint::getContentstackEndpoint('na', 'contentDelivery', true); +$stack = Contentstack::Stack('', '', ''); +$stack->setHost($host); + +$result = $stack + ->ContentType('') + ->Query() + ->toJSON() + ->includeEmbeddedItems() + ->find(); + +foreach ($result[0] as $entry) { + $html = Contentstack::jsonToHtml($entry['json_rte_field'], new Option($entry)); + echo $html; +} +``` + +### GraphQL with endpoint resolution + +```php +use Contentstack\Utils\Endpoint; + +$graphqlUrl = Endpoint::getContentstackEndpoint('eu', 'graphqlDelivery'); +// → "https://eu-graphql.contentstack.com" + +// Use this URL as the base for your GraphQL client +$client = new GraphQLClient($graphqlUrl, [ + 'headers' => [ + 'access_token' => '', + 'api_key' => '', + ] +]); +``` + +### Switching regions dynamically + +Change one variable to redirect all API traffic to a different region: + +```php +use Contentstack\Contentstack; +use Contentstack\Utils\Endpoint; + +function createStack(string $region): \Contentstack\Stack\Stack +{ + $host = Endpoint::getContentstackEndpoint($region, 'contentDelivery', true); + $stack = Contentstack::Stack( + getenv('CONTENTSTACK_API_KEY'), + getenv('CONTENTSTACK_DELIVERY_TOKEN'), + getenv('CONTENTSTACK_ENVIRONMENT') + ); + $stack->setHost($host); + return $stack; +} + +// Route traffic by region +$regions = ['na', 'eu', 'au', 'azure-na', 'azure-eu', 'gcp-na', 'gcp-eu']; + +foreach ($regions as $region) { + $stack = createStack($region); + $result = $stack->ContentType('page')->Query()->toJSON()->find(); + $count = count($result[0]); + $host = Endpoint::getContentstackEndpoint($region, 'contentDelivery'); + echo sprintf("%-10s %-45s %d entries\n", $region, $host, $count); +} +``` + +--- + +## 9. Error Handling + +All exceptions thrown are either `\InvalidArgumentException` (bad input) or `\RuntimeException` (infrastructure/file problem). + +### Empty region + +```php +use Contentstack\Utils\Endpoint; + +try { + Endpoint::getContentstackEndpoint(''); +} catch (\InvalidArgumentException $e) { + echo $e->getMessage(); + // "Empty region provided. Please put valid region." +} +``` + +### Unknown region + +```php +try { + Endpoint::getContentstackEndpoint('asia-pacific', 'contentDelivery'); +} catch (\InvalidArgumentException $e) { + echo $e->getMessage(); + // "Invalid region: asia-pacific" +} +``` + +### Unknown service key + +```php +try { + Endpoint::getContentstackEndpoint('na', 'cms'); +} catch (\InvalidArgumentException $e) { + echo $e->getMessage(); + // 'Service "cms" not found for region "na"' +} +``` + +### regions.json missing and no network + +```php +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." +} +``` + +### Corrupt regions.json + +```php +try { + Endpoint::getContentstackEndpoint('na', 'contentDelivery'); +} catch (\RuntimeException $e) { + echo $e->getMessage(); + // "contentstack/utils: regions.json is corrupt. + // Run "composer refresh-regions" to re-download it." +} +``` + +### Defensive pattern for production code + +```php +use Contentstack\Utils\Endpoint; + +function resolveHost(string $region, string $service): string +{ + try { + return Endpoint::getContentstackEndpoint($region, $service, true); + } catch (\InvalidArgumentException $e) { + // Bad config — log and fall back to default NA host + error_log('Endpoint config error: ' . $e->getMessage()); + return 'cdn.contentstack.io'; + } catch (\RuntimeException $e) { + // Infrastructure problem — log and fall back + error_log('Endpoint load error: ' . $e->getMessage()); + return 'cdn.contentstack.io'; + } +} +``` + +--- + +## 10. Keeping regions.json Up to Date + +Contentstack occasionally adds new regions or new service keys. The bundled `regions.json` needs to be refreshed when this happens. + +### Refresh manually + +```bash +composer refresh-regions +``` + +### Automate in CI/CD + +Add a refresh step before your deploy so the latest regions are always used: + +```yaml +# GitHub Actions example +- name: Install PHP dependencies + run: composer install --no-dev --optimize-autoloader +# regions.json is downloaded automatically by post-install-cmd + +# Or refresh explicitly if the file was cached between CI runs +- name: Refresh Contentstack regions + run: composer refresh-regions +``` + +### How the download script works + +`scripts/download-regions.php` is the script wired to the composer hooks: + +1. Tries **PHP curl extension** first — follows redirects, verifies SSL +2. Falls back to **`file_get_contents`** with a stream context +3. Validates the downloaded JSON has a `regions` array before writing +4. Writes to `src/assets/regions.json` +5. Prints the region count on success; warns (non-fatal) on failure + +The exit code is always `0` — a download failure is a warning, not a fatal error, because the runtime fallback in `Endpoint::loadRegions()` will attempt the download again on the first API call. + +--- + +## 11. Architecture + +### File structure + +``` +contentstack-utils-php/ +├── src/ +│ ├── Endpoint.php ← core implementation +│ ├── Utils.php ← proxy method for backward compat +│ └── assets/ +│ └── regions.json ← downloaded at install/runtime, NOT committed +├── scripts/ +│ └── download-regions.php ← called by composer post-install-cmd +├── tests/ +│ └── EndpointTest.php ← 39 tests, 99 assertions +└── composer.json ← post-install-cmd, post-update-cmd, refresh-regions +``` + +### `Endpoint.php` internal flow + +``` +getContentstackEndpoint($region, $service, $omitHttps) +│ +├── Guard: $region === '' → throw InvalidArgumentException +│ +├── loadRegions() +│ ├── $regionsData cached? → return cache (zero I/O) +│ ├── file_exists(regions.json)? → read + decode + cache +│ └── else → downloadAndSave() → read + decode + cache +│ +├── strtolower(trim($region)) → $normalized +│ +├── findRegionByIdOrAlias($regions, $normalized) +│ ├── Pass 1: match $row['id'] === $normalized +│ └── Pass 2: match strtolower($alias) === $normalized for each alias +│ └── null → throw InvalidArgumentException('Invalid region') +│ +├── $service provided? +│ ├── YES → $regionRow['endpoints'][$service] +│ │ └── missing? → throw InvalidArgumentException('Service not found') +│ │ └── omitHttps? → stripHttps($url) : $url +│ └── NO → $regionRow['endpoints'] +│ └── omitHttps? → stripHttpsFromMap($endpoints) : $endpoints +│ +└── return string|array +``` + +### Static cache lifetime + +`Endpoint::$regionsData` is a `static` class property. In PHP: +- It is initialised to `null` +- Set on the first `loadRegions()` call +- Persists for the entire PHP process lifetime (e.g. the full HTTP request in FPM, or the full CLI run) +- Reset explicitly via `Endpoint::resetCache()` (test use only) + +This means `regions.json` is read from disk **once per process**, regardless of how many times `getContentstackEndpoint()` is called. + +### Relationship between `Endpoint` and `Utils` + +``` +Utils::getContentstackEndpoint() ← thin proxy, no logic + │ + └── Endpoint::getContentstackEndpoint() ← all logic lives here +``` + +`Utils` delegates entirely to `Endpoint`. Both classes are in the `Contentstack\Utils` namespace so no `use` import is needed between them. + +### Composer hooks summary + +| Hook | When it fires | What it does | +|---|---|---| +| `post-install-cmd` | After `composer install` on the **root** package | Runs `scripts/download-regions.php` | +| `post-update-cmd` | After `composer update` on the **root** package | Runs `scripts/download-regions.php` | +| `refresh-regions` | `composer refresh-regions` (manual) | Runs `scripts/download-regions.php` | +| Runtime fallback | First `getContentstackEndpoint()` call when file is missing | `Endpoint::downloadAndSave()` downloads the file silently | + +> **Important:** `post-install-cmd` and `post-update-cmd` only fire when this package **is the root** (i.e. being developed directly). When another project runs `composer require contentstack/utils`, those hooks are skipped — the runtime fallback handles the download transparently. From 97fc1c988c3a3de8d6faa6ef79a831028bec066c Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:57:14 +0530 Subject: [PATCH 8/9] Updated release date --- CHANGELOG.md | 2 +- composer.json | 2 +- composer.lock | 332 ++++++++++++++++++++++++++++++-------------------- 3 files changed, 199 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af262b7..4d7347c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## [1.3.0](https://github.com/contentstack/contentstack-utils-php/tree/v1.3.0) (2026-06-03) +## [1.3.0](https://github.com/contentstack/contentstack-utils-php/tree/v1.3.0) (2026-06-15) - 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 diff --git a/composer.json b/composer.json index d95db1a..d120859 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.3-dev" } }, "config": { diff --git a/composer.lock b/composer.lock index d1c8386..f52cb20 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b2e005b86c4b714258652f3e6f71fdd1", + "content-hash": "598e67e9ae54cde01ce0e71e4262fe0c", "packages": [ { "name": "marc-mabe/php-enum", - "version": "v4.7.0", + "version": "v4.7.2", "source": { "type": "git", "url": "https://github.com/marc-mabe/php-enum.git", - "reference": "3da42cc1daceaf98c858e56f59d1ccd52b011fdc" + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/3da42cc1daceaf98c858e56f59d1ccd52b011fdc", - "reference": "3da42cc1daceaf98c858e56f59d1ccd52b011fdc", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", "shasum": "" }, "require": { @@ -28,13 +28,13 @@ "phpbench/phpbench": "^0.16.10 || ^1.0.4", "phpstan/phpstan": "^1.3.1", "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", - "vimeo/psalm": "^4.17.0" + "vimeo/psalm": "^4.17.0 | ^5.26.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.6-dev", - "dev-3.x": "3.2-dev" + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" } }, "autoload": { @@ -75,9 +75,9 @@ ], "support": { "issues": "https://github.com/marc-mabe/php-enum/issues", - "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.0" + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" }, - "time": "2022-04-19T02:21:46+00:00" + "time": "2025-09-14T11:18:39+00:00" } ], "packages-dev": [ @@ -153,16 +153,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -170,11 +170,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -200,7 +201,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -208,20 +209,20 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/2218c2252c874a4624ab2f613d86ac32d227bc69", - "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -232,7 +233,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -240,7 +241,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -264,26 +265,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-02-21T19:24:10+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -324,9 +326,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -381,35 +389,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -418,7 +426,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -447,7 +455,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -455,7 +463,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -700,45 +708,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.17", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1a156980d78a6666721b7e8e8502fe210b587fcd", - "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -783,7 +791,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -794,25 +802,33 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-02-23T13:14:51+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -847,7 +863,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -855,7 +871,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -970,16 +986,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -1032,15 +1048,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -1101,16 +1129,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -1155,7 +1183,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -1163,7 +1191,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -1230,16 +1258,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -1295,28 +1323,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -1359,15 +1399,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -1540,16 +1592,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -1591,28 +1643,40 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -1624,7 +1688,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1645,8 +1709,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -1654,7 +1717,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -1767,16 +1830,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.0", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -1793,11 +1856,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -1841,22 +1899,26 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-02-16T15:06:51+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -1885,7 +1947,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -1893,17 +1955,17 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=7.2" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } From 54d4e53625b09ca446c29e7aaa68830a7ea8f7ff Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:30:46 +0530 Subject: [PATCH 9/9] updated ci workflow file --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0312297..24ef05a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}