From 9c2834f0a4961aa09d1caa827d7e8845010c6df6 Mon Sep 17 00:00:00 2001 From: yasodhadollu Date: Wed, 1 Apr 2026 13:04:34 +0530 Subject: [PATCH 1/3] add examples for PHP frameworks --- .github/workflows/publish.yml | 116 +++++++++++ .github/workflows/release.yml | 37 ---- README.md | 360 +++++++++++++++++++++++++++++++++- composer.json | 2 +- 4 files changed, 476 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ef5705b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,116 @@ +# Manual publish: validate, test, then create a GitHub Release and tag. +# In GitHub: Actions → Publish Supaship PHP SDK → Run workflow → enter version (e.g. 1.0.0). +# Packagist still updates from the new tag if the repo webhook is configured (see DEPLOY.md). + +name: Publish Supaship PHP SDK + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g. 1.2.0 or v1.2.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository (full history, all tags) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: json, openssl + coverage: none + + - name: Validate composer.json + run: composer validate --no-check-publish --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run tests + run: composer test + + - name: Resolve version tag + id: version + env: + RAW_VERSION: ${{ github.event.inputs.version }} + run: | + set -euo pipefail + STRIPPED="${RAW_VERSION#v}" + if ! printf '%s' "$STRIPPED" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$'; then + echo "::error::Version must be semver-like, e.g. 1.0.0 or v1.0.0 (optional pre-release: 1.0.0-beta.1)" + exit 1 + fi + TAG="v${STRIPPED}" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "stripped=${STRIPPED}" >> "$GITHUB_OUTPUT" + + - name: Build release notes from commits since last GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + REPO="${{ github.repository }}" + NOTES_FILE="${GITHUB_WORKSPACE}/release-notes.md" + + PREV_TAG=$(gh api "repos/${REPO}/releases/latest" --jq '.tag_name // empty' 2>/dev/null || true) + + if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "null" ]; then + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true) + fi + + HEADER="## Changes since previous release" + if [ -z "$PREV_TAG" ]; then + HEADER="## Changes (no prior GitHub release or git tag found)" + LOG=$(git log --pretty=format:'- %s (%h)' --no-merges -n 200) + elif git rev-parse "${PREV_TAG}^{commit}" >/dev/null 2>&1; then + LOG=$(git log "${PREV_TAG}..HEAD" --pretty=format:'- %s (%h)' --no-merges) + else + HEADER="## Changes (previous tag \`${PREV_TAG}\` not found locally — showing recent history)" + LOG=$(git log --pretty=format:'- %s (%h)' --no-merges -n 200) + fi + + if [ -z "$LOG" ]; then + LOG="_No new commits in this range._" + fi + + { + echo "$HEADER" + echo "" + echo "$LOG" + echo "" + } > "$NOTES_FILE" + + - name: Create GitHub Release (and tag) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="${{ steps.version.outputs.tag }}" + NOTES_FILE="${GITHUB_WORKSPACE}/release-notes.md" + + if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "::error::Release or tag ${TAG} already exists. Bump the version." + exit 1 + fi + + gh release create "$TAG" \ + --repo "${{ github.repository }}" \ + --target "${{ github.sha }}" \ + --title "$TAG" \ + --notes-file "$NOTES_FILE" + + echo "Created ${TAG}. If Packagist is linked, it should pick up this tag shortly." + echo "See DEPLOY.md for troubleshooting." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 4b10727..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Runs full checks when you push a version tag (e.g. v1.0.0). -# Publishing to Packagist still requires a one-time setup (see DEPLOY.md): Packagist pulls from GitHub when tags appear. - -name: Release Supaship PHP SDK - -on: - push: - tags: - - 'v*' - -jobs: - verify: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - extensions: json, openssl - coverage: none - - - name: Validate composer.json - run: composer validate --no-check-publish --strict - - - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-interaction - - - name: Run tests - run: composer test - - - name: Package name reminder - run: | - echo "If Packagist is linked to this repo, it will pick up tag ${GITHUB_REF_NAME} automatically (may take a minute)." - echo "See DEPLOY.md if the version does not appear on https://packagist.org/packages/supashiphq/php-sdk" diff --git a/README.md b/README.md index 5a5a7ef..2d4fa49 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,364 @@ Optional per-request context: $client->getFeature('new-ui', ['context' => ['plan' => 'enterprise']]); ``` +## Framework integrations + +`SupaClient` has no framework-specific code paths: register it once in the container (or a bootstrap file), inject it where you need flags, and call `getFeature` / `getFeatures`. All three examples assume `composer require supashiphq/php-sdk` is already done. + +Evaluations are **synchronous** (each call waits for the HTTP response unless you wrap them yourself, e.g. queue or async jobs). + +--- + +### Laravel + +**1. Environment** + +In `.env`: + +```env +SUPASHIP_SDK_KEY=your-sdk-key +SUPASHIP_ENVIRONMENT=production +``` + +**2. Config file** (e.g. `config/supaship.php`) + +```php + env('SUPASHIP_SDK_KEY'), + 'environment' => env('SUPASHIP_ENVIRONMENT', 'production'), + /** + * Central list of flags and fallbacks — keep in sync with what you use in Supaship. + */ + 'features' => [ + 'new-ui' => false, + 'theme-config' => [ + 'primaryColor' => '#007bff', + 'darkMode' => false, + ], + ], +]; +``` + +**3. Register the client** in `app/Providers/AppServiceProvider.php` (method `register()`): + +```php +use Illuminate\Support\ServiceProvider; +use Supaship\SupaClient; + +class AppServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->singleton(SupaClient::class, function ($app) { + $config = $app['config']->get('supaship'); + + return new SupaClient([ + 'sdkKey' => $config['sdk_key'], + 'environment' => $config['environment'], + 'features' => $config['features'], + 'context' => [ + // Filled at boot or request time — see below + 'appEnv' => config('app.env'), + ], + ]); + }); + } +} +``` + +**4. Per-request context** (e.g. after auth) + +In `AppServiceProvider::boot()` or a middleware: + +```php +use Illuminate\Support\Facades\Auth; +use Supaship\SupaClient; + +public function boot(): void +{ + $this->app->afterResolving(SupaClient::class, function (SupaClient $client) { + $user = Auth::user(); + if ($user) { + $client->updateContext([ + 'userId' => (string) $user->id, + 'email' => $user->email ?? '', + ]); + } + }); +} +``` + +**5. Use in a controller** + +```php +use Supaship\SupaClient; + +class DashboardController extends Controller +{ + public function __construct(private readonly SupaClient $features) {} + + public function index() + { + $showNewUi = $this->features->getFeature('new-ui'); + $theme = $this->features->getFeature('theme-config'); + + return view('dashboard', compact('showNewUi', 'theme')); + } +} +``` + +Optional **middleware** that only refreshes context is often cleaner than `afterResolving` when you need `Auth::user()` on every request. + +--- + +### Symfony + +**1. Environment** + +In `.env.local` (do not commit secrets): + +```env +SUPASHIP_SDK_KEY=your-sdk-key +SUPASHIP_ENVIRONMENT=production +``` + +**2. Parameters** in `config/services.yaml` (Symfony 6/7 style): + +```yaml +parameters: + supaship.sdk_key: '%env(SUPASHIP_SDK_KEY)%' + supaship.environment: '%env(SUPASHIP_ENVIRONMENT)%' + supaship.features: + new-ui: false + theme-config: + primaryColor: '#007bff' + darkMode: false +``` + +For larger `features` maps, you can load a dedicated file with `imports:` or define the array in PHP via a small config class; the important part is passing the same structure into `SupaClient`. + +**3. Service definition** in `config/services.yaml`: + +```yaml +services: + Supaship\SupaClient: + class: Supaship\SupaClient + arguments: + - { + sdkKey: '%supaship.sdk_key%', + environment: '%supaship.environment%', + features: '%supaship.features%', + context: { appEnv: '%kernel.environment%' } + } + public: true +``` + +**4. Subscriber** to attach the current user to the client (example using Symfony security): + +```php +isMainRequest()) { + return; + } + + $user = $this->security->getUser(); + if ($user === null) { + return; + } + + $this->client->updateContext([ + // Adjust to your User class / identifier field + 'userId' => method_exists($user, 'getUserIdentifier') + ? $user->getUserIdentifier() + : (string) spl_object_id($user), + ]); + } + + public static function getSubscribedEvents(): array + { + return [KernelEvents::REQUEST => ['onKernelRequest', 8]]; + } +} +``` + +Register the subscriber (Symfony auto-wires if `App\` is configured; otherwise add explicit service tags for `kernel.event_subscriber`). + +**5. Controller** + +```php +use Supaship\SupaClient; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class HomeController extends AbstractController +{ + #[Route('/', name: 'home')] + public function index(SupaClient $features): Response + { + $newUi = $features->getFeature('new-ui'); + + return $this->render('home.html.twig', ['new_ui' => $newUi]); + } +} +``` + +--- + +### CodeIgniter 4 + +**1. Environment** + +In `.env`: + +```env +supaship.sdkKey = "your-sdk-key" +supaship.environment = "production" +``` + +**2. Central feature map** + +Create `app/Config/SupashipFeatures.php` (or keep the array inside `Services` if you prefer): + +```php + */ + public static function fallbacks(): array + { + return [ + 'new-ui' => false, + 'theme-config' => [ + 'primaryColor' => '#007bff', + 'darkMode' => false, + ], + ]; + } +} +``` + +**3. Service registration** in `app/Config/Services.php`: + +```php + getenv('supaship.sdkKey') ?: '', + 'environment' => getenv('supaship.environment') ?: 'production', + 'features' => SupashipFeatures::fallbacks(), + 'context' => [ + 'ciEnv' => ENVIRONMENT, + ], + ]); + } +} +``` + +**4. Per-request context** — e.g. in a filter `app/Filters/SupashipContext.php`: + +```php +get('userId')) { + $client->updateContext([ + 'userId' => (string) $session->get('userId'), + ]); + } + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + } +} +``` + +Register the filter in `app/Config/Filters.php` for the routes or groups that need targeting. + +**5. Controller** + +```php + $features->getFeature('new-ui'), + 'theme' => $features->getFeature('theme-config'), + ]; + + return view('home', $data); + } +} +``` + +--- + +### Shared tips (all frameworks) + +- Keep **`features`** (fallback map) in one file so it stays aligned with names in the Supaship dashboard. +- Use **`sensitiveContextProperties`** in the client config when passing emails or IDs you want hashed before they leave your server (same as the JavaScript SDK). +- Prefer **`getFeatures(['a','b','c'])`** for a single HTTP round-trip instead of many **`getFeature`** calls when loading a page. + ## Constants `Supaship\Constants::DEFAULT_FEATURES_URL` and `DEFAULT_EVENTS_URL` match the JavaScript SDK defaults. @@ -113,7 +471,7 @@ composer install composer test ``` -Maintainers: see **[DEPLOY.md](DEPLOY.md)** for registering the package on Packagist, webhooks, and version tags. +Maintainers: To ship a version from GitHub, use **Actions → Publish Supaship PHP SDK** (`publish.yml`): it validates, runs tests, and opens a GitHub Release with auto-generated notes from commits since the last release. ## License diff --git a/composer.json b/composer.json index 7da3c49..45db33a 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "supashiphq/php-sdk", - "description": "Lightweight PHP SDK for Supaship feature flags (no runtime dependencies)", + "description": "PHP SDK for Supaship feature flags", "type": "library", "license": "MIT", "require": { From 7016cd223e6f3077b29f439f06b92888d1f430ef Mon Sep 17 00:00:00 2001 From: yasodhadollu Date: Wed, 1 Apr 2026 13:09:59 +0530 Subject: [PATCH 2/3] add unit test stubs and examples --- DEPLOY.md | 199 +++++++++++++++++++++++++++++++++++++++ README.md | 161 ++++++++++++++++++++++++++++++- src/Testing/HttpStub.php | 46 +++++++++ tests/HttpStubTest.php | 36 +++++++ 4 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 DEPLOY.md create mode 100644 src/Testing/HttpStub.php create mode 100644 tests/HttpStubTest.php diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..0d3c9c9 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,199 @@ +# Publishing the Supaship PHP SDK (beginner guide) + +This document explains **end to end** how a public PHP library on GitHub becomes installable with Composer (Packagist). No prior publishing experience is assumed. + +## What you are actually “publishing” + +PHP libraries are **not uploaded** to Packagist like some other ecosystems. Instead: + +1. Your code lives in a **Git** repository (for example on GitHub). +2. **Packagist** (packagist.org) registers that repository and watches it for **new Git tags**. +3. When you create a version tag (for example `v1.0.0`), Packagist creates a **release** of your package that `composer require` can install. + +So “publish” means: **push clean code to GitHub** and **create a semver Git tag**. Packagist does the rest once it is connected. + +## Glossary + +| Term | Meaning | +|------|--------| +| **Composer** | PHP’s dependency manager (`composer install`, `composer require`). | +| **Packagist** | The default registry Composer uses to find packages. | +| **Package name** | The `name` field in `composer.json` (here: `supashiphq/php-sdk`). It must be **unique** on Packagist. | +| **Semantic version** | Versions like `1.0.0`, `1.1.0`, `2.0.0`. Users depend on ranges like `^1.0`. | +| **Git tag** | A named pointer to a specific commit, e.g. `v1.0.0`, often used as a release marker. | + +## One-time prerequisites + +1. **Git** installed, and you can push to the GitHub remote for this repo. +2. **PHP** (8.1+) and **Composer** installed on your machine for local checks. +3. A **Packagist.org account** (free): sign up at https://packagist.org + +## One-time: connect the GitHub repository to Packagist + +These steps assume the library already has a valid `composer.json` in the **root** of the repo (this project does). + +### Step 1 — Confirm `composer.json` + +Open `composer.json` and check: + +- **`name`**: `vendor/package` format, lowercase, matches what you want on Packagist (`supashiphq/php-sdk`). +- **`type`**: `"library"` for a reusable package. +- **`license`**, **`require`** (PHP version and extensions), **`autoload`**: all set. + +Locally run: + +```bash +composer validate --strict +composer install +composer test +``` + +Fix any errors before submitting to Packagist. + +### Step 2 — Push the repository to GitHub + +If the repo is not on GitHub yet: + +1. Create a **new empty** repository on GitHub (no README/license needed if you already have them locally). +2. From your laptop, in the SDK folder: + +```bash +git remote add origin https://github.com/ORGANIZATION/php-sdk.git +git push -u origin main +``` + +Use your real org/user and repo URL. + +### Step 3 — Submit the package on Packagist + +1. Log in to https://packagist.org +2. Click **Submit** (top right). +3. Paste the **GitHub repository URL** (HTTPS), e.g. `https://github.com/ORGANIZATION/php-sdk` +4. Click **Check**, then **Submit**. + +Packagist will read `composer.json` from the default branch and register the package. + +### Step 4 — Enable automatic updates (GitHub webhook) + +Without a webhook, you must click **Update** on Packagist after every change. With a webhook, new tags and branch updates sync automatically. + +On Packagist, open your package → **Maintainers** / **Settings** (wording may vary) and follow **“GitHub Service Hook”** or **“Webhook”** instructions. Typically you: + +1. Copy a **webhook URL** and/or token from Packagist. +2. On GitHub: repo **Settings → Webhooks → Add webhook** and paste the URL Packagist gives you. + +After this, pushing tags or commits usually triggers Packagist to refresh within about a minute. + +## Every release: tagging a new version + +Semantic versioning (https://semver.org) in short: + +- **MAJOR** (x.0.0): breaking API changes. +- **MINOR** (1.x.0): new features, backwards compatible. +- **PATCH** (1.0.x): bug fixes, backwards compatible. + +### Step 1 — Prepare the repo + +1. Merge work to `main` (or your release branch). +2. Run tests and validation: + +```bash +composer validate --strict +composer install +composer test +``` + +3. **Commit** any pending changes with a clear message. + +### Step 2 — Choose the next version number + +Examples: + +- First public release: `1.0.0` +- Bugfix: bump patch (`1.0.0` → `1.0.1`) +- New backwards-compatible feature: bump minor (`1.0.1` → `1.1.0`) + +### Step 3 — Create an annotated Git tag + +Many PHP projects use a **`v` prefix** on tags (e.g. `v1.0.0`). The **tag name** is what Packagist turns into a version; it must match what Composer expects. + +```bash +git checkout main +git pull + +git tag -a v1.0.0 -m "Release 1.0.0" +git push origin v1.0.0 +``` + +- **`-a`**: annotated tag (recommended; includes message and metadata). +- **`git push origin v1.0.0`**: publishing the tag is what notifies Packagist (via webhook). + +### Step 4 — Confirm on Packagist + +Open `https://packagist.org/packages/supashiphq/php-sdk` (adjust if your vendor name differs). Within a short time you should see the new version listed. + +If it does not appear: + +- Check **GitHub Actions** on the repo (this project runs **Release** workflow on `v*` tags). +- On Packagist, use **Update** manually once. +- Verify the tag exists on GitHub (**Releases** or **Tags** tab). + +### Step 5 — Optional: GitHub Release notes + +A **Git tag** is enough for Packagist. A **GitHub Release** (with notes) is optional but nice for humans: + +1. GitHub repo → **Releases** → **Draft a new release** +2. Choose the tag `v1.0.0` +3. Add title and changelog, publish. + +This does not replace Packagist; it is documentation for users browsing GitHub. + +## How users install the published package + +After the package is on Packagist: + +```bash +composer require supashiphq/php-sdk +``` + +They can pin versions: + +```bash +composer require supashiphq/php-sdk:^1.0 +``` + +## GitHub Actions in this repo + +| Workflow | When it runs | Purpose | +|----------|----------------|--------| +| `ci.yml` | Push / PR to `main` or `master` | `composer validate`, install deps, **`composer test`** on PHP 8.1–8.3. | +| `publish.yml` | **Manually** (Actions → Run workflow) | Asks for a **version**, runs the same checks, then creates a **GitHub Release** and **tag** with release notes built from commits since the **previous GitHub release** (or recent history if this is the first release). | + +### Using `publish.yml` instead of tagging by hand + +1. Open the repository on GitHub → **Actions**. +2. Select **Publish Supaship PHP SDK**. +3. Click **Run workflow**. +4. Enter a version (`1.0.0` or `v1.0.0`). +5. After tests pass, the workflow creates the tag and release; Packagist can pick up the new tag like any other tag push. + +You can still tag locally if you prefer; **`publish.yml` is optional** but keeps validation and notes in one place. + +These workflows **do not** log in to Packagist for you. Public packages are normally updated by Packagist’s **GitHub integration** when tags appear on the default branch. + +## Troubleshooting + +| Problem | What to check | +|---------|----------------| +| Packagist says **“package not found”** after `composer require` | Typo in package name; Packagist not submitted yet; wait a few minutes after submit. | +| **New tag missing** on Packagist | Webhook missing or failed (GitHub **Settings → Webhooks**); click **Update** on Packagist; confirm `git push origin vX.Y.Z` succeeded. | +| **`composer validate` fails** | Invalid JSON, missing required fields, bad version constraints. | +| **Tests fail on CI** | Match PHP extensions (`json`, `openssl`); run `composer test` locally. | + +## Changing the public package name + +If you change `name` in `composer.json`, you effectively create a **new** Packagist package. Avoid unless you intend to deprecate the old name and coordinate with your team and docs. + +--- + +**Summary:** Push your repo to GitHub → register it once on Packagist → set up the webhook → for each release, merge to `main`, run tests, then either run the **`publish.yml`** workflow (recommended: validates, then creates the GitHub Release and tag) **or** tag manually with `git tag` / `git push origin vX.Y.Z`. Users install with `composer require supashiphq/php-sdk`. diff --git a/README.md b/README.md index 2d4fa49..402eed8 100644 --- a/README.md +++ b/README.md @@ -440,13 +440,168 @@ class Home extends Controller - Use **`sensitiveContextProperties`** in the client config when passing emails or IDs you want hashed before they leave your server (same as the JavaScript SDK). - Prefer **`getFeatures(['a','b','c'])`** for a single HTTP round-trip instead of many **`getFeature`** calls when loading a page. +## Testing + +Unit tests should **not** call Supaship Edge. Pass an **`httpHandler`** under `networkConfig` (same hook as in [Advanced: `httpHandler`](#advanced-httphandler)) so `SupaClient` never opens a socket. + +### `Supaship\Testing\HttpStub` + +The package includes a tiny helper so you do not hand-build JSON for every test: + +```php +use Supaship\SupaClient; +use Supaship\Testing\HttpStub; + +$client = new SupaClient([ + 'sdkKey' => 'test-key', + 'environment' => 'test', + 'features' => [ + 'new-ui' => false, + 'theme-config' => ['darkMode' => false], + ], + 'context' => ['userId' => '42'], + 'networkConfig' => [ + 'httpHandler' => HttpStub::success([ + 'new-ui' => true, + 'theme-config' => ['darkMode' => true, 'primaryColor' => '#111'], + ]), + ], +]); + +$this->assertTrue($client->getFeature('new-ui')); +``` + +Simulate API or transport failures (client falls back to your configured defaults): + +```php +'networkConfig' => [ + 'httpHandler' => HttpStub::failure(503, 'unavailable'), +], +``` + +### Laravel: testing a route that injects `SupaClient` + +Your app resolves `SupaClient` from the container (e.g. `AppServiceProvider` registers a **singleton**). In a feature test you **swap** that binding for a client wired with **`HttpStub`**, then call the route. Laravel will inject your test double instead of the real client. + +Assume this route (simplified): + +```php +Route::get('/', function (SupaClient $client) { + $isNewUi = $client->getFeature('cool-new-feature', ['context' => [ + 'userId' => '123', + ]]); + + return $isNewUi ? view('new-welcome') : view('welcome'); +}); +``` + +Use the **same `features` fallback map** shape as in production (at least the keys you request). Register a client whose `httpHandler` returns the flag value you want for that test: + +```php + 'test', + 'environment' => 'testing', + 'features' => [ + 'cool-new-feature' => false, + ], + 'context' => [], + 'networkConfig' => [ + 'httpHandler' => HttpStub::success([ + 'cool-new-feature' => $coolNewFeatureEnabled, + ]), + ], + ]); + } + + public function test_home_uses_welcome2_when_flag_is_true(): void + { + $this->app->instance(SupaClient::class, $this->clientWithFlag(true)); + + $this->get('/') + ->assertOk() + ->assertViewIs('new-welcome'); + } + + public function test_home_uses_welcome_when_flag_is_false(): void + { + $this->app->instance(SupaClient::class, $this->clientWithFlag(false)); + + $this->get('/') + ->assertOk() + ->assertViewIs('welcome'); + } +} +``` + +Why this works: + +- **`$this->app->instance(SupaClient::class, …)`** tells Laravel: “when anything needs `SupaClient`, use this instance.” It runs **before** `$this->get('/')`, so the closure receives your stubbed client. +- **`HttpStub::success([...])`** simulates Edge returning that variation, so **`getFeature`** never performs a real HTTP request. +- To test **fallback** behavior (e.g. Edge down), use **`HttpStub::failure()`** and assert `welcome` if your fallback for `cool-new-feature` is false. + +### Asserting what would be sent to Edge + +The handler receives the POST URL and the **request body string** (JSON). Capture it in a closure when you care about `environment`, `features`, or `context`: + +```php +$captured = null; + +$client = new SupaClient([ + 'sdkKey' => 'sk', + 'environment' => 'staging', + 'features' => ['promo' => false], + 'context' => ['region' => 'eu'], + 'networkConfig' => [ + 'httpHandler' => function (string $url, string $jsonBody) use (&$captured) { + $captured = json_decode($jsonBody, true, flags: JSON_THROW_ON_ERROR); + + return ['statusCode' => 200, 'body' => '{"features":{"promo":{"variation":true}}}']; + }, + ], +]); + +$client->getFeatures(['promo'], ['context' => ['plan' => 'pro']]); + +$this->assertSame('staging', $captured['environment']); +$this->assertSame(['promo'], $captured['features']); +$this->assertSame(['region' => 'eu', 'plan' => 'pro'], $captured['context']); +``` + +### PHPUnit in your app + +Add a dev dependency and point to your tests directory (typical `phpunit.xml.dist`): + +```bash +composer require --dev phpunit/phpunit +``` + +Then run: + +```bash +vendor/bin/phpunit +``` + +The SDK’s own test suite is `composer test` from a clone of this repository (`vendor/bin/phpunit` after `composer install`). + ## Constants `Supaship\Constants::DEFAULT_FEATURES_URL` and `DEFAULT_EVENTS_URL` match the JavaScript SDK defaults. ## Advanced: `httpHandler` -For custom HTTP stacks or tests, you can inject a handler (same idea as `fetchFn` in the JavaScript SDK): +For **production** custom HTTP (proxy, corporate CA, tracing), or ad-hoc test doubles, inject a handler (same idea as `fetchFn` in the JavaScript SDK). For most unit tests, prefer **`HttpStub`** in the [Testing](#testing) section. ```php $client = new SupaClient([ @@ -464,13 +619,15 @@ $client = new SupaClient([ The handler must return `['statusCode' => int, 'body' => string]` where `body` is the raw JSON response. -## Developing & tests +## Developing & tests (this repository) ```bash composer install composer test ``` +This runs PHPUnit on `tests/`, including stub coverage for **`Supaship\Testing\HttpStub`**. + Maintainers: To ship a version from GitHub, use **Actions → Publish Supaship PHP SDK** (`publish.yml`): it validates, runs tests, and opens a GitHub Release with auto-generated notes from commits since the last release. ## License diff --git a/src/Testing/HttpStub.php b/src/Testing/HttpStub.php new file mode 100644 index 0000000..080b8a3 --- /dev/null +++ b/src/Testing/HttpStub.php @@ -0,0 +1,46 @@ + $variations feature name => variation value (Edge: features[name].variation) + */ + public static function success(array $variations = [], int $statusCode = 200): \Closure + { + $features = []; + foreach ($variations as $name => $value) { + $features[$name] = ['variation' => $value]; + } + + try { + $json = json_encode(['features' => $features], JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new \RuntimeException($e->getMessage(), 0, $e); + } + + return static function (string $url, string $jsonBody) use ($statusCode, $json): array { + return ['statusCode' => $statusCode, 'body' => $json]; + }; + } + + /** + * Handler that returns a non-2xx status or arbitrary body (simulates errors). + */ + public static function failure(int $statusCode = 500, string $body = ''): \Closure + { + return static function (string $url, string $jsonBody) use ($statusCode, $body): array { + return ['statusCode' => $statusCode, 'body' => $body]; + }; + } +} diff --git a/tests/HttpStubTest.php b/tests/HttpStubTest.php new file mode 100644 index 0000000..560c1c7 --- /dev/null +++ b/tests/HttpStubTest.php @@ -0,0 +1,36 @@ + true]); + $decoded = json_decode($handler('', '{}')['body'], true, flags: JSON_THROW_ON_ERROR); + + self::assertSame(['flag' => ['variation' => true]], $decoded['features']); + } + + public function testClientUsesStubWithoutNetwork(): void + { + $client = new SupaClient([ + 'sdkKey' => 'test', + 'environment' => 'test', + 'features' => ['x' => false], + 'context' => [], + 'networkConfig' => [ + 'featuresAPIUrl' => 'https://test.local/features', + 'httpHandler' => HttpStub::success(['x' => true]), + ], + ]); + + self::assertTrue($client->getFeature('x')); + } +} From ec9b47765527ffb545c71b9b5b7c73462d454e28 Mon Sep 17 00:00:00 2001 From: yasodhadollu Date: Wed, 1 Apr 2026 13:23:21 +0530 Subject: [PATCH 3/3] remove deploy.md file --- DEPLOY.md | 199 ------------------------------------------------------ 1 file changed, 199 deletions(-) delete mode 100644 DEPLOY.md diff --git a/DEPLOY.md b/DEPLOY.md deleted file mode 100644 index 0d3c9c9..0000000 --- a/DEPLOY.md +++ /dev/null @@ -1,199 +0,0 @@ -# Publishing the Supaship PHP SDK (beginner guide) - -This document explains **end to end** how a public PHP library on GitHub becomes installable with Composer (Packagist). No prior publishing experience is assumed. - -## What you are actually “publishing” - -PHP libraries are **not uploaded** to Packagist like some other ecosystems. Instead: - -1. Your code lives in a **Git** repository (for example on GitHub). -2. **Packagist** (packagist.org) registers that repository and watches it for **new Git tags**. -3. When you create a version tag (for example `v1.0.0`), Packagist creates a **release** of your package that `composer require` can install. - -So “publish” means: **push clean code to GitHub** and **create a semver Git tag**. Packagist does the rest once it is connected. - -## Glossary - -| Term | Meaning | -|------|--------| -| **Composer** | PHP’s dependency manager (`composer install`, `composer require`). | -| **Packagist** | The default registry Composer uses to find packages. | -| **Package name** | The `name` field in `composer.json` (here: `supashiphq/php-sdk`). It must be **unique** on Packagist. | -| **Semantic version** | Versions like `1.0.0`, `1.1.0`, `2.0.0`. Users depend on ranges like `^1.0`. | -| **Git tag** | A named pointer to a specific commit, e.g. `v1.0.0`, often used as a release marker. | - -## One-time prerequisites - -1. **Git** installed, and you can push to the GitHub remote for this repo. -2. **PHP** (8.1+) and **Composer** installed on your machine for local checks. -3. A **Packagist.org account** (free): sign up at https://packagist.org - -## One-time: connect the GitHub repository to Packagist - -These steps assume the library already has a valid `composer.json` in the **root** of the repo (this project does). - -### Step 1 — Confirm `composer.json` - -Open `composer.json` and check: - -- **`name`**: `vendor/package` format, lowercase, matches what you want on Packagist (`supashiphq/php-sdk`). -- **`type`**: `"library"` for a reusable package. -- **`license`**, **`require`** (PHP version and extensions), **`autoload`**: all set. - -Locally run: - -```bash -composer validate --strict -composer install -composer test -``` - -Fix any errors before submitting to Packagist. - -### Step 2 — Push the repository to GitHub - -If the repo is not on GitHub yet: - -1. Create a **new empty** repository on GitHub (no README/license needed if you already have them locally). -2. From your laptop, in the SDK folder: - -```bash -git remote add origin https://github.com/ORGANIZATION/php-sdk.git -git push -u origin main -``` - -Use your real org/user and repo URL. - -### Step 3 — Submit the package on Packagist - -1. Log in to https://packagist.org -2. Click **Submit** (top right). -3. Paste the **GitHub repository URL** (HTTPS), e.g. `https://github.com/ORGANIZATION/php-sdk` -4. Click **Check**, then **Submit**. - -Packagist will read `composer.json` from the default branch and register the package. - -### Step 4 — Enable automatic updates (GitHub webhook) - -Without a webhook, you must click **Update** on Packagist after every change. With a webhook, new tags and branch updates sync automatically. - -On Packagist, open your package → **Maintainers** / **Settings** (wording may vary) and follow **“GitHub Service Hook”** or **“Webhook”** instructions. Typically you: - -1. Copy a **webhook URL** and/or token from Packagist. -2. On GitHub: repo **Settings → Webhooks → Add webhook** and paste the URL Packagist gives you. - -After this, pushing tags or commits usually triggers Packagist to refresh within about a minute. - -## Every release: tagging a new version - -Semantic versioning (https://semver.org) in short: - -- **MAJOR** (x.0.0): breaking API changes. -- **MINOR** (1.x.0): new features, backwards compatible. -- **PATCH** (1.0.x): bug fixes, backwards compatible. - -### Step 1 — Prepare the repo - -1. Merge work to `main` (or your release branch). -2. Run tests and validation: - -```bash -composer validate --strict -composer install -composer test -``` - -3. **Commit** any pending changes with a clear message. - -### Step 2 — Choose the next version number - -Examples: - -- First public release: `1.0.0` -- Bugfix: bump patch (`1.0.0` → `1.0.1`) -- New backwards-compatible feature: bump minor (`1.0.1` → `1.1.0`) - -### Step 3 — Create an annotated Git tag - -Many PHP projects use a **`v` prefix** on tags (e.g. `v1.0.0`). The **tag name** is what Packagist turns into a version; it must match what Composer expects. - -```bash -git checkout main -git pull - -git tag -a v1.0.0 -m "Release 1.0.0" -git push origin v1.0.0 -``` - -- **`-a`**: annotated tag (recommended; includes message and metadata). -- **`git push origin v1.0.0`**: publishing the tag is what notifies Packagist (via webhook). - -### Step 4 — Confirm on Packagist - -Open `https://packagist.org/packages/supashiphq/php-sdk` (adjust if your vendor name differs). Within a short time you should see the new version listed. - -If it does not appear: - -- Check **GitHub Actions** on the repo (this project runs **Release** workflow on `v*` tags). -- On Packagist, use **Update** manually once. -- Verify the tag exists on GitHub (**Releases** or **Tags** tab). - -### Step 5 — Optional: GitHub Release notes - -A **Git tag** is enough for Packagist. A **GitHub Release** (with notes) is optional but nice for humans: - -1. GitHub repo → **Releases** → **Draft a new release** -2. Choose the tag `v1.0.0` -3. Add title and changelog, publish. - -This does not replace Packagist; it is documentation for users browsing GitHub. - -## How users install the published package - -After the package is on Packagist: - -```bash -composer require supashiphq/php-sdk -``` - -They can pin versions: - -```bash -composer require supashiphq/php-sdk:^1.0 -``` - -## GitHub Actions in this repo - -| Workflow | When it runs | Purpose | -|----------|----------------|--------| -| `ci.yml` | Push / PR to `main` or `master` | `composer validate`, install deps, **`composer test`** on PHP 8.1–8.3. | -| `publish.yml` | **Manually** (Actions → Run workflow) | Asks for a **version**, runs the same checks, then creates a **GitHub Release** and **tag** with release notes built from commits since the **previous GitHub release** (or recent history if this is the first release). | - -### Using `publish.yml` instead of tagging by hand - -1. Open the repository on GitHub → **Actions**. -2. Select **Publish Supaship PHP SDK**. -3. Click **Run workflow**. -4. Enter a version (`1.0.0` or `v1.0.0`). -5. After tests pass, the workflow creates the tag and release; Packagist can pick up the new tag like any other tag push. - -You can still tag locally if you prefer; **`publish.yml` is optional** but keeps validation and notes in one place. - -These workflows **do not** log in to Packagist for you. Public packages are normally updated by Packagist’s **GitHub integration** when tags appear on the default branch. - -## Troubleshooting - -| Problem | What to check | -|---------|----------------| -| Packagist says **“package not found”** after `composer require` | Typo in package name; Packagist not submitted yet; wait a few minutes after submit. | -| **New tag missing** on Packagist | Webhook missing or failed (GitHub **Settings → Webhooks**); click **Update** on Packagist; confirm `git push origin vX.Y.Z` succeeded. | -| **`composer validate` fails** | Invalid JSON, missing required fields, bad version constraints. | -| **Tests fail on CI** | Match PHP extensions (`json`, `openssl`); run `composer test` locally. | - -## Changing the public package name - -If you change `name` in `composer.json`, you effectively create a **new** Packagist package. Avoid unless you intend to deprecate the old name and coordinate with your team and docs. - ---- - -**Summary:** Push your repo to GitHub → register it once on Packagist → set up the webhook → for each release, merge to `main`, run tests, then either run the **`publish.yml`** workflow (recommended: validates, then creates the GitHub Release and tag) **or** tag manually with `git tag` / `git push origin vX.Y.Z`. Users install with `composer require supashiphq/php-sdk`.