From e917e512fcc16737934b0a355d8291e0ed062e1c Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 10:52:37 +0100 Subject: [PATCH 01/14] Add PubSub package split plan (PDR-091b) Execution plan for the ably-php -> ably-pubsub-php split: single ably/pubsub-server package published from a distribution mirror, no core package, Ably\PubSub namespace, Server::createHttpClient door. Co-Authored-By: Claude Fable 5.1 --- plan.md | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..9d45013 --- /dev/null +++ b/plan.md @@ -0,0 +1,207 @@ +# ably-php → ably-pubsub-php: PubSub package split plan + +Execution plan for applying PDR-091b (PubSub package split, major releases) to the PHP SDK. + +**Sources of truth:** +- [PDR-091: SDK naming and MAU classification](https://ably.atlassian.net/wiki/spaces/product/pages/5220106242) — parent, DECIDED IN PRINCIPLE. Device/server split, factory doors, the agent header as the declaration mechanism. Names `ably-php → ably-pubsub-php` explicitly as a rename-in-place repo. +- [PDR-091b companion: PubSub package split (major releases)](https://ably.atlassian.net/wiki/spaces/product/pages/5362810886) — DECIDED. New majors on a new core, not thin wrappers; repo renames; old packages "not touched at all" and EOL after one year. Names the PHP-specific publishing rebind: "Packagist (ably-php) — GitHub webhook/App install tied to the repo; needs re-pointing." +- [PDR-091b2: Per-SDK rollout plan v2](https://ably.atlassian.net/wiki/spaces/product/pages/5348425729) — IN REVIEW. PHP section, verbatim: "Repo: ably-php → ably-pubsub-php. New package: `ably/pubsub-server`. Factory door: `createHttpClient(...)` only." Owner field is blank. **No core package is named for PHP** — the only SDK row without one (see step 1). The agent-identifier convention (per-language family rename + cross-SDK versionless side flags) is Umair's footer comment on this page (2026-09-02). +- [PDR-091c companion: high-level SDKs stay unified](https://ably.atlassian.net/wiki/spaces/product/pages/5363499015) — DECIDED. PHP has two Ably-owned wrapper packages, `ably/ably-php-laravel` and `ably/laravel-broadcaster`; they stay unified, ship a new major depending on the new package, and construct through the door (step 27). +- [PDR-091d companion: public API renaming](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781) — **IN REVIEW** (steps marked ⚠️091d are conditional on it being decided). Names `AblyRest → HttpClient` explicitly. +- [SDK device/server split — step sequence doc](https://docs.google.com/document/d/1r96vOSOft1yT84NxbdpCh0IomTwzIpKskQ-wA5e28M0) — the 12-step order this plan follows. +- Reference implementations: [ably-js#2293](https://github.com/ably/ably-js/pull/2293) (split, MERGED to `integration/v3`), [#2294](https://github.com/ably/ably-js/pull/2294) (UTS per side), [#2296](https://github.com/ably/ably-js/pull/2296) (lockstep release), [#2297](https://github.com/ably/ably-js/pull/2297) (versionless side flags + `ably-pubsub-js` identifier); [ably-ruby#453/#454/#455/#457](https://github.com/ably/ably-ruby/pulls) and `Git/ably-ruby/plan.md`; [ably-python#683/#684/#685/#686/#682](https://github.com/ably/ably-python/pulls) and `Git/ably-python/plan.md` (server-only siblings; Python's `ably_pubsub` namespace decision is the closest analogue to PHP's, step 2). Also ably-dotnet#1333–1335 and ably-java#1232–1235 for the door/agent shape. +- Agent registry: [ably-common#361](https://github.com/ably/ably-common/pull/361) — **OPEN** as of 2026-09-09. Registers `ably-pubsub-server`/`ably-pubsub-device` (versionless flags, `type: runtime`) and `ably-pubsub-php` (versioned `sdk`, `source: https://github.com/ably/ably-php` — update the source to the renamed repo before or after merge). Today's registry has `ably-php` only, plus the wrapper entries `ably-php-laravel` and `laravel-broadcaster`. +- Infra prerequisite: [infrastructure#13005](https://github.com/ably/infrastructure/pull/13005) — **MERGED 2026-09-01**; `ably-sdk-builds-ably-pubsub-php` IAM role exists (the `ably-php` role stays). [infrastructure#13054](https://github.com/ably/infrastructure/pull/13054) added the ID-qualified OIDC subject. +- Already open in this repo: [ably-php#222](https://github.com/ably/ably-php/pull/222) (`pubsub-split/rename-references` → `main`) — `features.yml` repository-name + README licence link. **Merge only in the rename freeze window** (step 17). There is no prior thin-wrapper PR to close (unlike ably-python#681 / ably-js#2291). + +**What PHP ships (per PDR-091b2):** PHP is a **server-only, REST-only** SDK (no realtime client exists; the README points realtime users at the MQTT adapter). One new public Composer package, **`ably/pubsub-server`**, with a single factory door, **`createHttpClient(...)`**. No device package, no realtime door. Repo renamed `ably-php → ably-pubsub-php` (direct rename — Packagist identity is the `vendor/name` in `composer.json`, not the repo URL, so the ably-go/ably-cocoa copy-first exception does not apply). Today's `ably/ably-php` package (currently 1.1.12) enters a 1-year maintenance window, then EOL. + +**Current repo facts the plan relies on (verified 2026-09-09):** +- Single flat package: `composer.json` `name: ably/ably-php`, PSR-4 `Ably\\ → src/`, `tests\\ → tests/`; 3.3k lines in `src/`, 28 test files. No `composer.json` version field — Packagist derives versions from git tags (`1.1.12`, no `v` prefix). **The only version site is `Defaults::LIB_VERSION`** (`src/Defaults.php:6`). +- Requires `php: ^7.2 || ^8.0`; CI matrix tests 7.2–8.4 × JSON/msgpack (`check.yml`). PHP 7.x and 8.0 are EOL upstream. +- Agent string is built in one place, `AblyRest::ablyAgentHeader()` (`src/AblyRest.php:37`): `ably-php/ php/` plus entries from a **static, process-global** `AblyRest::$agents` map set via `AblyRest::setAblyAgentHeader($name, $version)` (and the `@deprecated` `setLibraryFlavourString`). Sent as `Ably-Agent` from `src/AblyRest.php:187`. **There is no per-client `agents` option.** `tests/HttpTest.php:52-85` asserts the exact current shape (RSC7d), including the `laravel customLib/2.3.5` static-append form. +- Both Laravel wrappers rely on the static setter: `ably-php-laravel` calls `AblyRest::setAblyAgentHeader('laravel', $laravelVersion)` then `new AblyRest(config('ably'))`; `laravel-broadcaster` sets `laravel-broadcaster/` and `laravel/` the same way and type-hints `\Ably\AblyRest` in `AblyBroadcaster::__construct`. They require `ably/ably-php: ~1.1.9` and `^1.1` respectively. +- Publishing today is **manual and tag-driven**: bump `LIB_VERSION`, update `CHANGELOG.md`, push tag, create a GitHub release; Packagist auto-updates via the legacy repo webhook `https://packagist.org/api/github?username=ably` (`push` events). There is **no release workflow** and no trusted publishing concept on Packagist. Packagist maintainer account: `ably`. `ably/ably-php` has ~8.7M total / ~387k monthly downloads. +- `features.yml` uploads via `ably/features` with `repository-name` (already `ably-pubsub-php` on PR #222's branch). No docs workflow. +- Non-Composer extras that carry the old identity: `ably-loader.php` (hand-rolled autoloader for `Ably\`), `demo/index.php` + `Procfile` (Heroku demo using `new \Ably\AblyRest`), `ably-common` submodule at the repo root (test fixtures). +- Known `@deprecated` surface today: `Auth::authorise` (→ `authorize`), `AblyRest::setLibraryFlavourString`. No `endpoint` client option exists yet (ADR-119 was never applied to PHP), so `restHost`/`environment` are **not** deprecated here and are not deletion candidates. +- Packagist names: `ably/pubsub-server` (and `ably/pubsub-core`) do not exist. Packagist has **no name reservation**; a package exists once it is submitted against a repository whose default branch `composer.json` carries that name. +- **Packagist mechanics, verified against the composer/packagist and composer/composer sources (2026-09-09):** + - Uniqueness is by **package name only** (`UniquePackageValidator` → `findOneByName`). Two Packagist packages may point at the same repository URL. The name is read from the default branch's `composer.json` at submission (`Package::setRepository` → `getComposerInformation(getRootIdentifier())`). + - **There is no name-based filtering of versions.** Composer's `VcsRepository::preProcess` deliberately overwrites every tag's and branch's `name` with the default branch's name ("this ensures that a package can be renamed in one place and that all old tags will still be installable using that new name"), and Packagist's `Updater` then stamps its own package name on every version. Consequence: **every Packagist package bound to a repository indexes every Composer-valid tag and every branch of that repository, whatever their `composer.json` says.** Tags whose name is not a valid version (e.g. `pubsub-server/2.0.0`) are skipped ("Skipped tag …, invalid tag name"). + - Repository renames are handled: the GitHub webhook resolves a package by URL first, then by the stored GitHub repository ID (`remoteId`), and on an ID match **rewrites the stored URL and schedules a full re-crawl** (`ApiController::findGitHubPackagesByRepository`). The updater also refreshes the URL from the driver. So auto-update does **not** silently stop at a rename; editing the URL by hand is optional (and `PopularPackageSafetyValidator` allows it for a same-ID rename even on a package with >50k downloads). Re-pointing a >50k-download package at a *different* repository is blocked for maintainers and needs Packagist support; a crawl that finds a different repository ID **freezes** the package (`RemoteIdMismatch`). + - Maintainers can delete individual versions (`delete_version`); maintainer-pulled rows stay deleted across re-crawls. Maintainers can mark a package **abandoned with a replacement package**, which Composer prints on every install. + +--- + +## Phase 0 — Decisions and prerequisites (before writing code) + +1. **Packaging shape on Packagist — ONE published package, `ably/pubsub-server`, no `ably/pubsub-core`, published from a read-only distribution mirror.** This is the PHP-specific fork from the pattern, and PDR-091b2 already lists only `ably/pubsub-server` for PHP; record the reasoning in the b2 PHP section so it is a decision rather than an omission: + - **Why no core:** a core buys nothing in PHP — there is no device package to share it with, and PHP cannot re-export types (no `export … from`, only `class_alias`), so a separate core would force consumers to type-hint `Ably\PubSub\Core\Models\Message` from a package whose description says "do not depend on this". The high-level consumers (the two Laravel wrappers, PDR-091c) can depend on `ably/pubsub-server` directly: 091c's reason for depending on core types — not picking a side — does not arise when there is exactly one side. Each extra Composer package is also an extra mirror repo (below). + - **Why a mirror:** Packagist indexes **every** Composer-valid tag and branch of a repository under **every** package bound to that repository, ignoring the `composer.json` name (verified, see repo facts). If `ably/pubsub-server` were registered against this repo, it would serve `1.1.12` (old namespace, old name) as an installable version; and `ably/ably-php` would serve `2.0.0` (new namespace) as its latest — any `ably/ably-php: *` or `>=1.1` consumer would upgrade into a broken install. Neither can be prevented by naming or by the updater; only by maintainers deleting versions by hand after every release. So the two packages **cannot share a repository**, and the legacy package must stay on this repository (its ~8.7M downloads put it behind `PopularPackageSafetyValidator`, so its URL cannot be moved to a different repository without Packagist support, and a repo-ID change would freeze it). Therefore: + - `ably/ably-php` stays bound to this repo (renamed to `ably-pubsub-php`; Packagist follows the rename by repository ID). It keeps indexing `1.x` tags from `maintenance/1.x`. + - `ably/pubsub-server` is bound to a **read-only distribution mirror repo**, e.g. `ably/ably-pubsub-php-dist` (name is the owner's call; it is a build artifact, not a development repo, so PDR-091's "repo names never carry side" does not apply, and its README says "read-only mirror — develop and file issues at ably-pubsub-php"). `release.yml` pushes the release commit and the `2.0.0` tag there (step 15). No subtree split is needed because the package is the whole repo; the mirror is a plain push of `main` at the release commit. + - **Release tags in this repo must not be Composer-valid versions**, or `ably/ably-php` indexes them. Use a namespaced tag, `pubsub-server/2.0.0`, for the GitHub release and CHANGELOG compare links here; the plain `2.0.0` tag exists only on the mirror. Composer skips the namespaced tag as an invalid version name (`VcsRepository::validateTag`). + - Rejected alternatives: register both packages on this repo and prune versions by hand after every release (a window on every release where `composer require ably/ably-php` resolves to 2.x, and ongoing toil); move the legacy package to a mirror of `maintenance/1.x` instead (needs Packagist support because of the download threshold, changes the repo ID, and puts the high-traffic package on the moving part); the ably-go/ably-cocoa copy-under-old-name route (a new repo at `ably/ably-php` would destroy GitHub's redirects and the legacy Packagist entry would resolve to a repo with a different ID and be frozen). + - **Surface this to the programme:** PDR-091b states registry identity "is not tied to the repo URL" for Packagist. Identity is not, but *version discovery* is — every tag of the repo belongs to every package on it — which is why PHP needs the mirror. Add it to the b2 PHP row so no one plans a second Composer package from this repo later. +2. **PHP namespace — recommend `Ably\PubSub\`.** The legacy package owns the PSR-4 prefix `Ably\` → `src/`. Composer installs `ably/ably-php` and `ably/pubsub-server` side by side (a Laravel app mid-migration, or a transitive dependency still on 1.x), and if both declared `Ably\` the autoloader would resolve `Ably\AblyRest` to whichever prefix path is searched first — a silent mismatch, and PDR-091b also says the old package is "not re-exported or reused". So the new package uses a distinct root: implementation and public types under **`Ably\PubSub\`** (`Ably\PubSub\Models\Message`, `Ably\PubSub\Models\ClientOptions`, …), the door at **`Ably\PubSub\Server`**. Rejected: keeping `Ably\` (clobbers the legacy package); `Ably\PubSub\Core\` for the implementation (the types are public surface in PHP whether we like it or not, and "Core" in every consumer's `use` line contradicts "internal"). Record in PDR-091b2. +3. **Factory-door surface** — record it in PDR-091b2 and fill in the blank Owner field. Proposal, matching the agreed door name in PHP idiom: + - `Ably\PubSub\Server::createHttpClient(array|string|ClientOptions $options): \Ably\PubSub\AblyRest` (⚠️091d: returns `HttpClient`). Accepts exactly what the constructor accepts today — an options array, a `ClientOptions`, or a bare key/token string (reuse the constructor's `strpos(':')` disambiguation rather than duplicating it). + - Only door. No `createRealtimeClient` (no realtime client exists) and no device door. Say so explicitly in b2 so nobody expects a stub. + - The client constructor stays public (a `final` static door with a private constructor is not idiomatic for a class other Ably code and tests construct), but is marked `@internal` with a docblock pointing at the door, and the README/docs never show it. Consequence to state in the guide: a bare `new AblyRest(...)` from the new package sends no side flag and will be rejected on MAU accounts once pricing is live. +4. **New major version — `2.0.0`.** Current is 1.1.12. Tags keep the repo's existing no-`v` convention (`2.0.0`, `2.0.0-rc1`). Composer treats `-rc1`/`-beta1` as non-stable, so `composer require ably/pubsub-server` never resolves a prerelease unless `minimum-stability` allows it. Single package, so "lockstep" reduces to one tag and one version site. +5. **PHP floor — recommend `php: ^8.1`** for the new package, CI matrix 8.1–8.5 (8.5 is current; `curl_close` deprecation fix already in 1.1.12). 7.2–8.0 are EOL and Laravel 10+ requires 8.1; a major is the only place to raise this. Keep `rybakit/msgpack`, `ext-json`, `ext-curl`, `ext-openssl`. Modern-PHP cleanups (typed properties, `readonly`, enums) are **not** in scope — this is a repackaging, not a rewrite. Owner's call; if they want to keep 7.x support, that is a one-line change in `composer.json` and the matrix. +6. **⚠️091d — build on current names, rename last.** Phases 2–5 use `AblyRest`/`Channel`/`Presence`; if PDR-091d is approved, the new package ships `HttpClient` (and the owner decides whether `Channel`/`Channels`/`Presence` become `HttpChannel`/`HttpChannels`/`HttpPresence` for cross-SDK consistency — PHP never had a `Rest` prefix on them) and the deprecated surface is deleted rather than ported — as one final mechanical PR (Phase 6, step 18). +7. **Register the agent identifiers.** Both strings PHP will send must be in the ably-common registry before any prerelease ships them: the versionless side flag `ably-pubsub-server` and the versioned family identifier `ably-pubsub-php`. Both are in ably-common#361 (OPEN) — track it to merge; if it stalls, split the PHP entries into their own PR. Ask for the `ably-pubsub-php` `source` to be the renamed URL. The `-server` suffix is load-bearing: realtime grants the MAU server exemption on API-key auth by matching an agent entry ending in `-server`. Then bump the `ably-common` submodule pin in this repo. The wrapper identifiers `laravel`, `laravel-broadcaster`, `ably-php-laravel` are secondary identifiers and keep their names. +8. **Get the MAU pricing release date** from the [MAU pricing release plan (DRAFT, 2026-08-27)](https://ably.atlassian.net/wiki/spaces/PRICE/pages/5315690510) / project hub — it drives the GA target and the forcing-function messaging in the migration guide. +9. **Packagist account and mirror-push credentials.** Confirm who holds the `ably` Packagist account (1Password / SDK team) — it is needed to submit `ably/pubsub-server` against the mirror (step 15c), to pull `dev-main` from the legacy package (step 22), and to mark the legacy package abandoned at EOL (step 26). Confirm the webhook secret on this repo's Packagist hook is that account's API token (it survives the rename; the hook is repo-bound). Create the mirror repo and a credential that lets `release.yml` push to it (a GitHub App installation or a fine-grained PAT scoped to the mirror, stored as a repo secret here — the default `GITHUB_TOKEN` cannot push cross-repo). Set the mirror up with the same Packagist webhook. + +## Phase 1 — Open the integration branch + +10. Create a long-lived **`integration/v2`** branch off `main` (ably-ruby uses `integration/v2` for the same bump; ably-js `integration/v3`, ably-python `integration/v4`). All split work lands there as **stacked PRs**; nothing ships from it until Phase 7. Apply the same branch protection/required checks as `main`. `check.yml` and `features.yml` trigger on `pull_request` regardless of base so PRs into the integration branch get CI for free; add `integration/v2` to their `push:` branch lists so the merged state is also checked. +10b. **The PR stack.** Mirror ably-ruby (#453 → #454; #455/#457 straight to `main`) and ably-python (#683 → #684 → #686; #685/#682 to `main`): each PR is based on the one before it so they review independently and merge in order into `integration/v2`; the workflow/rename PRs go to `main` directly because they must exist on the default branch to be useful. Branch names `pubsub-split/`. + + | # | Branch | Base | Contents | Plan steps | + | --- | --- | --- | --- | --- | + | 1 | `pubsub-split/restructure` | `integration/v2` | `composer.json` (name, PSR-4 `Ably\\PubSub\\`, php floor), namespace move `Ably\` → `Ably\PubSub\` across `src/` and `tests/`, `Server::createHttpClient` door, per-client `agents` option + family identifier rename + side stamping, agent assertions, packaging-invariant tests, delete `ably-loader.php`/`demo/`/`Procfile` | 11, 12, 13, 14, 14b, 14c | + | 2 | `pubsub-split/release-tooling` | PR 1's branch | `release.yml` (dispatch, pre-flight, mirror push + `2.0.0` tag, namespaced tag + GitHub release here, Packagist visibility poll), `release-dry-run` job in `check.yml`, `CONTRIBUTING.md` release section, `.gitattributes` export-ignore | 15, 15b, 15c | + | 3 | `pubsub-split/release-workflow-on-main` | **`main`** | cherry-pick of PR 2's `release.yml` only, so `workflow_dispatch` is registered while the split still lives on `integration/v2`. Pre-flight refuses a ref whose `composer.json` is still `ably/ably-php`, so it is inert on `main`. **Merge precondition: the 1.x maintenance branch (step 24) is cut first** | 15b, 24 | + | 4 ([#222](https://github.com/ably/ably-php/pull/222)) | `pubsub-split/rename-references` | **`main`** | `features.yml` repository-name, README licence link; merged in the rename freeze window, then merged forward into `integration/v2` | 17 | + | 5 | `pubsub-split/docs` | PR 2's branch | README rewrite, `UPDATING.md` 1.x→2.0 migration section, `CHANGELOG.md` 2.0.0 entry | 19, 20 | + | 6 | `pubsub-split/api-rename` (⚠️091d) | PR 5's branch | `AblyRest`→`HttpClient` etc., deprecated-surface deletions, test/doc updates — opened only once 091d is decided | 18 | + + Rules for the stack: PR 1's namespace move is a mechanical `sed` over ~60 files — keep it in its own commit, separate from the door/agent commits, so reviewers can diff behaviour without the noise (ably-python's "pure move, no behaviour" commit rule). Each PR's description links the plan step it implements. When a lower PR merges into `integration/v2`, retarget the next one. Merge `main` into `integration/v2` after PRs 3 and 4 land. Periodically merge `main` forward so the integration branch never drifts far from released fixes. + +## Phase 2 — Repo restructure: the `ably/pubsub-server` package + +11. **Re-identify the package** — same flat layout, new identity: + ``` + composer.json # name: ably/pubsub-server; description names the door and says "for servers"; + # php ^8.1; autoload psr-4 "Ably\\PubSub\\": "src/"; autoload-dev "tests\\": "tests/" + src/ + Server.php # the door: Ably\PubSub\Server::createHttpClient() + AblyRest.php … # today's src/**, namespace Ably\ → Ably\PubSub\ (⚠️091d renames land in Phase 6) + Defaults.php # LIB_VERSION = '2.0.0' — still the only version site + tests/ # namespace tests\ unchanged; `use Ably\…` → `use Ably\PubSub\…` + ably-common/ # submodule stays (test fixtures) + ``` + - No `core/`/`server/` directory split: with one Composer package it would be cosmetic (Packagist needs `composer.json` at the root), and it would make the `git mv` diff larger for no packaging gain. If step 1's fallback (a published core) is ever taken, the split can be introduced then. + - Delete `ably-loader.php` (hand-rolled autoloader; Composer is the only supported install path and the README already says so), `demo/` and `Procfile` (unmaintained Heroku demo). List them in the migration guide as removed. + - `keywords` gain `pubsub`, `server`; `homepage` stays; add `support.source` pointing at this repo (not the mirror) so `composer info` sends people to the development repo. `composer validate --strict` must pass (add `--strict` to CI — today it is plain `composer validate`). + - Add a `.gitattributes` with `export-ignore` for `tests/`, `ably-common/`, `.github/`, `phpunit.xml`, `plan.md` — Packagist dist archives are GitHub zipballs and honour it, so consumers stop downloading the test suite and submodule. +12. **Implement the door in `src/Server.php`:** + - `Server::createHttpClient($options)`: normalise the argument exactly as the constructor does (string → `['key'=>…]`/`['token'=>…]`, `ClientOptions` instance passed through), merge the side entry into `agents` (step 13), construct and return the client. Nothing else — it is a door, not a room. + - `final class Server` with a private constructor and the single static method; a `SERVER_AGENT_IDENTIFIER = 'ably-pubsub-server'` constant carrying the suffix-is-load-bearing comment copied in spirit from ably-ruby's `server.rb` / ably-python's `server/__init__.py`. + - Docblock: the two-sentence "servers are trusted environments, exempt from MAU counting; this package names that side" statement the siblings use, so the generated API docs say the same thing in every language. +13. **Agent plumbing.** Rework the static header to match the ably-js contract (`packages/shared/side.ts` / `getAgentString`) and the convention in ably-common#361: + - Add an additive **`agents` client option** (`ClientOptions::$agents`, `array`). `ablyAgentHeader()` becomes an instance method reading `$this->options->agents`, rendering each entry as `name/version`, or a bare `name` when the value is `null` or `''` (a flag, like `browser`). Single seam: `src/AblyRest.php:187` is the only place the header is sent. + - **Remove the static `$agents` map, `setAblyAgentHeader()` and the deprecated `setLibraryFlavourString()`.** Process-global mutable state that leaks between client instances is the wrong shape for a per-client declaration, and the only callers (the two Laravel wrappers) are being re-released on this package anyway (step 27). `setAblyAgentHeader` is not currently deprecated, so this is a stated owner decision, not a silent drop — record it in the b2 PHP section and the deletion list (step 18) with this reason. + - Rename the family identifier `ably-php` → **`ably-pubsub-php`** (versioned with `LIB_VERSION`) — on the integration branch, before any prerelease, so even prerelease traffic partitions cleanly from legacy `ably-php/*` traffic. The maintenance branch keeps `ably-php`. + - The door appends `ably-pubsub-server => null`. Target wire shape: `ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server`; with a wrapper: `ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server laravel/11.0.0 laravel-broadcaster/1.0.4`. + - The side entry is applied last and wins any collision on its own identifier — the side is the package's to declare, not the caller's. Caller-supplied `agents` entries are preserved in order. + - Keep `.ably/capabilities.yaml`'s `Agent Identifier: Agents:` (already declared; now true per-client rather than per-process). + +## Phase 3 — Tests and conformance + +14. Rework the test suite to the new namespace (`use Ably\PubSub\…`), keep the matrix green on the new floor (8.1–8.5 × JSON/msgpack), and run the full sandbox suite (`TestApp` against `ABLY_ENV=sandbox`) from the integration branch. PHP has no UTS, so the existing PHPUnit suite is the conformance pass. Update the `features.yml` job only if `ably/features` needs the new layout (it reads `.ably/capabilities.yaml`, which is unchanged). +14b. **Add explicit agent assertions that fail loudly** (this is what billing reads), in `tests/HttpTest.php` via the existing `HttpMock`: + - `Server::createHttpClient(...)`: the `Ably-Agent` request header matches `^ably-pubsub-php/\d+\.\d+\.\d+(\S*)? php/\S+ ably-pubsub-server$`, contains the versionless `ably-pubsub-server` token, and contains **no** `ably-pubsub-server/` form (the `name/undefined` regression ably-js#2297 guards against — in PHP the equivalent trap is `'ably-pubsub-server' => ''` or `null` rendering as `ably-pubsub-server/`). + - A bare `new AblyRest(...)` from the new package declares **no** side (asserts the door is the only stamping path). + - `agents => ['my-sdk' => '1.0']` survives and precedes the side entry; `agents => ['ably-pubsub-server' => 'x']` cannot override the side entry (renders versionless). + - Rewrite the RSC7d assertions at `tests/HttpTest.php:61` and `:80` for the new family identifier and the option-based (not static) wrapper form. +14c. **Add packaging-invariant tests** (PHPUnit, cheap, run on every PR): + - `composer.json` `name` is `ably/pubsub-server`; PSR-4 maps only `Ably\\PubSub\\`; no file under `src/` declares `namespace Ably;` or any namespace outside `Ably\PubSub\` (a missed file in the move would silently collide with the legacy package in a mixed install). + - `Defaults::LIB_VERSION` matches the top `CHANGELOG.md` entry (the release pre-flight checks this against the tag too; this catches it at PR time). + - Every class name in `src/` is loadable through Composer's generated autoloader (`composer dump-autoload -o` then `class_exists` over the map) — the PHP analogue of "the wheel contains the files it should". + +## Phase 4 — Release tooling: auto-publishing + +15. **Add `release.yml`** — PHP has no upload step (Packagist indexes git tags), so "publishing" is: push the release commit and version tag to the mirror, tag and release here, and prove Packagist saw it. `workflow_dispatch` with a required `version` input: + - **Pre-flight before anything is pushed:** version input is valid semver; equals `Defaults::LIB_VERSION`; equals the top `CHANGELOG.md` heading; `composer.json` `name` is `ably/pubsub-server` (refuses the legacy layout — this is what makes PR 3 inert on `main`); `composer validate --strict`; neither `pubsub-server/` here nor `` on the mirror already exists. Run the unit-level tests (mocked HTTP) as part of the pre-flight; the sandbox matrix already ran on the merged PR. + - **Publish to the mirror:** push the dispatched commit to the mirror's `main` and create the annotated tag `` there (this is the only place a Composer-valid `2.x` tag ever exists). Then create the namespaced tag `pubsub-server/` here and a GitHub release on it with the CHANGELOG section as body (`contents: write`). Prereleases (`-rc1`) are GitHub prereleases; Composer treats them as non-stable. + - **Post-publish check:** poll `https://repo.packagist.org/p2/ably/pubsub-server.json` until the version appears, with a bounded timeout; fail loudly if it does not. This is the automated proof that the mirror's Packagist webhook fired and the package resolved. + - **Re-run safety:** each step checks for its own artifact (mirror tag, local tag, GitHub release) and skips if present, so a run that failed part-way is completed by re-running it at the same version. + - **Guard against the one thing that must never happen:** the pre-flight also fails if a Composer-valid tag (`^v?\d+\.\d+\.\d+`) exists in this repo at or above `2.0.0` — that would mean someone tagged a 2.x release here by hand and `ably/ably-php` is now serving it. The `release-dry-run` job checks the same on every PR. +15b. **Make the workflow dispatchable early.** `workflow_dispatch` only registers once the file exists on the **default branch** — and this work merges to `integration/v2` until Phase 7 — so **cherry-pick `release.yml` to `main`** (PR 3), inert there because the pre-flight refuses `ably/ably-php`. Add a `release-dry-run` job to `check.yml` that runs the same pre-flight logic (minus pushes) on every PR so version-site/CHANGELOG drift surfaces continuously. Deliberately test the guardrails from a throwaway branch: mismatched version (aborts, nothing pushed), and a re-run at an existing version (skips to the poll). +15c. **Create the mirror and register `ably/pubsub-server` on Packagist — this can happen before the integration branch merges.** Because the mirror is its own repository, its default branch can carry `name: ably/pubsub-server` from day one: seed it with the first dispatch from `integration/v2` (a `2.0.0-rc1` or a throwaway `0.0.1-alpha1`), submit the mirror URL on packagist.org, confirm the derived name, add the SDK-team maintainers, and confirm the webhook updates it. This decouples the Packagist path from the `main` flip (step 22) and lets the prerelease (step 21) be a real Packagist install. +16. **Rewrite `CONTRIBUTING.md`'s release process** for the new flow (bump `LIB_VERSION` + CHANGELOG in a PR → merge → dispatch `release.yml`; no manual tagging), and state plainly **why no one may ever push a plain `2.x.y` tag to this repository** (the legacy Packagist package would serve it) and that 1.x maintenance tags stay plain because the legacy package depends on them. (The current CONTRIBUTING has no release section at all — it is the five-line fork/branch/PR list.) + +## Phase 5 — Repo rename and publishing rebind + +17. **Rename the repo `ably-php` → `ably-pubsub-php`** (direct rename; GitHub redirects cover clones and web links). The IAM prerequisite (infrastructure#13005) is merged, so this can happen any time — the natural slot is after PRs 1–2 merge to `integration/v2`, coordinated with the programme's cross-SDK rename freeze window (check with Evgenii whether renames are batched; ably-ruby is already renamed), and before Packagist is touched. Announce the freeze window to the SDK team first. In the same pass: + - Merge PR #222 (`features.yml` `repository-name: ably-pubsub-php`, README licence link) immediately after the rename, then merge `main` forward into `integration/v2`. On the integration branch also update README badges/links, `CONTRIBUTING.md`, `.ably/capabilities.yaml` links and `composer.json` `support.source`/`issues` if added. Leave historic `CHANGELOG.md` links alone (redirects cover them). + - **Legacy `ably/ably-php` Packagist entry — verify, do not rebind.** The repo webhook survives the rename and its payload carries the new URL plus the unchanged GitHub repository ID; Packagist falls back to matching by that ID, rewrites the stored URL itself and schedules a full re-crawl (verified in `ApiController::findGitHubPackagesByRepository`). So the PDR-091b line "Packagist … needs re-pointing" is satisfied automatically for PHP. Push a no-op commit to `maintenance/1.x` after the rename and confirm the package page shows the new repository URL and a fresh "last updated". Only if it does not, edit the URL by hand (allowed for a same-ID rename despite the download threshold). Update PR #222's description, which currently tells the reader to expect a stale URL. + - **Never recreate a repo named `ably/ably-php` afterwards.** A new repo under the old name destroys GitHub's redirect for every existing clone and link. PHP is a direct-rename SDK (the maintenance branch lives in the renamed repo), so the old name stays vacant forever. + - **Done when:** clone and web redirects verified (`git ls-remote` on an existing checkout, plus web URLs for the repo, a PR, and a file permalink); CI fully green post-rename (`check.yml` matrix on both `main` and `integration/v2`); `features.yml` runs clean with the new repository-name **and its upload lands** at `sdk.ably.com/builds/ably/ably-pubsub-php/main/…` (proves the new IAM role — this failure mode is otherwise silent); Packagist `ably/ably-php` shows the new repository URL and auto-updated on the no-op push. The mirror is unaffected by the rename. + +## Phase 6 — Public API pass (⚠️091d) — last change on the integration branch + +18. **This is deliberately the final code change before the integration branch merges**, gated on PDR-091d being approved — everything in Phases 2–5 is built and kept green on the current names, then this lands as one mechanical pass on top: + - Rename `Ably\PubSub\AblyRest` → `Ably\PubSub\HttpClient` (the door's return type follows). Owner decides on `Channel`/`Channels`/`Presence` → `HttpChannel`/`HttpChannels`/`HttpPresence` (091d's `RestChannel → HttpChannel` row; PHP's classes carry no prefix today, so this is consistency, not a rename of a wrong name). `PaginatedResult`, `HttpPaginatedResponse`, models and `Push*` classes stay. + - Produce the PHP deprecated-surface deletion list and delete rather than port. Known today: `Auth::authorise` (deprecated since 1.0), `AblyRest::setLibraryFlavourString` (deprecated) — plus the step-13 owner decision to drop the static `setAblyAgentHeader`, and the step-11 deletions (`ably-loader.php`, `demo/`, `Procfile`). Anything already deprecated is a deletion candidate by default; keeping it requires a stated reason. Nothing not currently deprecated is silently dropped. + - Re-run the full Phase 3 pass after the rename. + - Record the full old-name → new-name table in PDR-091b2's PHP section (it feeds the migration guide, step 20, and the Laravel wrappers' type hints, step 27). + - If 091d is **declined**, skip this phase and ship the surface as-is (the door returns `AblyRest`). If it is **still undecided** when the rest of the branch is done, escalate to the programme before merging: renaming later costs another major. + +## Phase 7 — Docs, prerelease, GA + +19. Rewrite `README.md` for the new package: `composer require ably/pubsub-server`; a single door quickstart (`use Ably\PubSub\Server; $ably = Server::createHttpClient(['key' => …]);`); a **Package** section saying what "server" means here (trusted runtime, API-key auth, MAU-exempt, the wire agent shape); the Laravel section pointing at the new wrapper majors; supported PHP versions table on the new floor; a "Migrating from `ably/ably-php` 1.x" section pointing at `UPDATING.md`; badges moved to `ably/pubsub-server` (they resolve once the package exists on Packagist, step 15c). Add one line for 1.x users pointing at `maintenance/1.x`, since the legacy Packagist page renders this README after step 22. Keep the "REST only, MQTT for realtime" note — it is still true. +20. **Migration guide** (new `UPDATING.md`, section "1.x (`ably/ably-php`) → 2.0.0 (`ably/pubsub-server`)"): machine-applicable mapping table — + + | 1.x (`ably/ably-php`) | 2.0 (`ably/pubsub-server`) | + | --- | --- | + | `composer require ably/ably-php` | `composer require ably/pubsub-server` | + | `use Ably\AblyRest;` / `new AblyRest($opts)` | `use Ably\PubSub\Server;` / `Server::createHttpClient($opts)` | + | `use Ably\Models\Message;` (any type) | `use Ably\PubSub\Models\Message;` | + | `AblyRest::setAblyAgentHeader('x', 'v')` | `Server::createHttpClient(['agents' => ['x' => 'v'], …])` | + | `require 'ably-loader.php'` | removed — use Composer's autoloader | + | ⚠️091d: `\Ably\AblyRest` type hints | `\Ably\PubSub\HttpClient` | + + Plus the deleted-API list, the statement that the returned client is the same REST client (channels, history, presence, auth, push admin unchanged), the MAU forcing function, and the EOL date. All samples rewritten onto the new package; the 23 PHP-bearing pages in the `docs` repo (`getting-started/php.mdx`, `getting-started/laravel.mdx`, `api/rest-sdk*.mdx`, auth/*, channels/*, …) and LLM-facing docs are the docs team's cross-SDK pass — hand them the table. +21. **Prerelease `2.0.0-rc1`** via `release.yml` from `integration/v2` — a real Packagist release through the mirror (step 15c), so the consumer-side check is the real path: in a clean container, `composer require ably/pubsub-server:2.0.0-rc1` from Packagist (no VCS repository stanza), construct through the door and assert the `Ably-Agent` value against a sandbox app. Confirm `composer require ably/pubsub-server` with default stability does **not** pick the rc. Install it **alongside** `ably/ably-php:1.1.12` in one project and confirm both autoload without collision (the step-2 rationale). Check the `ably/ably-php` Packagist page afterwards: it must show **no** `2.0.0-rc1` (proves the namespaced-tag rule in step 1 holds). +22. **Merge `integration/v2` to `main`.** New development continues on `main` under the renamed repo. From this moment the legacy `ably/ably-php` Packagist entry indexes `main` as `dev-main` carrying 2.x code (Composer forces the default branch's name onto every version, so this is not a mismatch it can detect), and its package page renders the 2.0 README. Mitigate: a maintainer pulls the `dev-main` version from `ably/ably-php` on packagist.org once (pulled rows stay pulled), and the 2.0 README carries a line pointing 1.x users at `maintenance/1.x`. Stable 1.x versions are unaffected — verify on the page, and in Phase 8 by the trivial 1.x patch release (step 25). +23. **GA release `2.0.0`, coordinated.** Dispatch `release.yml` at `main`; the Packagist poll is the proof. Released in the org-wide coordinated window with the other SDKs (rollout date is set by the programme, keyed to the MAU pricing date — do not GA unilaterally). GitHub release notes (on `pubsub-server/2.0.0` here) point at `UPDATING.md`. + +## Phase 8 — Maintenance window for the old `ably/ably-php` package + +24. Cut a maintenance branch from the last 1.x release (`1.1.12`, which is today's `main` less PR #222; naming TBC org-wide, e.g. `maintenance/1.x`) **before PR 3 (step 10b) merges to `main`**. Nothing on PHP's `main` breaks legacy releases as early as it does for Python (there is no tag-triggered publish to replace), but cutting it first keeps the rule uniform across SDKs and means a 1.x fix never has to be untangled from split work. The `ably/ably-php` package's future security/critical fixes are tagged from this branch only (`1.1.13`, …), by the existing manual process (or a copy of `release.yml` with the pre-flight pinned to `ably/ably-php`, owner's choice). No new features, **no deprecation notice in code, no runtime side-detection** — 091b says the existing packages are "not touched at all"; once MAU pricing is live the old constructor is rejected server-side, which is the intended forcing function. Its agent string stays `ably-php/1.x`, its namespace stays `Ably\`, its `composer.json` name stays `ably/ably-php`. +25. Cut a trivial patch release (`1.1.13`, plain tag) from the maintenance branch **after** steps 17 and 22 and confirm it appears on Packagist under `ably/ably-php` **and not** under `ably/pubsub-server` (the mirror never receives maintenance commits) — this proves the post-rename webhook path (step 17) and the two-repository separation (step 1) with a harmless release rather than at the first real security fix. +26. Publish the support policy: README banner + CHANGELOG entry on the maintenance branch and on `main` stating maintenance-only status, the EOL date (GA date + 1 year), and a link to the migration guide. When the programme issues deprecation notices (PDR-091 deferred decision D3, after MAU is live), mark `ably/ably-php` **abandoned on Packagist with `ably/pubsub-server` as the replacement** — Composer then prints "Package ably/ably-php is abandoned, you should avoid using it. Use ably/pubsub-server instead." on every install, which is the most effective migration nudge PHP has. In ably-common, add the sunset for `ably-php` (all versions) at the EOL date. The Laravel wrappers' 1.x lines EOL on the same date (step 27). + +## Phase 9 — Dependent Ably packages (PDR-091c; outside this repo, but this repo's owner drives them) + +27. **`ably/ably-php-laravel` and `ably/laravel-broadcaster` each ship a new major** depending on `ably/pubsub-server: ^2.0` (091c: unified, no split, dependency change only). Changes in each: replace `AblyRest::setAblyAgentHeader(...)` + `new AblyRest($config)` with `Server::createHttpClient($config + ['agents' => ['laravel' => $laravelVersion, 'laravel-broadcaster' => LIB_VERSION]])`; `laravel-broadcaster` retypes `AblyBroadcaster::__construct(AblyRest $ably, …)` to the new class (⚠️091d: `HttpClient`); READMEs and the `getting-started/laravel.mdx` page follow. Their agent identifiers keep their names (secondary identifiers). Their current majors enter the same 1-year maintenance window. Sequence: open their PRs once PR 1 here is merged and the namespace is fixed; release them in the same GA window, pinned to `2.0.0`, so a Laravel user never sees a wrapper that cannot resolve its dependency. + +--- + +## Cross-cutting checklist + +- [ ] Single-package shape (no `ably/pubsub-core`), the distribution mirror, and the namespaced-tag rule recorded in PDR-091b2; the "Packagist version discovery is repo-bound" finding raised with the programme (step 1) +- [ ] Mirror repo created, push credential in place, Packagist webhook on the mirror (step 9) +- [ ] Namespace `Ably\PubSub\` recorded in PDR-091b2 (step 2) +- [ ] Door signature `Server::createHttpClient(...)`, constructor `@internal`, static-agent removal recorded; Owner field filled (steps 3, 13) +- [ ] Version `2.0.0`, PHP floor `^8.1` (or owner's alternative) decided (steps 4, 5) +- [ ] `ably-pubsub-server` + `ably-pubsub-php` present in ably-common registry with the renamed `source`; submodule bumped (step 7; ably-common#361) +- [ ] MAU release date known; GA window agreed with programme (steps 8, 23) +- [ ] Packagist `ably` account access confirmed (step 9) +- [ ] ⚠️091d outcome tracked; Phase 6 executed, skipped, or escalated accordingly (step 18) +- [ ] Agent assertions in CI fail loudly, incl. the no-`ably-pubsub-server/` regression and the bare-constructor-declares-nothing check (step 14b) +- [ ] `release.yml` on `main` (inert), dry-run job on every PR, guardrails deliberately tested (steps 15, 15b) +- [ ] `ably/pubsub-server` registered on Packagist against the mirror; webhook proven (step 15c) +- [ ] Post-rename: redirects, CI, features upload (IAM), Packagist `ably/ably-php` URL auto-followed and auto-update proven; PR #222 description corrected (step 17) +- [ ] `2.0.0-rc1` proven via a real Packagist install, default-stability check, side-by-side install, and absence from `ably/ably-php` (step 21) +- [ ] `dev-main` pulled from `ably/ably-php` after the `main` flip (step 22) +- [ ] Maintenance branch cut before PR 3 merges; `1.1.13` proves the legacy Packagist path; EOL policy published; registry sunset added (steps 24–26) +- [ ] Laravel wrapper majors opened and released in the GA window (step 27) + +## Sequencing notes / risks + +- **Packagist is the PHP-specific trap, and it is about tags, not names.** Every package bound to a repository indexes every Composer-valid tag of that repository (Composer rewrites each version's name to the default branch's; Packagist stamps its own). So `ably/ably-php` and `ably/pubsub-server` cannot share this repository, the new package ships from a distribution mirror, and **a plain `2.x.y` tag must never be pushed here** — the `release.yml` pre-flight and the dry-run job refuse if one exists. The only residual leak is `dev-main` of the legacy package after the `main` flip, handled by a one-time maintainer pull (step 22). +- **The rename itself is benign for Packagist.** Auto-update follows the repository ID and self-corrects the URL; PR #222's warning about stale URLs is over-cautious and its description should be corrected. The rename still needs the freeze-window choreography for the features upload (IAM role), and a no-op push to prove the webhook path (step 17). +- **The mirror is one more thing to own:** a push credential, a webhook, a README banner, and the rule that issues and PRs are never accepted there. Put all four in `CONTRIBUTING.md`. +- **The agent-registry PR (step 7) is the only step worth doing immediately**; everything else flows through the integration branch. The `-server` suffix must never be changed without preserving the suffix (billing classifies by it) — the comment on the constant says so. +- **Namespace clobbering.** The new package must not declare anything under bare `Ably\`; the packaging test (14c) asserts it at PR time and the side-by-side install (21) proves it against real Composer resolution. +- **Static agent state goes away.** Both Laravel wrappers break on the new package until their own majors ship (step 27) — this is intended (they are being re-released regardless), but it means the wrapper PRs must be open before the PHP GA, not after. +- **PHP has one version site and one package**, so the lockstep pre-flight is small; the value of `release.yml` here is auto-publishing (PDR-091b's forcing function), the mirror push that no human should do by hand, and the Packagist visibility proof, not lockstep. +- **091d is the only rename that touches consumers' type hints** (`AblyRest` in `laravel-broadcaster`'s constructor and in every customer's DI container). Keep Phase 6 last so the wrapper PRs can be opened against a stable namespace and only the class name changes if 091d lands. +- **No realtime, no UTS, no device package**: PHP is the smallest split in the programme. The risk is not code volume, it is the registry/rename choreography above and getting the Laravel wrappers out in the same window. From 68f5acff9bc63e6b512d39979c625aaeab3b51cf Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:31:37 +0100 Subject: [PATCH 02/14] plan: record the PR numbers of the split stack (#223, #225, #226, #224) Co-Authored-By: Claude Fable 5.1 --- plan.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plan.md b/plan.md index 9d45013..aec7352 100644 --- a/plan.md +++ b/plan.md @@ -63,11 +63,11 @@ Execution plan for applying PDR-091b (PubSub package split, major releases) to t | # | Branch | Base | Contents | Plan steps | | --- | --- | --- | --- | --- | - | 1 | `pubsub-split/restructure` | `integration/v2` | `composer.json` (name, PSR-4 `Ably\\PubSub\\`, php floor), namespace move `Ably\` → `Ably\PubSub\` across `src/` and `tests/`, `Server::createHttpClient` door, per-client `agents` option + family identifier rename + side stamping, agent assertions, packaging-invariant tests, delete `ably-loader.php`/`demo/`/`Procfile` | 11, 12, 13, 14, 14b, 14c | - | 2 | `pubsub-split/release-tooling` | PR 1's branch | `release.yml` (dispatch, pre-flight, mirror push + `2.0.0` tag, namespaced tag + GitHub release here, Packagist visibility poll), `release-dry-run` job in `check.yml`, `CONTRIBUTING.md` release section, `.gitattributes` export-ignore | 15, 15b, 15c | - | 3 | `pubsub-split/release-workflow-on-main` | **`main`** | cherry-pick of PR 2's `release.yml` only, so `workflow_dispatch` is registered while the split still lives on `integration/v2`. Pre-flight refuses a ref whose `composer.json` is still `ably/ably-php`, so it is inert on `main`. **Merge precondition: the 1.x maintenance branch (step 24) is cut first** | 15b, 24 | + | 1 ([#223](https://github.com/ably/ably-php/pull/223)) | `pubsub-split/restructure` | `integration/v2` | `composer.json` (name, PSR-4 `Ably\\PubSub\\`, php floor), namespace move `Ably\` → `Ably\PubSub\` across `src/` and `tests/`, `Server::createHttpClient` door, per-client `agents` option + family identifier rename + side stamping, agent assertions, packaging-invariant tests, delete `ably-loader.php`/`demo/`/`Procfile` | 11, 12, 13, 14, 14b, 14c | + | 2 ([#225](https://github.com/ably/ably-php/pull/225)) | `pubsub-split/release-tooling` | PR 1's branch | `release.yml` (dispatch, pre-flight, mirror push + `2.0.0` tag, namespaced tag + GitHub release here, Packagist visibility poll), `release-dry-run` job in `check.yml`, `CONTRIBUTING.md` release section, `.gitattributes` export-ignore | 15, 15b, 15c | + | 3 ([#226](https://github.com/ably/ably-php/pull/226)) | `pubsub-split/release-workflow-on-main` | **`main`** | cherry-pick of PR 2's `release.yml` only, so `workflow_dispatch` is registered while the split still lives on `integration/v2`. Pre-flight refuses a ref whose `composer.json` is still `ably/ably-php`, so it is inert on `main`. **Merge precondition: the 1.x maintenance branch (step 24) is cut first** | 15b, 24 | | 4 ([#222](https://github.com/ably/ably-php/pull/222)) | `pubsub-split/rename-references` | **`main`** | `features.yml` repository-name, README licence link; merged in the rename freeze window, then merged forward into `integration/v2` | 17 | - | 5 | `pubsub-split/docs` | PR 2's branch | README rewrite, `UPDATING.md` 1.x→2.0 migration section, `CHANGELOG.md` 2.0.0 entry | 19, 20 | + | 5 ([#224](https://github.com/ably/ably-php/pull/224)) | `pubsub-split/docs` | PR 2's branch | README rewrite, `UPDATING.md` 1.x→2.0 migration section, `CHANGELOG.md` 2.0.0 entry | 19, 20 | | 6 | `pubsub-split/api-rename` (⚠️091d) | PR 5's branch | `AblyRest`→`HttpClient` etc., deprecated-surface deletions, test/doc updates — opened only once 091d is decided | 18 | Rules for the stack: PR 1's namespace move is a mechanical `sed` over ~60 files — keep it in its own commit, separate from the door/agent commits, so reviewers can diff behaviour without the noise (ably-python's "pure move, no behaviour" commit rule). Each PR's description links the plan step it implements. When a lower PR merges into `integration/v2`, retarget the next one. Merge `main` into `integration/v2` after PRs 3 and 4 land. Periodically merge `main` forward so the integration branch never drifts far from released fixes. From d5cf09e992d559a631f99aedddae196e5ebd2952 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 10:59:13 +0100 Subject: [PATCH 03/14] Re-identify the package as ably/pubsub-server Per PDR-091b2's PHP row this repository now produces the Pub/Sub server package rather than ably/ably-php: - name becomes ably/pubsub-server and the description names the door, so `composer info` points at the only supported entry point; - PSR-4 root becomes `Ably\PubSub\` (the legacy package keeps `Ably\`, so the two can be installed side by side without the autoloader resolving `Ably\AblyRest` to whichever prefix path is searched first); - the PHP floor moves to ^8.1. 7.2-8.0 are EOL upstream and Laravel 10+ already requires 8.1; a major is the only place to raise this; - keywords gain pubsub/server, and support.source/support.issues point at the development repo (ably-pubsub-php) rather than the distribution mirror the package is published from; - rybakit/msgpack gains an upper bound. The old `>=0.9.1` is an unbound constraint, which `composer validate --strict` rejects, and CI is moving to --strict. The namespace move itself is the next commit, so this one leaves the tree temporarily unloadable. Also adds .gitattributes: Packagist dist archives are GitHub zipballs and honour export-ignore, so consumers stop downloading the test suite, the ably-common fixture submodule and the CI/planning files. Co-Authored-By: Claude Opus 5 (1M context) --- .gitattributes | 11 +++++++++++ composer.json | 18 +++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..be2d039 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Keep the Packagist dist archive to what a consumer actually installs. +# GitHub zipballs honour export-ignore, so this trims the test suite, the +# ably-common fixture submodule and the CI/planning files from every release. +/tests export-ignore +/ably-common export-ignore +/.github export-ignore +/phpunit.xml export-ignore +/plan.md export-ignore +/.ably export-ignore +/.gitmodules export-ignore +/.gitattributes export-ignore diff --git a/composer.json b/composer.json index 5bca3d4..1b46f0d 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,21 @@ { - "name": "ably/ably-php", - "description": "Ably REST client library for PHP.", - "keywords": ["messaging", "messages", "ably"], + "name": "ably/pubsub-server", + "description": "Ably Pub/Sub SDK for servers (PHP). Construct clients with Ably\\PubSub\\Server::createHttpClient().", + "keywords": ["messaging", "messages", "ably", "pubsub", "server"], "homepage": "https://www.ably.com/", + "support": { + "source": "https://github.com/ably/ably-pubsub-php", + "issues": "https://github.com/ably/ably-pubsub-php/issues" + }, "require": { - "php": "^7.2 || ^8.0", - "rybakit/msgpack": ">=0.9.1", + "php": "^8.1", + "rybakit/msgpack": "^0.9.1 || ^1.0", "ext-json" : "*", "ext-curl" : "*", "ext-openssl" : "*" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.5" + "phpunit/phpunit": "^9.5" }, "license": "Apache-2.0", "authors": [ @@ -22,7 +26,7 @@ ], "autoload": { "psr-4": { - "Ably\\": "src/" + "Ably\\PubSub\\": "src/" } }, "autoload-dev": { From 87cad085529ec3a62e9e00eb71064e876034c566 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:00:15 +0100 Subject: [PATCH 04/14] Move the namespace Ably\ -> Ably\PubSub\ (pure move, no behaviour change) Mechanical rename of every `Ably\`-rooted symbol reference across src/ and tests/: namespace declarations, `use` statements, fully-qualified type hints, the fully-qualified class-name strings passed to PaginatedResult and HttpPaginatedResponse, and docblock types. No behaviour changes and no renamed classes: AblyRest is still AblyRest, only its namespace moved. Kept as its own commit so the door and agent commits that follow can be reviewed without this noise. Verified: `grep -rn 'Ably\\[A-Z]' src tests | grep -v 'Ably\\PubSub'` is empty, and every namespace declared under src/ is Ably\PubSub or a child of it. Co-Authored-By: Claude Opus 5 (1M context) --- src/AblyRest.php | 32 +++++----- src/Auth.php | 34 +++++------ src/Channel.php | 24 ++++---- src/Channels.php | 4 +- src/Defaults.php | 2 +- src/Exceptions/AblyException.php | 4 +- src/Exceptions/AblyRequestException.php | 2 +- src/Host.php | 2 +- src/HostCache.php | 4 +- src/Http.php | 18 +++--- src/Log.php | 2 +- src/Models/AuthOptions.php | 8 +-- src/Models/BaseMessage.php | 10 ++-- src/Models/BaseOptions.php | 4 +- src/Models/ChannelOptions.php | 6 +- src/Models/CipherParams.php | 2 +- src/Models/ClientOptions.php | 16 ++--- src/Models/DeviceDetails.php | 4 +- src/Models/DevicePushDetails.php | 4 +- src/Models/ErrorInfo.php | 2 +- src/Models/HttpPaginatedResponse.php | 8 +-- src/Models/Message.php | 2 +- src/Models/PaginatedResult.php | 8 +-- src/Models/PresenceMessage.php | 2 +- src/Models/PushChannelSubscription.php | 2 +- src/Models/Stats.php | 20 +++---- src/Models/Stats/ConnectionTypes.php | 8 +-- src/Models/Stats/MessageCount.php | 2 +- src/Models/Stats/MessageTraffic.php | 10 ++-- src/Models/Stats/MessageTypes.php | 8 +-- src/Models/Stats/RequestCount.php | 2 +- src/Models/Stats/ResourceCount.php | 2 +- src/Models/Status/ChannelDetails.php | 2 +- src/Models/TokenDetails.php | 2 +- src/Models/TokenParams.php | 2 +- src/Models/TokenRequest.php | 2 +- src/Models/Untyped.php | 2 +- src/Presence.php | 8 +-- src/Push.php | 2 +- src/PushAdmin.php | 2 +- src/PushChannelSubscriptions.php | 8 +-- src/PushDeviceRegistrations.php | 8 +-- src/Utils/Crypto.php | 6 +- src/Utils/CurlWrapper.php | 2 +- src/Utils/Miscellaneous.php | 2 +- src/Utils/Stringifiable.php | 2 +- tests/AblyRestRequestTest.php | 4 +- tests/AblyRestTest.php | 14 ++--- tests/AppStatsTest.php | 4 +- tests/AuthTest.php | 18 +++--- tests/ChannelHistoryTest.php | 6 +- tests/ChannelIdempotentTest.php | 10 ++-- tests/ChannelMessagesTest.php | 22 +++---- tests/ChannelStatusTest.php | 2 +- tests/ClientIdTest.php | 8 +-- tests/ClientOptionsTest.php | 4 +- tests/CryptoTest.php | 22 +++---- tests/DefaultsTest.php | 4 +- tests/HostCacheTest.php | 4 +- tests/HostTest.php | 4 +- tests/HttpTest.php | 14 ++--- tests/LogTest.php | 4 +- tests/MiscellaneousTest.php | 4 +- tests/PresenceTest.php | 8 +-- tests/PushAdminTest.php | 4 +- tests/PushChannelSubscriptionsTest.php | 8 +-- tests/PushDeviceRegistrationsTest.php | 10 ++-- tests/TokenTest.php | 6 +- tests/TypesTest.php | 80 ++++++++++++------------- tests/UtilsTest.php | 2 +- tests/factories/TestApp.php | 6 +- 71 files changed, 290 insertions(+), 290 deletions(-) diff --git a/src/AblyRest.php b/src/AblyRest.php index 86f6a1c..6fe9d9d 100644 --- a/src/AblyRest.php +++ b/src/AblyRest.php @@ -1,12 +1,12 @@ channels->get() - * @return \Ably\Channel Channel + * @return \Ably\PubSub\Channel Channel */ public function channel( $name, $options = [] ) { return $this->channels->get( $name, $options ); @@ -112,7 +112,7 @@ public function channel( $name, $options = [] ) { * @return array Statistics */ public function stats( $params = [] ) { - return new PaginatedResult( $this, 'Ably\Models\Stats', $cipher = false, 'GET', '/stats', $params ); + return new PaginatedResult( $this, 'Ably\PubSub\Models\Stats', $cipher = false, 'GET', '/stats', $params ); } /** @@ -255,7 +255,7 @@ public function requestInternal( $method, $path, $headers = [], $params = [], $r * @param array $params GET parameters to append to $path * @param array|object $body JSON-encodable structure to send in the body - leave empty for GET requests * @param array $headers HTTP headers to send - * @return \Ably\Models\HttpPaginatedResponse + * @return \Ably\PubSub\Models\HttpPaginatedResponse * @throws AblyRequestException This exception is only thrown for status codes >= 500 */ public function request( $method, $path, $params = [], $body = '', $headers = []) { @@ -267,7 +267,7 @@ public function request( $method, $path, $params = [], $body = '', $headers = [] throw new AblyException( 'GET requests cannot have a JSON body', 400, 40000 ); } - return new HttpPaginatedResponse( $this, 'Ably\Models\Untyped', null, $method, $path, $body, $headers ); // RSC19d + return new HttpPaginatedResponse( $this, 'Ably\PubSub\Models\Untyped', null, $method, $path, $body, $headers ); // RSC19d } // RTN17c diff --git a/src/Auth.php b/src/Auth.php index 8423c6e..caf6ba2 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -1,12 +1,12 @@ authorizeInternal( $tokenParams, $authOptions ); @@ -168,7 +168,7 @@ public function getAuthHeaders() { } /** - * @return \Ably\Models\TokenDetails Token currently in use + * @return \Ably\PubSub\Models\TokenDetails Token currently in use */ public function getTokenDetails() { return $this->tokenDetails; @@ -178,9 +178,9 @@ public function getTokenDetails() { * Request a new token. * @param array|null $tokenParams Requested token parameters * @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones - * @param \Ably\Models\ClientOptions|array $options - * @throws \Ably\Exceptions\AblyException - * @return \Ably\Models\TokenDetails The new token + * @param \Ably\PubSub\Models\ClientOptions|array $options + * @throws \Ably\PubSub\Exceptions\AblyException + * @return \Ably\PubSub\Models\TokenDetails The new token */ public function requestToken( $tokenParams = [], $authOptions = [] ) { // token clientId priority: @@ -207,9 +207,9 @@ public function requestToken( $tokenParams = [], $authOptions = [] ) { $data = $callback($tokenParamsMerged); // returned data can be either a signed TokenRequest or TokenDetails or just a token string - if ( is_a( $data, '\Ably\Models\TokenRequest' ) ) { + if ( is_a( $data, '\Ably\PubSub\Models\TokenRequest' ) ) { $signedTokenRequest = $data; - } else if ( is_a( $data, '\Ably\Models\TokenDetails' ) ) { + } else if ( is_a( $data, '\Ably\PubSub\Models\TokenDetails' ) ) { return $data; } else if ( is_string( $data ) ) { return new TokenDetails( $data ); @@ -281,9 +281,9 @@ public function requestToken( $tokenParams = [], $authOptions = [] ) { * Create a signed token request based on known credentials * and the given token params. This would typically be used if creating * signed requests for submission by another client. - * @param \Ably\Models\TokenParams $tokenParams - * @param \Ably\Models\AuthOptions $authOptions - * @return \Ably\Models\TokenRequest A signed token request + * @param \Ably\PubSub\Models\TokenParams $tokenParams + * @param \Ably\PubSub\Models\AuthOptions $authOptions + * @return \Ably\PubSub\Models\TokenRequest A signed token request */ public function createTokenRequest( $tokenParams = [], $authOptions = [] ) { $tokenClientId = $this->defaultTokenParams->clientId; diff --git a/src/Channel.php b/src/Channel.php index a31a63c..4db9b1a 100644 --- a/src/Channel.php +++ b/src/Channel.php @@ -1,12 +1,12 @@ ably, 'Ably\Models\Message', + return new PaginatedResult( $this->ably, 'Ably\PubSub\Models\Message', $this->getCipherParams(), 'GET', $this->getPath() . '/messages', $params ); diff --git a/src/Channels.php b/src/Channels.php index bdc7eb1..8d6c12d 100644 --- a/src/Channels.php +++ b/src/Channels.php @@ -1,5 +1,5 @@ ably = $ably; $this->model = $model; diff --git a/src/Models/PresenceMessage.php b/src/Models/PresenceMessage.php index 0909620..4bdfc15 100644 --- a/src/Models/PresenceMessage.php +++ b/src/Models/PresenceMessage.php @@ -1,5 +1,5 @@ ably, 'Ably\Models\PresenceMessage', $this->channel->getCipherParams(), 'GET', $this->channel->getPath() . '/presence', $params ); + return new PaginatedResult( $this->ably, 'Ably\PubSub\Models\PresenceMessage', $this->channel->getCipherParams(), 'GET', $this->channel->getPath() . '/presence', $params ); } /** @@ -33,6 +33,6 @@ public function get( $params = [] ) { * @return PaginatedResult */ public function history( $params = [] ) { - return new PaginatedResult( $this->ably, 'Ably\Models\PresenceMessage', $this->channel->getCipherParams(), 'GET', $this->channel->getPath() . '/presence/history', $params ); + return new PaginatedResult( $this->ably, 'Ably\PubSub\Models\PresenceMessage', $this->channel->getCipherParams(), 'GET', $this->channel->getPath() . '/presence/history', $params ); } } \ No newline at end of file diff --git a/src/Push.php b/src/Push.php index 60d5b56..907ff9f 100644 --- a/src/Push.php +++ b/src/Push.php @@ -1,5 +1,5 @@ ably, 'Ably\Models\PushChannelSubscription', + return new PaginatedResult( $this->ably, 'Ably\PubSub\Models\PushChannelSubscription', $cipher = false, 'GET', $path, $params ); } diff --git a/src/PushDeviceRegistrations.php b/src/PushDeviceRegistrations.php index edcb8b3..8328f6c 100644 --- a/src/PushDeviceRegistrations.php +++ b/src/PushDeviceRegistrations.php @@ -1,8 +1,8 @@ ably, 'Ably\Models\DeviceDetails', $cipher = false, 'GET', $path, $params ); + return new PaginatedResult( $this->ably, 'Ably\PubSub\Models\DeviceDetails', $cipher = false, 'GET', $path, $params ); } /** diff --git a/src/Utils/Crypto.php b/src/Utils/Crypto.php index 6b0b6e3..bca32e9 100644 --- a/src/Utils/Crypto.php +++ b/src/Utils/Crypto.php @@ -1,8 +1,8 @@ assertTrue( $this->iterateObjectCheck0( $stats ), 'Expected newly created Stats to have zero values.' ); } diff --git a/tests/AuthTest.php b/tests/AuthTest.php index 0da208c..6d6a0f4 100644 --- a/tests/AuthTest.php +++ b/tests/AuthTest.php @@ -1,13 +1,13 @@ assertTrue( $ably->auth->requestTokenCalled, 'Expected authorize() to call requestToken()' ); $this->assertFalse( $ably->auth->isUsingBasicAuth(), 'Expected token auth to be used' ); - $this->assertInstanceOf( 'Ably\Models\TokenDetails', $tokenOriginal, + $this->assertInstanceOf( 'Ably\PubSub\Models\TokenDetails', $tokenOriginal, 'Expected authorize to return a TokenDetails object' ); $ably->auth->authorize(); diff --git a/tests/ChannelHistoryTest.php b/tests/ChannelHistoryTest.php index 74ebb45..915eaf5 100644 --- a/tests/ChannelHistoryTest.php +++ b/tests/ChannelHistoryTest.php @@ -1,8 +1,8 @@ getPrimaryRestHost(); $messages = []; @@ -512,8 +512,8 @@ public function testEncodingInteroperabilityRawToAbly() { public function testEncodingInteroperabilityAblyToRaw() { $fixture = json_decode( file_get_contents( __DIR__ . '/../ably-common/test-resources/messages-encoding.json' ) ); - $defaultOpts = new \Ably\Models\ClientOptions( self::$defaultOptions ); - $http = new \Ably\Http( $defaultOpts ); // initialize http class for raw requests with default timeouts + $defaultOpts = new \Ably\PubSub\Models\ClientOptions( self::$defaultOptions ); + $http = new \Ably\PubSub\Http( $defaultOpts ); // initialize http class for raw requests with default timeouts $server = 'https://' . $defaultOpts->getPrimaryRestHost(); $messages = []; diff --git a/tests/ChannelStatusTest.php b/tests/ChannelStatusTest.php index 088adc5..2c1ac7b 100644 --- a/tests/ChannelStatusTest.php +++ b/tests/ChannelStatusTest.php @@ -1,6 +1,6 @@ $key ]); - $this->assertInstanceOf( 'Ably\Models\CipherParams', $cipherParams ); + $this->assertInstanceOf( 'Ably\PubSub\Models\CipherParams', $cipherParams ); $this->assertEquals( $key, $cipherParams->key, 'Expected the key to match the provided key' ); $this->assertEquals( 'aes', $cipherParams->algorithm, 'Expected \'aes\' algorithm' ); $this->assertEquals( 128, $cipherParams->keyLength, 'Expected keyLength of 128' ); @@ -54,35 +54,35 @@ public function testGetDefaultParams() { try { Crypto::getDefaultParams( [] ); } catch (\Exception $ex) { - $this->assertInstanceOf( 'Ably\Exceptions\AblyException', $ex, + $this->assertInstanceOf( 'Ably\PubSub\Exceptions\AblyException', $ex, 'Expected to check for key being provided' ); } try { Crypto::getDefaultParams([ 'key' => 'abcd', 'keyLength' => 128 ]); // 32-bit key } catch (\Exception $ex) { - $this->assertInstanceOf( 'Ably\Exceptions\AblyException', $ex, + $this->assertInstanceOf( 'Ably\PubSub\Exceptions\AblyException', $ex, 'Expected to check for key and keyLength mismatch' ); } try { Crypto::getDefaultParams([ 'key' => 'abcd', 'keyLength' => 32 ]); // 32-bit key } catch (\Exception $ex) { - $this->assertInstanceOf( 'Ably\Exceptions\AblyException', $ex, + $this->assertInstanceOf( 'Ably\PubSub\Exceptions\AblyException', $ex, 'Expected to check for an unacceptable key length' ); } try { Crypto::getDefaultParams([ 'key' => Crypto::generateRandomKey(), 'algorithm' => 'fake' ]); } catch (\Exception $ex) { - $this->assertInstanceOf( 'Ably\Exceptions\AblyException', $ex, + $this->assertInstanceOf( 'Ably\PubSub\Exceptions\AblyException', $ex, 'Expected to raise an exception on unknown encryption algorithm' ); } try { Crypto::getDefaultParams([ 'key' => Crypto::generateRandomKey(), 'mode' => 'fake' ]); } catch (\Exception $ex) { - $this->assertInstanceOf( 'Ably\Exceptions\AblyException', $ex, + $this->assertInstanceOf( 'Ably\PubSub\Exceptions\AblyException', $ex, 'Expected to raise an exception on unknown encryption mode' ); } } @@ -102,7 +102,7 @@ public function testNonAESEncryptionSupport() { try { Crypto::getDefaultParams([ 'key' => Crypto::generateRandomKey(), 'algorithm' => 'fake' ]); } catch (\Exception $ex) { - $this->assertInstanceOf( 'Ably\Exceptions\AblyException', $ex, 'Expected to raise an exception on unknown encryption mode' ); + $this->assertInstanceOf( 'Ably\PubSub\Exceptions\AblyException', $ex, 'Expected to raise an exception on unknown encryption mode' ); } } diff --git a/tests/DefaultsTest.php b/tests/DefaultsTest.php index 9923023..82e4d91 100644 --- a/tests/DefaultsTest.php +++ b/tests/DefaultsTest.php @@ -1,7 +1,7 @@ curl = new CurlWrapperMock(); } diff --git a/tests/LogTest.php b/tests/LogTest.php index edef956..8fd6a57 100644 --- a/tests/LogTest.php +++ b/tests/LogTest.php @@ -1,7 +1,7 @@ verifyClassMembers( '\Ably\Models\Message', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\Message', [ 'id', 'clientId', 'connectionId', @@ -59,7 +59,7 @@ public function testMessageType() { } public function testPresenceMessageType() { - $this->verifyClassMembers( '\Ably\Models\PresenceMessage', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\PresenceMessage', [ 'id', 'action', 'clientId', @@ -70,7 +70,7 @@ public function testPresenceMessageType() { 'memberKey' ] ); - $this->verifyClassConstants( '\Ably\Models\PresenceMessage', [ + $this->verifyClassConstants( '\Ably\PubSub\Models\PresenceMessage', [ 'ABSENT' => 0, 'PRESENT' => 1, 'ENTER' => 2, @@ -80,7 +80,7 @@ public function testPresenceMessageType() { } public function testTokenRequestType() { - $this->verifyClassMembers( '\Ably\Models\TokenRequest', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\TokenRequest', [ 'keyName', 'clientId', 'nonce', @@ -91,7 +91,7 @@ public function testTokenRequestType() { } public function testTokenDetailsType() { - $this->verifyClassMembers( '\Ably\Models\TokenDetails', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\TokenDetails', [ 'token', 'expires', 'issued', @@ -101,7 +101,7 @@ public function testTokenDetailsType() { } public function testStatsType() { - $this->verifyClassMembers( '\Ably\Models\Stats', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\Stats', [ 'all', 'apiRequests', 'channels', @@ -117,7 +117,7 @@ public function testStatsType() { } public function testErrorInfoType() { - $this->verifyClassMembers( '\Ably\Models\ErrorInfo', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\ErrorInfo', [ 'code', 'statusCode', 'message', @@ -125,7 +125,7 @@ public function testErrorInfoType() { } public function testClientOptionsType() { - $this->verifyClassMembers( '\Ably\Models\ClientOptions', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\ClientOptions', [ 'clientId', 'logLevel', 'logHandler', @@ -151,7 +151,7 @@ public function testClientOptionsType() { 'idempotentRestPublishing', ] ); - $co = new \Ably\Models\ClientOptions(); + $co = new \Ably\PubSub\Models\ClientOptions(); $this->assertEquals( 4000, $co->httpOpenTimeout ); $this->assertEquals( 10000, $co->httpRequestTimeout ); $this->assertEquals( 3, $co->httpMaxRetryCount ); @@ -162,7 +162,7 @@ public function testClientOptionsType() { public function testClientOptionsIdempotent() { // Test default value - $co = new \Ably\Models\ClientOptions(); + $co = new \Ably\PubSub\Models\ClientOptions(); if (Defaults::API_VERSION <= '1.1') { $this->assertEquals( false, $co->idempotentRestPublishing ); } else { @@ -170,15 +170,15 @@ public function testClientOptionsIdempotent() } // Test explicit value - $co = new \Ably\Models\ClientOptions( array( 'idempotentRestPublishing' => true ) ); + $co = new \Ably\PubSub\Models\ClientOptions( array( 'idempotentRestPublishing' => true ) ); $this->assertEquals( true, $co->idempotentRestPublishing ); - $co = new \Ably\Models\ClientOptions( array( 'idempotentRestPublishing' => false ) ); + $co = new \Ably\PubSub\Models\ClientOptions( array( 'idempotentRestPublishing' => false ) ); $this->assertEquals( false, $co->idempotentRestPublishing ); } public function testAuthOptionsType() { - $this->verifyClassMembers( '\Ably\Models\ClientOptions', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\ClientOptions', [ 'key', 'authCallback', 'authUrl', @@ -190,7 +190,7 @@ public function testAuthOptionsType() { } public function testTokenParamsType() { - $this->verifyClassMembers( '\Ably\Models\TokenParams', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\TokenParams', [ 'ttl', 'capability', 'clientId', @@ -199,13 +199,13 @@ public function testTokenParamsType() { } public function testChannelOptionsType() { - $this->verifyClassMembers( '\Ably\Models\ChannelOptions', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\ChannelOptions', [ 'cipher', ] ); } public function testCipherParamsType() { - $this->verifyClassMembers( '\Ably\Models\CipherParams', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\CipherParams', [ 'algorithm', 'key', 'keyLength', @@ -214,16 +214,16 @@ public function testCipherParamsType() { } public function testStatsTypes() { - $stats = new \Ably\Models\Stats(); + $stats = new \Ably\PubSub\Models\Stats(); $this->verifyObjectTypes( $stats, [ - 'all' => 'Ably\Models\Stats\MessageTypes', - 'inbound' => 'Ably\Models\Stats\MessageTraffic', - 'outbound' => 'Ably\Models\Stats\MessageTraffic', - 'persisted' => 'Ably\Models\Stats\MessageTypes', - 'connections' => 'Ably\Models\Stats\ConnectionTypes', - 'channels' => 'Ably\Models\Stats\ResourceCount', - 'apiRequests' => 'Ably\Models\Stats\RequestCount', - 'tokenRequests' => 'Ably\Models\Stats\RequestCount', + 'all' => 'Ably\PubSub\Models\Stats\MessageTypes', + 'inbound' => 'Ably\PubSub\Models\Stats\MessageTraffic', + 'outbound' => 'Ably\PubSub\Models\Stats\MessageTraffic', + 'persisted' => 'Ably\PubSub\Models\Stats\MessageTypes', + 'connections' => 'Ably\PubSub\Models\Stats\ConnectionTypes', + 'channels' => 'Ably\PubSub\Models\Stats\ResourceCount', + 'apiRequests' => 'Ably\PubSub\Models\Stats\RequestCount', + 'tokenRequests' => 'Ably\PubSub\Models\Stats\RequestCount', 'intervalId' => 'string', 'intervalGranularity' => 'string', 'intervalTime' => 'integer', @@ -231,9 +231,9 @@ public function testStatsTypes() { // verify MessageTypes $this->verifyObjectTypes( $stats->all, [ - 'all' => 'Ably\Models\Stats\MessageCount', - 'messages' => 'Ably\Models\Stats\MessageCount', - 'presence' => 'Ably\Models\Stats\MessageCount', + 'all' => 'Ably\PubSub\Models\Stats\MessageCount', + 'messages' => 'Ably\PubSub\Models\Stats\MessageCount', + 'presence' => 'Ably\PubSub\Models\Stats\MessageCount', ] ); // verify MessageCount @@ -244,17 +244,17 @@ public function testStatsTypes() { // verify MessageTraffic $this->verifyObjectTypes( $stats->inbound, [ - 'all' => 'Ably\Models\Stats\MessageTypes', - 'realtime' => 'Ably\Models\Stats\MessageTypes', - 'rest' => 'Ably\Models\Stats\MessageTypes', - 'webhook' => 'Ably\Models\Stats\MessageTypes', + 'all' => 'Ably\PubSub\Models\Stats\MessageTypes', + 'realtime' => 'Ably\PubSub\Models\Stats\MessageTypes', + 'rest' => 'Ably\PubSub\Models\Stats\MessageTypes', + 'webhook' => 'Ably\PubSub\Models\Stats\MessageTypes', ] ); // verify ConnectionTypes $this->verifyObjectTypes( $stats->connections, [ - 'all' => 'Ably\Models\Stats\ResourceCount', - 'plain' => 'Ably\Models\Stats\ResourceCount', - 'tls' => 'Ably\Models\Stats\ResourceCount', + 'all' => 'Ably\PubSub\Models\Stats\ResourceCount', + 'plain' => 'Ably\PubSub\Models\Stats\ResourceCount', + 'tls' => 'Ably\PubSub\Models\Stats\ResourceCount', ] ); // verify ResourceCount @@ -275,7 +275,7 @@ public function testStatsTypes() { } public function testHttpPaginatedResponseType() { - $this->verifyClassMembers( '\Ably\Models\HttpPaginatedResponse', [ + $this->verifyClassMembers( '\Ably\PubSub\Models\HttpPaginatedResponse', [ 'items', 'statusCode', 'success', diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 4df2901..bccdbf4 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -1,7 +1,7 @@ Date: Wed, 9 Sep 2026 11:00:27 +0100 Subject: [PATCH 05/14] Remove the hand-rolled autoloader, the Heroku demo and its Procfile - ably-loader.php was a hand-rolled PSR-0-ish autoloader for the `Ably\` prefix. Composer is the only supported install path, so the loader is both unmaintained and now wrong (the prefix moved to `Ably\PubSub\`). - demo/ and Procfile were an unmaintained Heroku demo constructing `new \Ably\AblyRest` directly, which is exactly the shape the door now replaces. All three are listed as removals in the migration guide (docs PR). Co-Authored-By: Claude Opus 5 (1M context) --- Procfile | 1 - ably-loader.php | 15 ---- demo/index.php | 212 ------------------------------------------------ 3 files changed, 228 deletions(-) delete mode 100644 Procfile delete mode 100644 ably-loader.php delete mode 100644 demo/index.php diff --git a/Procfile b/Procfile deleted file mode 100644 index 05da7ab..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: vendor/bin/heroku-php-apache2 demo/ \ No newline at end of file diff --git a/ably-loader.php b/ably-loader.php deleted file mode 100644 index 7b6fc6d..0000000 --- a/ably-loader.php +++ /dev/null @@ -1,15 +0,0 @@ - $apiKey, -); - -if ($host) { - $settings['host'] = $host; -} - -// instantiate Ably -$app = new \Ably\AblyRest($settings); -$channel = $app->channel($channelName); - -if (!empty($_POST)) { - // publish a message - $channel->publish( $eventName, array('handle' => $_POST['handle'], 'message' => $_POST['message']) ); - die(); -} - -// get a list of recent messages and render the interface -$messages = $channel->history( array('direction' => 'backwards') )->items; - -?> - - - - - - Simple Chat Demo - - - - - -
-
-

Let's Chat [api_time: time()/1000) ?> | server_time: ]

-
-
-
- - -
- -
-
- -
- - - -
-
-
-
-
    - - - timestamp / 1000); - $day = date($date_format, $timestamp); ?> - -
  • - -
  • - data->handle ?>: data->message ?>
  • - -
-
-
-
-
-
- - - - - - - \ No newline at end of file From 5890766c824f706fb96d8c67f59b8c95c25e9ac1 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:02:21 +0100 Subject: [PATCH 06/14] Make the agent header per-client and rename the SDK identifier The `Ably-Agent` header is what billing reads to classify a connection, so it becomes a per-client declaration rather than process-global state: - ClientOptions gains an `agents` option (array). An entry renders as `name/version`, or as a bare `name` when the version is `null` or `''`. The versionless form is not a fallback for a missing version: it is how the ably-common registry declares flags, and `ably-pubsub-server` is registered that way (ably-common#361). - `ablyAgentHeader()` becomes an instance method reading `$this->options->agents`. `src/AblyRest.php` still sends the header from exactly one place. - The static `$agents` map, `setAblyAgentHeader()` and the deprecated `setLibraryFlavourString()` are removed. A process-global mutable map leaks between client instances, which is the wrong shape for a per-client declaration; the only callers are the two Laravel wrappers, which are being re-released against this package anyway (plan step 27). - The family identifier becomes `ably-pubsub-php`, so even prerelease traffic partitions cleanly from legacy `ably-php/*` traffic. It now lives in a named constant rather than inline in the header builder. - `Defaults::LIB_VERSION` is `2.0.0`, still the only version site. The string/array/ClientOptions normalisation the constructor did inline moves to `ClientOptions::normalizeConstructorArgument()` so the factory door in the next commit accepts exactly the same arguments from the same code rather than a copy of it. The constructor is marked `@internal` pointing at that door. The agent assertions in tests/HttpTest.php still target the old static API and are rewritten two commits later, with the new tests. Co-Authored-By: Claude Opus 5 (1M context) --- src/AblyRest.php | 79 +++++++++++++++--------------------- src/Defaults.php | 2 +- src/Models/ClientOptions.php | 38 +++++++++++++++++ 3 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/AblyRest.php b/src/AblyRest.php index 6fe9d9d..1b8a6ce 100644 --- a/src/AblyRest.php +++ b/src/AblyRest.php @@ -16,15 +16,12 @@ class AblyRest { public $options; + /** - * Map of agents that will be appended to the agent header. - * - * This should only be used by Ably-authored SDKs. - * If you need to use this then you have to add the agent to the agents.json file: - * https://github.com/ably/ably-common/blob/main/protocol/agents.json - * The keys represent agent names and its corresponding values represent agent versions. + * The versioned identifier of this SDK family, sent as the first entry of + * every `Ably-Agent` header. */ - protected static $agents = array(); + const SDK_AGENT_IDENTIFIER = 'ably-pubsub-php'; private function getAcceptHeader() { @@ -34,12 +31,24 @@ private function getAcceptHeader() return 'application/json'; } - static function ablyAgentHeader() + /** + * Renders the value of the `Ably-Agent` request header for this client + * (RSC7d): this SDK, the PHP runtime, then the client's `agents` entries + * in the order they were given. + * + * An entry whose version is `null` or `''` renders as a bare identifier + * with no `/`. That is not a fallback for a missing version, it is how the + * ably-common registry declares flags — `ably-pubsub-server` among them — + * and emitting `ably-pubsub-server/` instead would fail to classify. + * + * @return string + */ + public function ablyAgentHeader() { - $sdkIdentifier = 'ably-php/'.Defaults::LIB_VERSION; + $sdkIdentifier = self::SDK_AGENT_IDENTIFIER.'/'.Defaults::LIB_VERSION; $runtimeIdentifier = 'php/'.Miscellaneous::getNumeric(phpversion()); $agentHeader = $sdkIdentifier.' '.$runtimeIdentifier; - foreach(self::$agents as $agentIdentifier => $agentVersion) { + foreach($this->options->agents as $agentIdentifier => $agentVersion) { $agentHeader.= ' '.$agentIdentifier; if (!empty($agentVersion)) { $agentHeader.= '/'.$agentVersion; @@ -65,19 +74,22 @@ static function ablyAgentHeader() public $push; /** - * Constructor - * @param \Ably\PubSub\Models\ClientOptions|string array with options or a string with app key or token + * Constructor. + * + * @internal Construct clients through the factory door, + * {@see \Ably\PubSub\Server::createHttpClient()}, which is the only + * documented entry point of this package. A client built by calling this + * constructor directly declares no side in its `Ably-Agent` header, and + * so does not qualify for the server exemption from monthly-active-user + * counting. + * + * @param \Ably\PubSub\Models\ClientOptions|array|string $options array with + * options, a ClientOptions instance, or a string with an app key or token */ public function __construct( $options = [] ) { - # convert to options if a single key is provided - if ( is_string( $options ) ) { - if ( strpos( $options, ':' ) === false ) { - $options = [ 'token' => $options ]; - } else { - $options = [ 'key' => $options ]; - } - } + # convert to options if a single key or token string is provided + $options = ClientOptions::normalizeConstructorArgument( $options ); $this->options = new ClientOptions( $options ); @@ -184,7 +196,7 @@ public function requestInternal( $method, $path, $headers = [], $params = [], $r $mergedHeaders = array_merge( [ 'Accept: ' . $this->getAcceptHeader(), 'X-Ably-Version: ' .Defaults::API_VERSION, - 'Ably-Agent: ' .self::ablyAgentHeader(), + 'Ably-Agent: ' .$this->ablyAgentHeader(), ], $headers ); if ( $auth ) { // inject auth headers $mergedHeaders = array_merge( $this->auth->getAuthHeaders(), $mergedHeaders ); @@ -275,29 +287,4 @@ function hasActiveInternetConnection() { $response = $this->http->get(Defaults::$internetCheckUrl); return $response["body"] == Defaults::$internetCheckOk; } - - /** - * @deprecated - * Sets a "flavour string", that is sent in the `Ably-Agent` request header. - * Used for internal statistics. - * For instance setting 'laravel' results in: `Ably-Agent: laravel` - */ - public static function setLibraryFlavourString( $flavour = '' ) { - if (!empty($flavour)) { - self::setAblyAgentHeader($flavour); - } - } - - /** - * @param string $agentName represents agent_identifier - * @param string $agentVersion represents agent_identifier_version (optional) - * @return void - * @throws AblyException - */ - public static function setAblyAgentHeader($agentName, $agentVersion = '' ) { - if (empty($agentName)) { - throw new AblyException("agentName cannot be empty"); - } - self::$agents[$agentName] = $agentVersion; - } } diff --git a/src/Defaults.php b/src/Defaults.php index 2d32087..151178f 100644 --- a/src/Defaults.php +++ b/src/Defaults.php @@ -3,7 +3,7 @@ class Defaults { const API_VERSION = '2'; - const LIB_VERSION = '1.1.12'; + const LIB_VERSION = '2.0.0'; static $restHost = "rest.ably.io"; static $realtimeHost = "realtime.ably.io"; diff --git a/src/Models/ClientOptions.php b/src/Models/ClientOptions.php index 773cf24..6204061 100644 --- a/src/Models/ClientOptions.php +++ b/src/Models/ClientOptions.php @@ -112,6 +112,44 @@ class ClientOptions extends AuthOptions { */ public $authClass = 'Ably\PubSub\Auth'; + /** + * Additional agent entries appended to the `Ably-Agent` request header. + * + * This should only be used by Ably-authored SDKs and wrappers layered on + * top of this package. An identifier used here has to be registered in + * the ably-common agents registry first: + * https://github.com/ably/ably-common/blob/main/protocol/agents.json + * + * Keys are agent identifiers, values are agent versions. A `null` or + * empty-string value renders the identifier as a bare flag carrying no + * version, which is how the registry declares runtime flags such as + * `browser` and `ably-pubsub-server`. + * + * @var array + */ + public $agents = []; + + /** + * Normalises the single argument that the client constructor and the + * factory door both accept into something ClientOptions can be built from. + * + * A bare string is an API key when it contains a colon and a token + * otherwise. An array or an existing ClientOptions instance passes through + * unchanged, so an argument of any other type reaches the constructor and + * raises the constructor's own error rather than a vaguer failure later. + * + * @param \Ably\PubSub\Models\ClientOptions|array|string $options + * @return \Ably\PubSub\Models\ClientOptions|array + */ + public static function normalizeConstructorArgument( $options ) { + if ( is_string( $options ) ) { + return strpos( $options, ':' ) === false + ? [ 'token' => $options ] + : [ 'key' => $options ]; + } + + return $options; + } private function isProductionEnvironment() { return empty($this->environment) || strcasecmp($this->environment, "production") == 0; From 5929677861f722217a939dbb7eaf7d8562e8d5be Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:03:07 +0100 Subject: [PATCH 07/14] Add the factory door Ably\PubSub\Server::createHttpClient() The single entry point of the ably/pubsub-server package. It accepts exactly what the client constructor accepts (an options array, a ClientOptions instance, or a string holding an API key or a token, reusing the same normalisation), stamps the agent entry that declares the server side, and returns the client. Nothing else: it is a door, not a room. `final class` with a private constructor, and the side identifier lives in `SERVER_AGENT_IDENTIFIER` carrying the comment that says why the `-server` suffix must survive any future rename: realtime grants the MAU server exemption on API-key auth by matching an agent entry ending in `-server`. The side entry is merged last, so a caller passing `agents => ['ably-pubsub-server' => 'x']` gets the versionless flag anyway: which side the package declares is the package's to state. Caller entries are otherwise preserved in order. A ClientOptions argument is cloned rather than mutated, so passing one instance to the door twice does not accumulate agents on the caller's object. No realtime door and no device door, per PDR-091b2's PHP row: PHP has no realtime client, so a stub would only invite the question. Co-Authored-By: Claude Opus 5 (1M context) --- src/Server.php | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/Server.php diff --git a/src/Server.php b/src/Server.php new file mode 100644 index 0000000..cdf3b71 --- /dev/null +++ b/src/Server.php @@ -0,0 +1,98 @@ + null ]; + + if ( $options instanceof ClientOptions ) { + $options = clone $options; + $options->agents = array_merge( $options->agents ?: [], $sideAgent ); + + return $options; + } + + if ( is_array( $options ) ) { + $callerAgents = isset( $options['agents'] ) ? $options['agents'] : []; + $options['agents'] = array_merge( $callerAgents, $sideAgent ); + + return $options; + } + + return $options; + } +} From 0df4818a511ef23f83394e1cfae442a07430384b Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:05:00 +0100 Subject: [PATCH 08/14] Assert the agent header and the packaging invariants The Ably-Agent header is what billing reads, so these assertions are exact rather than substring-tolerant. tests/HttpTest.php now checks: - the header a plain client sends, on the new `ably-pubsub-php` identifier, and that a second client renders the same one (no state leaks between instances now that the static map is gone); - wrapper attribution through the `agents` option, replacing the old static-setter assertions; - the door's header against `^ably-pubsub-php/\d+\.\d+\.\d+(\S*)? php/\S+ ably-pubsub-server$`; - that no `ably-pubsub-server/` token is ever sent, for a caller version of `'x'`, `''` and `null`. The registry declares the identifier as a flag and the versioned form does not classify; this is the PHP shape of the `name/undefined` regression ably-js#2297 guards against; - that a bare `new AblyRest(...)` declares no side, so the door is provably the only stamping path; - that caller entries survive and precede the side entry; - that the door accepts an array, a ClientOptions, an API-key string and a token string, and does not mutate a ClientOptions it was handed. tests/PackagingTest.php adds the invariants that otherwise fail silently until a consumer hits them: the composer name, PSR-4 mapping only `Ably\PubSub\`, no file under src/ declaring a namespace outside it (a file missed by the move would collide with the legacy package in a mixed install), `Defaults::LIB_VERSION` matching the top CHANGELOG heading, every class under src/ resolving through the generated autoloader, and the door being final, non-instantiable and still carrying the `-server` suffix. CHANGELOG.md gains the 2.0.0 heading the version-site check reads. The docs PR fills in its body. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 7 ++ tests/HttpTest.php | 170 ++++++++++++++++++++++++++++++++++------ tests/PackagingTest.php | 121 ++++++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 23 deletions(-) create mode 100644 tests/PackagingTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 642231c..1d65299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [2.0.0](https://github.com/ably/ably-pubsub-php/tree/pubsub-server/2.0.0) (unreleased) + +[Full Changelog](https://github.com/ably/ably-pubsub-php/compare/1.1.12...pubsub-server/2.0.0) + +The first release of the `ably/pubsub-server` package. Entries are filled in +before release; see [`UPDATING.md`](./UPDATING.md) for the 1.x migration. + ## [1.1.12](https://github.com/ably/ably-php/tree/1.1.12) (2026-06-23) [Full Changelog](https://github.com/ably/ably-php/compare/1.1.11...1.1.12) diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 18d7732..e40c7f4 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -7,6 +7,8 @@ use Ably\PubSub\Utils\CurlWrapper; use Ably\PubSub\Models\Untyped; use Ably\PubSub\Utils\Miscellaneous; +use Ably\PubSub\Models\ClientOptions; +use Ably\PubSub\Server; require_once __DIR__ . '/factories/TestApp.php'; @@ -42,46 +44,168 @@ public function testVersionHeaderPresence() { $curlParams = $ably->http->getCurlLastParams(); $this->assertContains( 'X-Ably-Version: ' . Defaults::API_VERSION, $curlParams[CURLOPT_HTTPHEADER], 'Expected Ably version header in HTTP request' ); - - AblyRest::setLibraryFlavourString(); } /** - * Verify proper agent header is set as per RSC7d + * Mock options that never reach the network. */ - public function testAblyAgentHeader() { - $opts = [ + private static function mockOptions( $extra = [] ) { + return array_merge( [ 'key' => 'fake.key:totallyFake', 'httpClass' => 'tests\HttpMock', - ]; - $ably = new AblyRest( $opts ); + ], $extra ); + } + + /** + * Makes one request through the given client and returns the value it sent + * in the Ably-Agent request header. + */ + private static function sentAgentHeader( $ably ) { $ably->time(); // make a request $curlParams = $ably->http->getCurlLastParams(); - $expectedAgentHeader = 'ably-php/'.Defaults::LIB_VERSION.' '.'php/'.Miscellaneous::getNumeric(phpversion()); - $this->assertContains( 'Ably-Agent: '. $expectedAgentHeader, $curlParams[CURLOPT_HTTPHEADER], - 'Expected Ably agent header in HTTP request' ); + foreach ( $curlParams[CURLOPT_HTTPHEADER] as $header ) { + if ( strpos( $header, 'Ably-Agent: ' ) === 0 ) { + return substr( $header, strlen( 'Ably-Agent: ' ) ); + } + } - $ably = new AblyRest( $opts ); - $ably->time(); // make a request + return null; + } - $curlParams = $ably->http->getCurlLastParams(); + /** + * The prefix every Ably-Agent header carries: this SDK and the runtime. + */ + private static function expectedPrefix() { + return 'ably-pubsub-php/'.Defaults::LIB_VERSION.' php/'.Miscellaneous::getNumeric( phpversion() ); + } - $this->assertContains( 'Ably-Agent: '. $expectedAgentHeader, $curlParams[CURLOPT_HTTPHEADER], - 'Expected Ably agent header in HTTP request' ); + /** + * Verify proper agent header is set as per RSC7d + */ + public function testAblyAgentHeader() { + $ably = new AblyRest( self::mockOptions() ); - AblyRest::setLibraryFlavourString( 'laravel'); - AblyRest::setAblyAgentHeader('customLib', '2.3.5'); - $ably = new AblyRest( $opts ); - $ably->time(); // make a request + $this->assertSame( self::expectedPrefix(), self::sentAgentHeader( $ably ), + 'Expected Ably agent header in HTTP request' ); - $curlParams = $ably->http->getCurlLastParams(); + // a second client renders the same header: no state leaks between instances + $ably = new AblyRest( self::mockOptions() ); - $expectedAgentHeader = 'ably-php/'.Defaults::LIB_VERSION.' '.'php/'.Miscellaneous::getNumeric(phpversion()).' laravel'.' customLib/2.3.5'; - $this->assertContains( 'Ably-Agent: '. $expectedAgentHeader, $curlParams[CURLOPT_HTTPHEADER], + $this->assertSame( self::expectedPrefix(), self::sentAgentHeader( $ably ), 'Expected Ably agent header in HTTP request' ); + } - AblyRest::setLibraryFlavourString(); + /** + * Wrapper attribution now travels as a client option rather than as + * process-global static state (RSC7d). + */ + public function testAblyAgentHeaderWithAgentsOption() { + $ably = new AblyRest( self::mockOptions( [ + 'agents' => [ 'laravel' => null, 'customLib' => '2.3.5' ], + ] ) ); + + $this->assertSame( self::expectedPrefix().' laravel customLib/2.3.5', self::sentAgentHeader( $ably ), + 'Expected agents option to be rendered in the Ably agent header' ); + } + + /** + * The door declares the server side, and declares it as a versionless flag. + * + * This is what the billing system reads to grant the MAU server exemption, + * so the assertions here are deliberately exact. + */ + public function testDoorDeclaresServerSide() { + $agentHeader = self::sentAgentHeader( Server::createHttpClient( self::mockOptions() ) ); + + $this->assertMatchesRegularExpression( + '/^ably-pubsub-php\/\d+\.\d+\.\d+(\S*)? php\/\S+ ably-pubsub-server$/', + $agentHeader, + 'Expected the door to declare the server side in the Ably agent header' + ); + } + + /** + * A versioned `ably-pubsub-server/` token must never be sent: the + * registry declares the identifier as a flag, and the versioned form does + * not classify. This is the PHP shape of the regression ably-js#2297 + * guards against, where an absent version rendered as `name/undefined`. + * + * @dataProvider sideAgentVersionProvider + */ + public function testSideAgentIsNeverVersioned( $callerVersion ) { + $agentHeader = self::sentAgentHeader( Server::createHttpClient( self::mockOptions( [ + 'agents' => [ Server::SERVER_AGENT_IDENTIFIER => $callerVersion ], + ] ) ) ); + + $this->assertStringContainsString( ' ably-pubsub-server', $agentHeader, + 'Expected the versionless server side flag' ); + $this->assertStringNotContainsString( 'ably-pubsub-server/', $agentHeader, + 'The server side flag must never carry a version' ); + $this->assertSame( self::expectedPrefix().' ably-pubsub-server', $agentHeader, + 'Expected the caller not to be able to override the side entry' ); + } + + public function sideAgentVersionProvider() { + return [ + 'caller supplies a version' => [ 'x' ], + 'caller supplies an empty version' => [ '' ], + 'caller supplies null' => [ null ], + ]; + } + + /** + * The door is the only path that stamps a side. A client built by calling + * the constructor directly declares none. + */ + public function testBareConstructorDeclaresNoSide() { + $agentHeader = self::sentAgentHeader( new AblyRest( self::mockOptions() ) ); + + $this->assertStringNotContainsString( 'ably-pubsub-server', $agentHeader, + 'A directly constructed client must not declare the server side' ); + $this->assertSame( self::expectedPrefix(), $agentHeader ); + } + + /** + * A caller's own agent entries survive the door and precede the side entry. + */ + public function testDoorPreservesCallerAgents() { + $agentHeader = self::sentAgentHeader( Server::createHttpClient( self::mockOptions( [ + 'agents' => [ 'my-sdk' => '1.0' ], + ] ) ) ); + + $this->assertSame( self::expectedPrefix().' my-sdk/1.0 ably-pubsub-server', $agentHeader, + 'Expected caller agents to be preserved and to precede the side entry' ); + } + + /** + * The door accepts everything the constructor accepts. + */ + public function testDoorAcceptsEveryConstructorArgumentForm() { + $expected = self::expectedPrefix().' ably-pubsub-server'; + + // an options array + $fromArray = Server::createHttpClient( self::mockOptions() ); + $this->assertSame( $expected, self::sentAgentHeader( $fromArray ) ); + $this->assertSame( 'fake.key:totallyFake', $fromArray->options->key ); + + // a ClientOptions instance + $clientOptions = new ClientOptions( self::mockOptions() ); + $fromClientOptions = Server::createHttpClient( $clientOptions ); + $this->assertSame( $expected, self::sentAgentHeader( $fromClientOptions ) ); + $this->assertSame( 'fake.key:totallyFake', $fromClientOptions->options->key ); + $this->assertSame( [], $clientOptions->agents, + 'The door must not mutate the ClientOptions instance it was given' ); + + // a bare API key string (contains a colon) + $fromKey = Server::createHttpClient( 'fake.key:totallyFake' ); + $this->assertSame( 'fake.key:totallyFake', $fromKey->options->key ); + $this->assertSame( [ Server::SERVER_AGENT_IDENTIFIER => null ], $fromKey->options->agents ); + + // a bare token string (no colon) + $fromToken = Server::createHttpClient( 'totallyFakeToken' ); + $this->assertSame( 'totallyFakeToken', $fromToken->options->token ); + $this->assertSame( [ Server::SERVER_AGENT_IDENTIFIER => null ], $fromToken->options->agents ); } /** diff --git a/tests/PackagingTest.php b/tests/PackagingTest.php new file mode 100644 index 0000000..dca93eb --- /dev/null +++ b/tests/PackagingTest.php @@ -0,0 +1,121 @@ + relative path => namespace + */ + private static function sourceNamespaces() { + $namespaces = []; + $srcDir = self::$rootDir.'/src'; + + $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $srcDir ) ); + foreach ( $files as $file ) { + if ( $file->isDir() || $file->getExtension() !== 'php' ) { + continue; + } + + $relative = substr( $file->getPathname(), strlen( $srcDir ) + 1 ); + $matched = preg_match( '/^\s*namespace\s+([^;]+);/m', file_get_contents( $file->getPathname() ), $m ); + $namespaces[$relative] = $matched ? trim( $m[1] ) : ''; + } + + return $namespaces; + } + + public function testComposerPackageName() { + $this->assertSame( 'ably/pubsub-server', self::composerJson()['name'], + 'This repository publishes the ably/pubsub-server package' ); + } + + public function testPsr4MapsOnlyThePubSubNamespace() { + $psr4 = self::composerJson()['autoload']['psr-4']; + + $this->assertSame( [ 'Ably\\PubSub\\' => 'src/' ], $psr4, + 'The package must not claim the bare Ably\\ prefix, which the legacy ' + .'ably/ably-php package owns: in a side-by-side install the autoloader ' + .'would resolve Ably\\AblyRest to whichever prefix path is searched first' ); + } + + public function testNoSourceFileDeclaresANamespaceOutsidePubSub() { + $offenders = []; + foreach ( self::sourceNamespaces() as $relative => $namespace ) { + if ( $namespace !== 'Ably\\PubSub' && strpos( $namespace, 'Ably\\PubSub\\' ) !== 0 ) { + $offenders[$relative] = $namespace; + } + } + + $this->assertSame( [], $offenders, + 'Every file under src/ must declare Ably\\PubSub or a child of it; ' + .'a file missed by the namespace move would collide with the legacy package' ); + } + + public function testSourceTreeIsNotEmpty() { + $this->assertNotEmpty( self::sourceNamespaces(), + 'Sanity check: the namespace scan actually found files to check' ); + } + + public function testLibVersionMatchesTheTopChangelogEntry() { + $changelog = file_get_contents( self::$rootDir.'/CHANGELOG.md' ); + + $this->assertSame( 1, preg_match( '/^## \[([^\]]+)\]/m', $changelog, $m ), + 'Expected a "## [version](...)" heading in CHANGELOG.md' ); + $this->assertSame( $m[1], Defaults::LIB_VERSION, + 'Defaults::LIB_VERSION is the only version site and must match the ' + .'top CHANGELOG.md entry; the release pre-flight checks both against the tag' ); + } + + /** + * The PHP analogue of "the wheel contains the files it should": every class + * declared under src/ resolves through Composer's generated autoloader. + */ + public function testEveryClassInSrcIsAutoloadable() { + $unloadable = []; + + foreach ( self::sourceNamespaces() as $relative => $namespace ) { + $className = $namespace.'\\'.basename( $relative, '.php' ); + + if ( !class_exists( $className ) && !interface_exists( $className ) && !trait_exists( $className ) ) { + $unloadable[] = $className; + } + } + + $this->assertSame( [], $unloadable, + 'Every class under src/ must be loadable through the Composer autoloader' ); + } + + public function testTheDoorIsLoadableAndFinal() { + $door = new \ReflectionClass( \Ably\PubSub\Server::class ); + + $this->assertTrue( $door->isFinal(), 'The door must be final' ); + $this->assertFalse( $door->getConstructor()->isPublic(), 'The door must not be instantiable' ); + $this->assertSame( 'ably-pubsub-server', \Ably\PubSub\Server::SERVER_AGENT_IDENTIFIER ); + $this->assertStringEndsWith( '-server', \Ably\PubSub\Server::SERVER_AGENT_IDENTIFIER, + 'The -server suffix is what grants the MAU server exemption' ); + } +} From 336de7323009161770f85a139c5c2b6cc9d15e19 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:05:27 +0100 Subject: [PATCH 09/14] CI: test 8.1-8.5, cover integration/v2, validate strictly - The check matrix follows the new floor: 8.1 through 8.5. 7.2-8.0 are EOL upstream and no longer installable requirements of this package. 8.5 is stable in shivammathur/setup-php, so the top of the matrix is the current release rather than the previous one. - check.yml and features.yml also run on pushes to integration/v2, so the merged state of the split branch is checked and not just each PR into it. Both already run on pull_request regardless of base. - `composer validate` becomes `--strict`, which is what caught the unbound rybakit/msgpack constraint. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/check.yml | 7 ++++--- .github/workflows/features.yml | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index cfdf456..2cbf4f3 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,6 +6,7 @@ on: push: branches: - main + - integration/v2 permissions: {} @@ -18,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: [7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4] + php-version: ['8.1', '8.2', '8.3', '8.4', '8.5'] protocol: [ 'json', 'msgpack' ] ignorePlatformReq: [ '' ] @@ -34,8 +35,8 @@ jobs: php-version: ${{ matrix.php-version }} ini-values: error_reporting=E_ALL - - name: Validate composer.json and composer.lock - run: composer validate + - name: Validate composer.json + run: composer validate --strict - name: Install dependencies env: diff --git a/.github/workflows/features.yml b/.github/workflows/features.yml index 4b8be1e..c9ad975 100644 --- a/.github/workflows/features.yml +++ b/.github/workflows/features.yml @@ -5,6 +5,7 @@ on: push: branches: - main + - integration/v2 permissions: {} From 727256f85107fa96165b4da4c31d0507c93a309e Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:33:21 +0100 Subject: [PATCH 10/14] tests: drop the deprecated curl_close() from the sandbox fixture 1.1.12 removed curl_close() from the SDK for PHP 8.5, but the sandbox test-app factory still called it, so every 8.5 CI job printed a deprecation notice from tests/factories/TestApp.php. The handle is released when it goes out of scope; the floor is 8.1. Co-Authored-By: Claude Fable 5.1 --- tests/factories/TestApp.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/factories/TestApp.php b/tests/factories/TestApp.php index 6fc7c9c..ab104a0 100644 --- a/tests/factories/TestApp.php +++ b/tests/factories/TestApp.php @@ -127,7 +127,9 @@ private function request( $mode, $url, $headers = [], $params = '' ) { var_dump(curl_error($ch)); // Prints curl request error if exists } - curl_close ($ch); + // curl_close() has no effect since PHP 8.0 and is deprecated since 8.5; + // the handle is released when $ch goes out of scope. The floor is 8.1. + unset($ch); if ($this->debugRequests) { var_dump($raw); From ff09ec3ae50817ee227c9cff4a6ecfb56dcf5ede Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:23:50 +0100 Subject: [PATCH 11/14] Rewrite the README for ably/pubsub-server The package name is now the declaration of the side, so the README has to say what "server" means and show the header that carries it: a Package section covers the trusted runtime, the MAU exemption and the wire shape, and states that this is PHP's only Pub/Sub package (no device package, no core to depend on, still REST-only). Install line, badges and quickstart move onto the new package and the door; the supported-platforms table replaces the protocol-v1 deprecation notice with the new 8.1-8.5 floor. Adds a pointer to UPDATING.md, a line telling 1.x users where their branch lives (the legacy Packagist page renders this README after the split), and a note that Packagist is fed from a read-only mirror so issues belong here. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4d2251b..6b10af9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Ably Pub/Sub PHP Header](images/php-SDK-github.png) -[![Latest Stable Version](https://poser.pugx.org/ably/ably-php/v/stable)](https://packagist.org/packages/ably/ably-php) -[![License](https://poser.pugx.org/ably/ably-php/license)](https://github.com/ably/ably-pubsub-php/blob/main/LICENSE) +[![Latest Stable Version](https://poser.pugx.org/ably/pubsub-server/v/stable)](https://packagist.org/packages/ably/pubsub-server) +[![License](https://poser.pugx.org/ably/pubsub-server/license)](https://github.com/ably/ably-pubsub-php/blob/main/LICENSE) --- @@ -26,12 +26,29 @@ Everything you need to get started with Ably: --- +## Package + +This SDK ships as a single package, `ably/pubsub-server`. + +The package name declares where your code runs. A server is a trusted runtime: it typically authenticates with an API key, one that a browser or a mobile app must never hold, and its connections are exempt from monthly-active-user counting. That declaration has to reach Ably rather than only the README, so the package sends it on every request in the `Ably-Agent` header: + +``` +Ably-Agent: ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server +``` + +The trailing `ably-pubsub-server` entry is the part the platform matches on. It is stamped by `Ably\PubSub\Server::createHttpClient()`, so a client constructed any other way declares no side, and will be rejected on accounts that have monthly-active-user pricing enabled. + +This is the only Ably Pub/Sub package for PHP. There is no device package and no separate core package to depend on, because PHP is a server-side language: this SDK is REST-only and there is no PHP realtime client. See the [Ably REST API](#ably-rest-api) note below for realtime options. + +--- + ## Supported platforms Ably aims to support a wide range of platforms. If you experience any compatibility issues, open an issue in the repository or contact [Ably support](https://ably.com/support). -> [!IMPORTANT] -> PHP SDK versions < 1.1.9 will be [deprecated](https://ably.com/docs/platform/deprecate/protocol-v1) from November 1, 2025. +| Platform | Support | +| --- | --- | +| PHP | 8.1, 8.2, 8.3, 8.4, 8.5 | --- @@ -42,6 +59,8 @@ For Laravel applications, consider these framework-integrated alternatives that * **[Ably Pub/Sub PHP Laravel SDK](https://github.com/ably/ably-php-laravel)** - Laravel integration package with clean facade and dependency injection interface. * **[Ably Broadcaster for Laravel](https://github.com/ably/laravel-broadcaster)** - Official Laravel broadcaster for real-time event broadcasting. +Each needs a new major version to run on `ably/pubsub-server`; their current releases depend on `ably/ably-php` 1.x. Those majors ship in the same release window as this package. + --- ## Installation @@ -49,7 +68,7 @@ For Laravel applications, consider these framework-integrated alternatives that To get started with your project, install the package: ```sh -composer require ably/ably-php +composer require ably/pubsub-server ``` --- @@ -59,8 +78,10 @@ composer require ably/ably-php The following code connects to Ably's REST messaging service, gets reference to a channel to receive messages, and publishes a test message to that same channel: ```php -// Initialize Ably REST client -$ably = new AblyRest(['key' => 'your-ably-api-key', 'clientId' => 'me']); +use Ably\PubSub\Server; + +// Initialize the Ably HTTP (REST) client for a server +$ably = Server::createHttpClient(['key' => 'your-ably-api-key', 'clientId' => 'me']); // Get a reference to the 'test-channel' channel $channel = $ably->channel('test-channel'); @@ -69,6 +90,27 @@ $channel = $ably->channel('test-channel'); $channel->publish('test-event', 'hello world'); ``` +`createHttpClient()` accepts everything the 1.x client constructor accepted: an options array, an API key string, a token string, or a `ClientOptions` instance. + +If your own SDK or framework wraps this package, name it so its traffic is attributed to it: + +```php +$ably = Server::createHttpClient([ + 'key' => 'your-ably-api-key', + 'agents' => ['my-framework' => '1.2.3'], +]); +``` + +--- + +## Migrating from `ably/ably-php` 1.x + +`ably/pubsub-server` 2.0.0 supersedes `ably/ably-php`. The client it returns is the same REST client, so for most applications the migration is confined to the `composer require` line, the `use` statements, and the constructor call. [UPDATING.md](./UPDATING.md) has the full mapping table and a before/after example. + +If you are staying on 1.x for now, it is maintained on the `maintenance/1.x` branch of this repository, and receives security and critical-bug fixes only for one year from the 2.0.0 release. + +--- + ## Releases The [CHANGELOG.md](./CHANGELOG.md) contains details of the latest releases for this SDK. You can also view all Ably releases on [changelog.ably.com](https://changelog.ably.com). @@ -79,6 +121,8 @@ The [CHANGELOG.md](./CHANGELOG.md) contains details of the latest releases for t Read the [CONTRIBUTING.md](./CONTRIBUTING.md) guidelines to contribute to Ably. +Development happens in this repository, `ably-pubsub-php`. The Packagist package is published from a read-only distribution mirror, so issues and pull requests belong here. + --- ## Support, feedback, and troubleshooting @@ -87,4 +131,4 @@ For help or technical support, visit the [Ably Support page](https://ably.com/su ### Ably REST API -This SDK currently supports only the [Ably REST API](https://www.ably.com/docs/rest). For realtime capabilities, you can use the [MQTT adapter](https://www.ably.com/docs/mqtt) alongside [Mosquitto PHP](https://github.com/mgdm/Mosquitto-PHP) to implement Ably's Realtime features. \ No newline at end of file +This SDK currently supports only the [Ably REST API](https://www.ably.com/docs/rest). For realtime capabilities, you can use the [MQTT adapter](https://www.ably.com/docs/mqtt) alongside [Mosquitto PHP](https://github.com/mgdm/Mosquitto-PHP) to implement Ably's Realtime features. From 9d82492cad9ed7b919463b92b270ff1918467382 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:23:50 +0100 Subject: [PATCH 12/14] Add UPDATING.md with the 1.x -> 2.0.0 migration guide A machine-applicable mapping table (install line, door, namespace prefix, the agents option replacing the static setters, the removed loader, the PHP floor), a before/after example, what the removals are, and the far longer list of what is unchanged - the returned client is the same REST client. States the forcing function rather than implying a deadline: once MAU pricing is live the 1.x constructor is rejected on MAU accounts, and 1.x gets security and critical fixes for one year from the 2.0.0 release. Carries the same draft banner as ably-ruby's guide, because 091d may still rename AblyRest to HttpClient before GA. Co-Authored-By: Claude Opus 5 (1M context) --- UPDATING.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 UPDATING.md diff --git a/UPDATING.md b/UPDATING.md new file mode 100644 index 0000000..53dbfce --- /dev/null +++ b/UPDATING.md @@ -0,0 +1,86 @@ +# Upgrade / Migration Guide + +## 1.x (`ably/ably-php`) → 2.0.0 (`ably/pubsub-server`) + +> **Status: draft.** The final public API naming is still under review; [PDR-091d](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781) may rename `AblyRest` to `HttpClient` before the 2.0.0 GA release. This section will be finalized before GA. + +Version 2.0.0 ships from a new package, `ably/pubsub-server`, under a new namespace, `Ably\PubSub\`. `ably/ably-php` is superseded: it receives security and critical-bug fixes only for one year from the 2.0.0 release date, and is then end-of-life. + +Under monthly-active-user pricing the platform has to classify every connection as device-side or server-side. The new package declares that automatically, on every request, in the `Ably-Agent` header; the old constructor cannot declare anything. That is the forcing function for this migration: once monthly-active-user pricing is live, `new Ably\AblyRest(...)` from `ably/ably-php` is rejected on accounts where it is enabled. + +The client `Server::createHttpClient()` returns is the same REST client as before. Channels, message publishing, history, presence, authentication, push admin, crypto and every `ClientOptions` key behave exactly as they did in 1.x. For most applications the migration is confined to the `composer require` line, the `use` statements, and the constructor call. + +### Mapping + +| 1.x (`ably/ably-php`) | 2.0 (`ably/pubsub-server`) | +| --- | --- | +| `composer require ably/ably-php` | `composer require ably/pubsub-server` | +| `use Ably\AblyRest;` / `new AblyRest($opts)` | `use Ably\PubSub\Server;` / `Server::createHttpClient($opts)` | +| `use Ably\Models\Message;` (any `Ably\X` type) | `use Ably\PubSub\Models\Message;` (`Ably\PubSub\X`) | +| `AblyRest::setAblyAgentHeader('x', 'v')` | `Server::createHttpClient(['agents' => ['x' => 'v'], …])` | +| `AblyRest::setLibraryFlavourString('x')` | removed — use the `agents` option | +| `require 'ably-loader.php';` | removed — use Composer's autoloader (`vendor/autoload.php`) | +| PHP 7.2 – 8.0 | PHP `^8.1` (tested on 8.1 – 8.5) | +| ⚠️ [091d](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781): `\Ably\AblyRest` type hints | `\Ably\PubSub\HttpClient` (not yet decided) | + +Every class moves namespace and keeps its name, so the rename is mechanical: replace the prefix `Ably\` with `Ably\PubSub\` throughout, including in type hints, `catch` blocks and fully-qualified string class names. + +### Example + +```php +// 1.x +use Ably\AblyRest; + +$ably = new AblyRest(['key' => getenv('ABLY_API_KEY'), 'clientId' => 'me']); +$ably->channel('test-channel')->publish('test-event', 'hello world'); + +// 2.0 +use Ably\PubSub\Server; + +$ably = Server::createHttpClient(['key' => getenv('ABLY_API_KEY'), 'clientId' => 'me']); +$ably->channel('test-channel')->publish('test-event', 'hello world'); +``` + +`createHttpClient()` accepts everything the 1.x constructor accepted: an options array, an API key string, a token string, or a `ClientOptions` instance. A `ClientOptions` instance you pass in is copied rather than mutated. + +### Declaring the side + +Construct through the door. `new Ably\PubSub\AblyRest(...)` still works — the library and its own tests use it — but it declares no side, and will be rejected on monthly-active-user-enabled accounts just as the 1.x constructor is. The door produces: + +``` +Ably-Agent: ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server +``` + +If you are building an SDK or framework on top of this package, name it through the `agents` option instead of the removed static setters. Your entries are preserved, in order, ahead of the side entry: + +```php +$ably = Server::createHttpClient([ + 'key' => getenv('ABLY_API_KEY'), + 'agents' => ['laravel' => '11.0.0', 'laravel-broadcaster' => '1.0.4'], +]); + +// Ably-Agent: ably-pubsub-php/2.0.0 php/8.3.4 laravel/11.0.0 laravel-broadcaster/1.0.4 ably-pubsub-server +``` + +The `agents` option is per-client, unlike the process-global static setters it replaces, so two clients in one process can carry different attribution. + +### Removed in 2.0.0 + +* `AblyRest::setAblyAgentHeader()` and `AblyRest::$agents` — replaced by the per-client `agents` option. +* `AblyRest::setLibraryFlavourString()` — already deprecated in 1.x; replaced by the same option. +* `ably-loader.php`, the hand-rolled autoloader — Composer is the only supported install path. +* The `demo/` Heroku application and its `Procfile`. +* PHP 7.2 – 8.0 support. + +`Auth::authorise()`, the British-spelling alias deprecated in favour of `Auth::authorize()`, is still present in 2.0.0. It may be removed in the 091d pass before GA. + +### Unchanged + +* The REST client and its whole surface: `channel()`, `channels`, publishing, message history, presence and presence history, `auth`, token requests and token issuing, `push` admin, `stats`, `time()`, and crypto. +* Every `ClientOptions` key, and the array / key-string / token-string / `ClientOptions` forms of the constructor argument. +* Message and error semantics, including `AblyException` and its codes. +* Requirements: `ext-json`, `ext-curl`, `ext-openssl`, and `rybakit/msgpack` for the msgpack protocol. + +### Staying on 1.x + +`ably/ably-php` 1.x is maintained on the `maintenance/1.x` branch of this repository. It gets security and critical-bug fixes for one year from the 2.0.0 release date and no new features, then reaches end-of-life. Both packages can be installed side by side during a migration: they declare different namespaces and different PSR-4 prefixes, so their autoloading does not collide. From 389ff12857944b2f7d661a1fa3b45bba499a86a4 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 9 Sep 2026 11:23:51 +0100 Subject: [PATCH 13/14] Fill in the 2.0.0 CHANGELOG entry Body only: the heading and compare link that PR 1 added stay byte for byte, since PackagingTest reads the version out of the heading. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d65299..07ab5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,25 @@ [Full Changelog](https://github.com/ably/ably-pubsub-php/compare/1.1.12...pubsub-server/2.0.0) -The first release of the `ably/pubsub-server` package. Entries are filled in -before release; see [`UPDATING.md`](./UPDATING.md) for the 1.x migration. +The first release of the `ably/pubsub-server` package, superseding +`ably/ably-php`. See [`UPDATING.md`](./UPDATING.md) for the migration guide and +the full mapping table. + +**Breaking changes:** + +- The package is now `ably/pubsub-server`, installed with `composer require ably/pubsub-server`. `ably/ably-php` is superseded and receives security and critical-bug fixes only for one year from this release, from the `maintenance/1.x` branch. +- The namespace is now `Ably\PubSub\`. Every class keeps its name, so `Ably\Models\Message` becomes `Ably\PubSub\Models\Message`. +- Clients are constructed through `Ably\PubSub\Server::createHttpClient()`, which declares the server side on the wire. It accepts everything the constructor accepted: an options array, an API key string, a token string, or a `ClientOptions` instance. A client constructed directly declares no side, and is rejected on accounts with monthly-active-user pricing enabled. +- Removed `AblyRest::setAblyAgentHeader()` and `AblyRest::setLibraryFlavourString()`, replaced by the per-client `agents` client option. +- Removed `ably-loader.php`; Composer's autoloader is the only supported install path. +- Removed the `demo/` Heroku application and its `Procfile`. +- The minimum supported PHP version is now 8.1; the SDK is tested on 8.1 through 8.5. + +**Other changes:** + +- The SDK agent identifier is now `ably-pubsub-php`, and the server side is declared by an `ably-pubsub-server` entry: `Ably-Agent: ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server`. +- Added the `agents` client option (`array`) so an SDK or framework built on this package can attribute its own traffic. +- `rybakit/msgpack` is now constrained to `^0.9.1 || ^1.0`. ## [1.1.12](https://github.com/ably/ably-php/tree/1.1.12) (2026-06-23) From e79c2a94720ab05f2bda471325f35b7d1d2c9b76 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 18 Sep 2026 09:21:13 +0100 Subject: [PATCH 14/14] Rename AblyRest to PubSubHttpClient PDR-091d renames the client class so it states what it is rather than repeating the vendor name. This is the last public-API change before the 2.0.0 integration branch merges, and the only one that touches consumers' type hints and DI bindings. src/AblyRest.php moves to src/PubSubHttpClient.php to preserve the PSR-4 file/class mapping, and the two test classes follow; every use statement, type hint and docblock in src/ and tests/ is updated, so Server::createHttpClient() now returns \Ably\PubSub\PubSubHttpClient. The rest-prefixed ClientOptions keys are deliberately left alone: restHost and idempotentRestPublishing are cross-SDK spec names (RSC11, TO3n), and renaming them would leave PHP the only SDK spelling them differently. CHANGELOG gains a breaking-change entry; UPDATING.md gains a mapping row and loses its draft banner now the naming is settled. plan.md goes too: it sequenced the split work, which this commit completes. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 3 +- UPDATING.md | 12 +- plan.md | 207 ------------------ src/Auth.php | 6 +- src/Channel.php | 4 +- src/Channels.php | 4 +- src/Http.php | 2 +- src/Models/HttpPaginatedResponse.php | 6 +- src/Models/PaginatedResult.php | 4 +- src/Presence.php | 4 +- src/{AblyRest.php => PubSubHttpClient.php} | 10 +- src/Push.php | 4 +- src/PushAdmin.php | 4 +- src/PushChannelSubscriptions.php | 4 +- src/PushDeviceRegistrations.php | 4 +- src/Server.php | 4 +- tests/AppStatsTest.php | 4 +- tests/AuthTest.php | 48 ++-- tests/ChannelHistoryTest.php | 4 +- tests/ChannelIdempotentTest.php | 6 +- tests/ChannelMessagesTest.php | 12 +- tests/ChannelStatusTest.php | 4 +- tests/ClientIdTest.php | 40 ++-- tests/HttpTest.php | 22 +- tests/LogTest.php | 10 +- tests/PresenceTest.php | 4 +- ...st.php => PubSubHttpClientRequestTest.php} | 6 +- ...yRestTest.php => PubSubHttpClientTest.php} | 60 ++--- tests/PushAdminTest.php | 4 +- tests/PushChannelSubscriptionsTest.php | 4 +- tests/PushDeviceRegistrationsTest.php | 4 +- tests/TokenTest.php | 14 +- tests/TypesTest.php | 2 +- tests/factories/TestApp.php | 2 +- 34 files changed, 162 insertions(+), 370 deletions(-) delete mode 100644 plan.md rename src/{AblyRest.php => PubSubHttpClient.php} (98%) rename tests/{AblyRestRequestTest.php => PubSubHttpClientRequestTest.php} (93%) rename tests/{AblyRestTest.php => PubSubHttpClientTest.php} (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ab5bd..f300625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ the full mapping table. **Breaking changes:** - The package is now `ably/pubsub-server`, installed with `composer require ably/pubsub-server`. `ably/ably-php` is superseded and receives security and critical-bug fixes only for one year from this release, from the `maintenance/1.x` branch. -- The namespace is now `Ably\PubSub\`. Every class keeps its name, so `Ably\Models\Message` becomes `Ably\PubSub\Models\Message`. +- The namespace is now `Ably\PubSub\`. Every class other than the client keeps its name, so `Ably\Models\Message` becomes `Ably\PubSub\Models\Message`. +- The client class `Ably\AblyRest` is now `Ably\PubSub\PubSubHttpClient` ([PDR-091d](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781)). Applications that only construct through `Server::createHttpClient()` are unaffected; type hints and DI bindings naming the class must be updated. - Clients are constructed through `Ably\PubSub\Server::createHttpClient()`, which declares the server side on the wire. It accepts everything the constructor accepted: an options array, an API key string, a token string, or a `ClientOptions` instance. A client constructed directly declares no side, and is rejected on accounts with monthly-active-user pricing enabled. - Removed `AblyRest::setAblyAgentHeader()` and `AblyRest::setLibraryFlavourString()`, replaced by the per-client `agents` client option. - Removed `ably-loader.php`; Composer's autoloader is the only supported install path. diff --git a/UPDATING.md b/UPDATING.md index 53dbfce..f669df4 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -2,13 +2,11 @@ ## 1.x (`ably/ably-php`) → 2.0.0 (`ably/pubsub-server`) -> **Status: draft.** The final public API naming is still under review; [PDR-091d](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781) may rename `AblyRest` to `HttpClient` before the 2.0.0 GA release. This section will be finalized before GA. - Version 2.0.0 ships from a new package, `ably/pubsub-server`, under a new namespace, `Ably\PubSub\`. `ably/ably-php` is superseded: it receives security and critical-bug fixes only for one year from the 2.0.0 release date, and is then end-of-life. Under monthly-active-user pricing the platform has to classify every connection as device-side or server-side. The new package declares that automatically, on every request, in the `Ably-Agent` header; the old constructor cannot declare anything. That is the forcing function for this migration: once monthly-active-user pricing is live, `new Ably\AblyRest(...)` from `ably/ably-php` is rejected on accounts where it is enabled. -The client `Server::createHttpClient()` returns is the same REST client as before. Channels, message publishing, history, presence, authentication, push admin, crypto and every `ClientOptions` key behave exactly as they did in 1.x. For most applications the migration is confined to the `composer require` line, the `use` statements, and the constructor call. +The client `Server::createHttpClient()` returns is the same REST client as before, renamed from `AblyRest` to `PubSubHttpClient`. Channels, message publishing, history, presence, authentication, push admin, crypto and every `ClientOptions` key behave exactly as they did in 1.x. For most applications the migration is confined to the `composer require` line, the `use` statements, and the constructor call. ### Mapping @@ -21,9 +19,9 @@ The client `Server::createHttpClient()` returns is the same REST client as befor | `AblyRest::setLibraryFlavourString('x')` | removed — use the `agents` option | | `require 'ably-loader.php';` | removed — use Composer's autoloader (`vendor/autoload.php`) | | PHP 7.2 – 8.0 | PHP `^8.1` (tested on 8.1 – 8.5) | -| ⚠️ [091d](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781): `\Ably\AblyRest` type hints | `\Ably\PubSub\HttpClient` (not yet decided) | +| `\Ably\AblyRest` type hints | `\Ably\PubSub\PubSubHttpClient` ([091d](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781)) | -Every class moves namespace and keeps its name, so the rename is mechanical: replace the prefix `Ably\` with `Ably\PubSub\` throughout, including in type hints, `catch` blocks and fully-qualified string class names. +Every class moves namespace, and `AblyRest` is the only one that also changes name, so the rename is mechanical: replace the prefix `Ably\` with `Ably\PubSub\` throughout — including in type hints, `catch` blocks and fully-qualified string class names — and then `AblyRest` with `PubSubHttpClient`. ### Example @@ -45,7 +43,7 @@ $ably->channel('test-channel')->publish('test-event', 'hello world'); ### Declaring the side -Construct through the door. `new Ably\PubSub\AblyRest(...)` still works — the library and its own tests use it — but it declares no side, and will be rejected on monthly-active-user-enabled accounts just as the 1.x constructor is. The door produces: +Construct through the door. `new Ably\PubSub\PubSubHttpClient(...)` still works — the library and its own tests use it — but it declares no side, and will be rejected on monthly-active-user-enabled accounts just as the 1.x constructor is. The door produces: ``` Ably-Agent: ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server @@ -72,7 +70,7 @@ The `agents` option is per-client, unlike the process-global static setters it r * The `demo/` Heroku application and its `Procfile`. * PHP 7.2 – 8.0 support. -`Auth::authorise()`, the British-spelling alias deprecated in favour of `Auth::authorize()`, is still present in 2.0.0. It may be removed in the 091d pass before GA. +`Auth::authorise()`, the British-spelling alias deprecated in favour of `Auth::authorize()`, is still present in 2.0.0. ### Unchanged diff --git a/plan.md b/plan.md deleted file mode 100644 index aec7352..0000000 --- a/plan.md +++ /dev/null @@ -1,207 +0,0 @@ -# ably-php → ably-pubsub-php: PubSub package split plan - -Execution plan for applying PDR-091b (PubSub package split, major releases) to the PHP SDK. - -**Sources of truth:** -- [PDR-091: SDK naming and MAU classification](https://ably.atlassian.net/wiki/spaces/product/pages/5220106242) — parent, DECIDED IN PRINCIPLE. Device/server split, factory doors, the agent header as the declaration mechanism. Names `ably-php → ably-pubsub-php` explicitly as a rename-in-place repo. -- [PDR-091b companion: PubSub package split (major releases)](https://ably.atlassian.net/wiki/spaces/product/pages/5362810886) — DECIDED. New majors on a new core, not thin wrappers; repo renames; old packages "not touched at all" and EOL after one year. Names the PHP-specific publishing rebind: "Packagist (ably-php) — GitHub webhook/App install tied to the repo; needs re-pointing." -- [PDR-091b2: Per-SDK rollout plan v2](https://ably.atlassian.net/wiki/spaces/product/pages/5348425729) — IN REVIEW. PHP section, verbatim: "Repo: ably-php → ably-pubsub-php. New package: `ably/pubsub-server`. Factory door: `createHttpClient(...)` only." Owner field is blank. **No core package is named for PHP** — the only SDK row without one (see step 1). The agent-identifier convention (per-language family rename + cross-SDK versionless side flags) is Umair's footer comment on this page (2026-09-02). -- [PDR-091c companion: high-level SDKs stay unified](https://ably.atlassian.net/wiki/spaces/product/pages/5363499015) — DECIDED. PHP has two Ably-owned wrapper packages, `ably/ably-php-laravel` and `ably/laravel-broadcaster`; they stay unified, ship a new major depending on the new package, and construct through the door (step 27). -- [PDR-091d companion: public API renaming](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781) — **IN REVIEW** (steps marked ⚠️091d are conditional on it being decided). Names `AblyRest → HttpClient` explicitly. -- [SDK device/server split — step sequence doc](https://docs.google.com/document/d/1r96vOSOft1yT84NxbdpCh0IomTwzIpKskQ-wA5e28M0) — the 12-step order this plan follows. -- Reference implementations: [ably-js#2293](https://github.com/ably/ably-js/pull/2293) (split, MERGED to `integration/v3`), [#2294](https://github.com/ably/ably-js/pull/2294) (UTS per side), [#2296](https://github.com/ably/ably-js/pull/2296) (lockstep release), [#2297](https://github.com/ably/ably-js/pull/2297) (versionless side flags + `ably-pubsub-js` identifier); [ably-ruby#453/#454/#455/#457](https://github.com/ably/ably-ruby/pulls) and `Git/ably-ruby/plan.md`; [ably-python#683/#684/#685/#686/#682](https://github.com/ably/ably-python/pulls) and `Git/ably-python/plan.md` (server-only siblings; Python's `ably_pubsub` namespace decision is the closest analogue to PHP's, step 2). Also ably-dotnet#1333–1335 and ably-java#1232–1235 for the door/agent shape. -- Agent registry: [ably-common#361](https://github.com/ably/ably-common/pull/361) — **OPEN** as of 2026-09-09. Registers `ably-pubsub-server`/`ably-pubsub-device` (versionless flags, `type: runtime`) and `ably-pubsub-php` (versioned `sdk`, `source: https://github.com/ably/ably-php` — update the source to the renamed repo before or after merge). Today's registry has `ably-php` only, plus the wrapper entries `ably-php-laravel` and `laravel-broadcaster`. -- Infra prerequisite: [infrastructure#13005](https://github.com/ably/infrastructure/pull/13005) — **MERGED 2026-09-01**; `ably-sdk-builds-ably-pubsub-php` IAM role exists (the `ably-php` role stays). [infrastructure#13054](https://github.com/ably/infrastructure/pull/13054) added the ID-qualified OIDC subject. -- Already open in this repo: [ably-php#222](https://github.com/ably/ably-php/pull/222) (`pubsub-split/rename-references` → `main`) — `features.yml` repository-name + README licence link. **Merge only in the rename freeze window** (step 17). There is no prior thin-wrapper PR to close (unlike ably-python#681 / ably-js#2291). - -**What PHP ships (per PDR-091b2):** PHP is a **server-only, REST-only** SDK (no realtime client exists; the README points realtime users at the MQTT adapter). One new public Composer package, **`ably/pubsub-server`**, with a single factory door, **`createHttpClient(...)`**. No device package, no realtime door. Repo renamed `ably-php → ably-pubsub-php` (direct rename — Packagist identity is the `vendor/name` in `composer.json`, not the repo URL, so the ably-go/ably-cocoa copy-first exception does not apply). Today's `ably/ably-php` package (currently 1.1.12) enters a 1-year maintenance window, then EOL. - -**Current repo facts the plan relies on (verified 2026-09-09):** -- Single flat package: `composer.json` `name: ably/ably-php`, PSR-4 `Ably\\ → src/`, `tests\\ → tests/`; 3.3k lines in `src/`, 28 test files. No `composer.json` version field — Packagist derives versions from git tags (`1.1.12`, no `v` prefix). **The only version site is `Defaults::LIB_VERSION`** (`src/Defaults.php:6`). -- Requires `php: ^7.2 || ^8.0`; CI matrix tests 7.2–8.4 × JSON/msgpack (`check.yml`). PHP 7.x and 8.0 are EOL upstream. -- Agent string is built in one place, `AblyRest::ablyAgentHeader()` (`src/AblyRest.php:37`): `ably-php/ php/` plus entries from a **static, process-global** `AblyRest::$agents` map set via `AblyRest::setAblyAgentHeader($name, $version)` (and the `@deprecated` `setLibraryFlavourString`). Sent as `Ably-Agent` from `src/AblyRest.php:187`. **There is no per-client `agents` option.** `tests/HttpTest.php:52-85` asserts the exact current shape (RSC7d), including the `laravel customLib/2.3.5` static-append form. -- Both Laravel wrappers rely on the static setter: `ably-php-laravel` calls `AblyRest::setAblyAgentHeader('laravel', $laravelVersion)` then `new AblyRest(config('ably'))`; `laravel-broadcaster` sets `laravel-broadcaster/` and `laravel/` the same way and type-hints `\Ably\AblyRest` in `AblyBroadcaster::__construct`. They require `ably/ably-php: ~1.1.9` and `^1.1` respectively. -- Publishing today is **manual and tag-driven**: bump `LIB_VERSION`, update `CHANGELOG.md`, push tag, create a GitHub release; Packagist auto-updates via the legacy repo webhook `https://packagist.org/api/github?username=ably` (`push` events). There is **no release workflow** and no trusted publishing concept on Packagist. Packagist maintainer account: `ably`. `ably/ably-php` has ~8.7M total / ~387k monthly downloads. -- `features.yml` uploads via `ably/features` with `repository-name` (already `ably-pubsub-php` on PR #222's branch). No docs workflow. -- Non-Composer extras that carry the old identity: `ably-loader.php` (hand-rolled autoloader for `Ably\`), `demo/index.php` + `Procfile` (Heroku demo using `new \Ably\AblyRest`), `ably-common` submodule at the repo root (test fixtures). -- Known `@deprecated` surface today: `Auth::authorise` (→ `authorize`), `AblyRest::setLibraryFlavourString`. No `endpoint` client option exists yet (ADR-119 was never applied to PHP), so `restHost`/`environment` are **not** deprecated here and are not deletion candidates. -- Packagist names: `ably/pubsub-server` (and `ably/pubsub-core`) do not exist. Packagist has **no name reservation**; a package exists once it is submitted against a repository whose default branch `composer.json` carries that name. -- **Packagist mechanics, verified against the composer/packagist and composer/composer sources (2026-09-09):** - - Uniqueness is by **package name only** (`UniquePackageValidator` → `findOneByName`). Two Packagist packages may point at the same repository URL. The name is read from the default branch's `composer.json` at submission (`Package::setRepository` → `getComposerInformation(getRootIdentifier())`). - - **There is no name-based filtering of versions.** Composer's `VcsRepository::preProcess` deliberately overwrites every tag's and branch's `name` with the default branch's name ("this ensures that a package can be renamed in one place and that all old tags will still be installable using that new name"), and Packagist's `Updater` then stamps its own package name on every version. Consequence: **every Packagist package bound to a repository indexes every Composer-valid tag and every branch of that repository, whatever their `composer.json` says.** Tags whose name is not a valid version (e.g. `pubsub-server/2.0.0`) are skipped ("Skipped tag …, invalid tag name"). - - Repository renames are handled: the GitHub webhook resolves a package by URL first, then by the stored GitHub repository ID (`remoteId`), and on an ID match **rewrites the stored URL and schedules a full re-crawl** (`ApiController::findGitHubPackagesByRepository`). The updater also refreshes the URL from the driver. So auto-update does **not** silently stop at a rename; editing the URL by hand is optional (and `PopularPackageSafetyValidator` allows it for a same-ID rename even on a package with >50k downloads). Re-pointing a >50k-download package at a *different* repository is blocked for maintainers and needs Packagist support; a crawl that finds a different repository ID **freezes** the package (`RemoteIdMismatch`). - - Maintainers can delete individual versions (`delete_version`); maintainer-pulled rows stay deleted across re-crawls. Maintainers can mark a package **abandoned with a replacement package**, which Composer prints on every install. - ---- - -## Phase 0 — Decisions and prerequisites (before writing code) - -1. **Packaging shape on Packagist — ONE published package, `ably/pubsub-server`, no `ably/pubsub-core`, published from a read-only distribution mirror.** This is the PHP-specific fork from the pattern, and PDR-091b2 already lists only `ably/pubsub-server` for PHP; record the reasoning in the b2 PHP section so it is a decision rather than an omission: - - **Why no core:** a core buys nothing in PHP — there is no device package to share it with, and PHP cannot re-export types (no `export … from`, only `class_alias`), so a separate core would force consumers to type-hint `Ably\PubSub\Core\Models\Message` from a package whose description says "do not depend on this". The high-level consumers (the two Laravel wrappers, PDR-091c) can depend on `ably/pubsub-server` directly: 091c's reason for depending on core types — not picking a side — does not arise when there is exactly one side. Each extra Composer package is also an extra mirror repo (below). - - **Why a mirror:** Packagist indexes **every** Composer-valid tag and branch of a repository under **every** package bound to that repository, ignoring the `composer.json` name (verified, see repo facts). If `ably/pubsub-server` were registered against this repo, it would serve `1.1.12` (old namespace, old name) as an installable version; and `ably/ably-php` would serve `2.0.0` (new namespace) as its latest — any `ably/ably-php: *` or `>=1.1` consumer would upgrade into a broken install. Neither can be prevented by naming or by the updater; only by maintainers deleting versions by hand after every release. So the two packages **cannot share a repository**, and the legacy package must stay on this repository (its ~8.7M downloads put it behind `PopularPackageSafetyValidator`, so its URL cannot be moved to a different repository without Packagist support, and a repo-ID change would freeze it). Therefore: - - `ably/ably-php` stays bound to this repo (renamed to `ably-pubsub-php`; Packagist follows the rename by repository ID). It keeps indexing `1.x` tags from `maintenance/1.x`. - - `ably/pubsub-server` is bound to a **read-only distribution mirror repo**, e.g. `ably/ably-pubsub-php-dist` (name is the owner's call; it is a build artifact, not a development repo, so PDR-091's "repo names never carry side" does not apply, and its README says "read-only mirror — develop and file issues at ably-pubsub-php"). `release.yml` pushes the release commit and the `2.0.0` tag there (step 15). No subtree split is needed because the package is the whole repo; the mirror is a plain push of `main` at the release commit. - - **Release tags in this repo must not be Composer-valid versions**, or `ably/ably-php` indexes them. Use a namespaced tag, `pubsub-server/2.0.0`, for the GitHub release and CHANGELOG compare links here; the plain `2.0.0` tag exists only on the mirror. Composer skips the namespaced tag as an invalid version name (`VcsRepository::validateTag`). - - Rejected alternatives: register both packages on this repo and prune versions by hand after every release (a window on every release where `composer require ably/ably-php` resolves to 2.x, and ongoing toil); move the legacy package to a mirror of `maintenance/1.x` instead (needs Packagist support because of the download threshold, changes the repo ID, and puts the high-traffic package on the moving part); the ably-go/ably-cocoa copy-under-old-name route (a new repo at `ably/ably-php` would destroy GitHub's redirects and the legacy Packagist entry would resolve to a repo with a different ID and be frozen). - - **Surface this to the programme:** PDR-091b states registry identity "is not tied to the repo URL" for Packagist. Identity is not, but *version discovery* is — every tag of the repo belongs to every package on it — which is why PHP needs the mirror. Add it to the b2 PHP row so no one plans a second Composer package from this repo later. -2. **PHP namespace — recommend `Ably\PubSub\`.** The legacy package owns the PSR-4 prefix `Ably\` → `src/`. Composer installs `ably/ably-php` and `ably/pubsub-server` side by side (a Laravel app mid-migration, or a transitive dependency still on 1.x), and if both declared `Ably\` the autoloader would resolve `Ably\AblyRest` to whichever prefix path is searched first — a silent mismatch, and PDR-091b also says the old package is "not re-exported or reused". So the new package uses a distinct root: implementation and public types under **`Ably\PubSub\`** (`Ably\PubSub\Models\Message`, `Ably\PubSub\Models\ClientOptions`, …), the door at **`Ably\PubSub\Server`**. Rejected: keeping `Ably\` (clobbers the legacy package); `Ably\PubSub\Core\` for the implementation (the types are public surface in PHP whether we like it or not, and "Core" in every consumer's `use` line contradicts "internal"). Record in PDR-091b2. -3. **Factory-door surface** — record it in PDR-091b2 and fill in the blank Owner field. Proposal, matching the agreed door name in PHP idiom: - - `Ably\PubSub\Server::createHttpClient(array|string|ClientOptions $options): \Ably\PubSub\AblyRest` (⚠️091d: returns `HttpClient`). Accepts exactly what the constructor accepts today — an options array, a `ClientOptions`, or a bare key/token string (reuse the constructor's `strpos(':')` disambiguation rather than duplicating it). - - Only door. No `createRealtimeClient` (no realtime client exists) and no device door. Say so explicitly in b2 so nobody expects a stub. - - The client constructor stays public (a `final` static door with a private constructor is not idiomatic for a class other Ably code and tests construct), but is marked `@internal` with a docblock pointing at the door, and the README/docs never show it. Consequence to state in the guide: a bare `new AblyRest(...)` from the new package sends no side flag and will be rejected on MAU accounts once pricing is live. -4. **New major version — `2.0.0`.** Current is 1.1.12. Tags keep the repo's existing no-`v` convention (`2.0.0`, `2.0.0-rc1`). Composer treats `-rc1`/`-beta1` as non-stable, so `composer require ably/pubsub-server` never resolves a prerelease unless `minimum-stability` allows it. Single package, so "lockstep" reduces to one tag and one version site. -5. **PHP floor — recommend `php: ^8.1`** for the new package, CI matrix 8.1–8.5 (8.5 is current; `curl_close` deprecation fix already in 1.1.12). 7.2–8.0 are EOL and Laravel 10+ requires 8.1; a major is the only place to raise this. Keep `rybakit/msgpack`, `ext-json`, `ext-curl`, `ext-openssl`. Modern-PHP cleanups (typed properties, `readonly`, enums) are **not** in scope — this is a repackaging, not a rewrite. Owner's call; if they want to keep 7.x support, that is a one-line change in `composer.json` and the matrix. -6. **⚠️091d — build on current names, rename last.** Phases 2–5 use `AblyRest`/`Channel`/`Presence`; if PDR-091d is approved, the new package ships `HttpClient` (and the owner decides whether `Channel`/`Channels`/`Presence` become `HttpChannel`/`HttpChannels`/`HttpPresence` for cross-SDK consistency — PHP never had a `Rest` prefix on them) and the deprecated surface is deleted rather than ported — as one final mechanical PR (Phase 6, step 18). -7. **Register the agent identifiers.** Both strings PHP will send must be in the ably-common registry before any prerelease ships them: the versionless side flag `ably-pubsub-server` and the versioned family identifier `ably-pubsub-php`. Both are in ably-common#361 (OPEN) — track it to merge; if it stalls, split the PHP entries into their own PR. Ask for the `ably-pubsub-php` `source` to be the renamed URL. The `-server` suffix is load-bearing: realtime grants the MAU server exemption on API-key auth by matching an agent entry ending in `-server`. Then bump the `ably-common` submodule pin in this repo. The wrapper identifiers `laravel`, `laravel-broadcaster`, `ably-php-laravel` are secondary identifiers and keep their names. -8. **Get the MAU pricing release date** from the [MAU pricing release plan (DRAFT, 2026-08-27)](https://ably.atlassian.net/wiki/spaces/PRICE/pages/5315690510) / project hub — it drives the GA target and the forcing-function messaging in the migration guide. -9. **Packagist account and mirror-push credentials.** Confirm who holds the `ably` Packagist account (1Password / SDK team) — it is needed to submit `ably/pubsub-server` against the mirror (step 15c), to pull `dev-main` from the legacy package (step 22), and to mark the legacy package abandoned at EOL (step 26). Confirm the webhook secret on this repo's Packagist hook is that account's API token (it survives the rename; the hook is repo-bound). Create the mirror repo and a credential that lets `release.yml` push to it (a GitHub App installation or a fine-grained PAT scoped to the mirror, stored as a repo secret here — the default `GITHUB_TOKEN` cannot push cross-repo). Set the mirror up with the same Packagist webhook. - -## Phase 1 — Open the integration branch - -10. Create a long-lived **`integration/v2`** branch off `main` (ably-ruby uses `integration/v2` for the same bump; ably-js `integration/v3`, ably-python `integration/v4`). All split work lands there as **stacked PRs**; nothing ships from it until Phase 7. Apply the same branch protection/required checks as `main`. `check.yml` and `features.yml` trigger on `pull_request` regardless of base so PRs into the integration branch get CI for free; add `integration/v2` to their `push:` branch lists so the merged state is also checked. -10b. **The PR stack.** Mirror ably-ruby (#453 → #454; #455/#457 straight to `main`) and ably-python (#683 → #684 → #686; #685/#682 to `main`): each PR is based on the one before it so they review independently and merge in order into `integration/v2`; the workflow/rename PRs go to `main` directly because they must exist on the default branch to be useful. Branch names `pubsub-split/`. - - | # | Branch | Base | Contents | Plan steps | - | --- | --- | --- | --- | --- | - | 1 ([#223](https://github.com/ably/ably-php/pull/223)) | `pubsub-split/restructure` | `integration/v2` | `composer.json` (name, PSR-4 `Ably\\PubSub\\`, php floor), namespace move `Ably\` → `Ably\PubSub\` across `src/` and `tests/`, `Server::createHttpClient` door, per-client `agents` option + family identifier rename + side stamping, agent assertions, packaging-invariant tests, delete `ably-loader.php`/`demo/`/`Procfile` | 11, 12, 13, 14, 14b, 14c | - | 2 ([#225](https://github.com/ably/ably-php/pull/225)) | `pubsub-split/release-tooling` | PR 1's branch | `release.yml` (dispatch, pre-flight, mirror push + `2.0.0` tag, namespaced tag + GitHub release here, Packagist visibility poll), `release-dry-run` job in `check.yml`, `CONTRIBUTING.md` release section, `.gitattributes` export-ignore | 15, 15b, 15c | - | 3 ([#226](https://github.com/ably/ably-php/pull/226)) | `pubsub-split/release-workflow-on-main` | **`main`** | cherry-pick of PR 2's `release.yml` only, so `workflow_dispatch` is registered while the split still lives on `integration/v2`. Pre-flight refuses a ref whose `composer.json` is still `ably/ably-php`, so it is inert on `main`. **Merge precondition: the 1.x maintenance branch (step 24) is cut first** | 15b, 24 | - | 4 ([#222](https://github.com/ably/ably-php/pull/222)) | `pubsub-split/rename-references` | **`main`** | `features.yml` repository-name, README licence link; merged in the rename freeze window, then merged forward into `integration/v2` | 17 | - | 5 ([#224](https://github.com/ably/ably-php/pull/224)) | `pubsub-split/docs` | PR 2's branch | README rewrite, `UPDATING.md` 1.x→2.0 migration section, `CHANGELOG.md` 2.0.0 entry | 19, 20 | - | 6 | `pubsub-split/api-rename` (⚠️091d) | PR 5's branch | `AblyRest`→`HttpClient` etc., deprecated-surface deletions, test/doc updates — opened only once 091d is decided | 18 | - - Rules for the stack: PR 1's namespace move is a mechanical `sed` over ~60 files — keep it in its own commit, separate from the door/agent commits, so reviewers can diff behaviour without the noise (ably-python's "pure move, no behaviour" commit rule). Each PR's description links the plan step it implements. When a lower PR merges into `integration/v2`, retarget the next one. Merge `main` into `integration/v2` after PRs 3 and 4 land. Periodically merge `main` forward so the integration branch never drifts far from released fixes. - -## Phase 2 — Repo restructure: the `ably/pubsub-server` package - -11. **Re-identify the package** — same flat layout, new identity: - ``` - composer.json # name: ably/pubsub-server; description names the door and says "for servers"; - # php ^8.1; autoload psr-4 "Ably\\PubSub\\": "src/"; autoload-dev "tests\\": "tests/" - src/ - Server.php # the door: Ably\PubSub\Server::createHttpClient() - AblyRest.php … # today's src/**, namespace Ably\ → Ably\PubSub\ (⚠️091d renames land in Phase 6) - Defaults.php # LIB_VERSION = '2.0.0' — still the only version site - tests/ # namespace tests\ unchanged; `use Ably\…` → `use Ably\PubSub\…` - ably-common/ # submodule stays (test fixtures) - ``` - - No `core/`/`server/` directory split: with one Composer package it would be cosmetic (Packagist needs `composer.json` at the root), and it would make the `git mv` diff larger for no packaging gain. If step 1's fallback (a published core) is ever taken, the split can be introduced then. - - Delete `ably-loader.php` (hand-rolled autoloader; Composer is the only supported install path and the README already says so), `demo/` and `Procfile` (unmaintained Heroku demo). List them in the migration guide as removed. - - `keywords` gain `pubsub`, `server`; `homepage` stays; add `support.source` pointing at this repo (not the mirror) so `composer info` sends people to the development repo. `composer validate --strict` must pass (add `--strict` to CI — today it is plain `composer validate`). - - Add a `.gitattributes` with `export-ignore` for `tests/`, `ably-common/`, `.github/`, `phpunit.xml`, `plan.md` — Packagist dist archives are GitHub zipballs and honour it, so consumers stop downloading the test suite and submodule. -12. **Implement the door in `src/Server.php`:** - - `Server::createHttpClient($options)`: normalise the argument exactly as the constructor does (string → `['key'=>…]`/`['token'=>…]`, `ClientOptions` instance passed through), merge the side entry into `agents` (step 13), construct and return the client. Nothing else — it is a door, not a room. - - `final class Server` with a private constructor and the single static method; a `SERVER_AGENT_IDENTIFIER = 'ably-pubsub-server'` constant carrying the suffix-is-load-bearing comment copied in spirit from ably-ruby's `server.rb` / ably-python's `server/__init__.py`. - - Docblock: the two-sentence "servers are trusted environments, exempt from MAU counting; this package names that side" statement the siblings use, so the generated API docs say the same thing in every language. -13. **Agent plumbing.** Rework the static header to match the ably-js contract (`packages/shared/side.ts` / `getAgentString`) and the convention in ably-common#361: - - Add an additive **`agents` client option** (`ClientOptions::$agents`, `array`). `ablyAgentHeader()` becomes an instance method reading `$this->options->agents`, rendering each entry as `name/version`, or a bare `name` when the value is `null` or `''` (a flag, like `browser`). Single seam: `src/AblyRest.php:187` is the only place the header is sent. - - **Remove the static `$agents` map, `setAblyAgentHeader()` and the deprecated `setLibraryFlavourString()`.** Process-global mutable state that leaks between client instances is the wrong shape for a per-client declaration, and the only callers (the two Laravel wrappers) are being re-released on this package anyway (step 27). `setAblyAgentHeader` is not currently deprecated, so this is a stated owner decision, not a silent drop — record it in the b2 PHP section and the deletion list (step 18) with this reason. - - Rename the family identifier `ably-php` → **`ably-pubsub-php`** (versioned with `LIB_VERSION`) — on the integration branch, before any prerelease, so even prerelease traffic partitions cleanly from legacy `ably-php/*` traffic. The maintenance branch keeps `ably-php`. - - The door appends `ably-pubsub-server => null`. Target wire shape: `ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server`; with a wrapper: `ably-pubsub-php/2.0.0 php/8.3.4 ably-pubsub-server laravel/11.0.0 laravel-broadcaster/1.0.4`. - - The side entry is applied last and wins any collision on its own identifier — the side is the package's to declare, not the caller's. Caller-supplied `agents` entries are preserved in order. - - Keep `.ably/capabilities.yaml`'s `Agent Identifier: Agents:` (already declared; now true per-client rather than per-process). - -## Phase 3 — Tests and conformance - -14. Rework the test suite to the new namespace (`use Ably\PubSub\…`), keep the matrix green on the new floor (8.1–8.5 × JSON/msgpack), and run the full sandbox suite (`TestApp` against `ABLY_ENV=sandbox`) from the integration branch. PHP has no UTS, so the existing PHPUnit suite is the conformance pass. Update the `features.yml` job only if `ably/features` needs the new layout (it reads `.ably/capabilities.yaml`, which is unchanged). -14b. **Add explicit agent assertions that fail loudly** (this is what billing reads), in `tests/HttpTest.php` via the existing `HttpMock`: - - `Server::createHttpClient(...)`: the `Ably-Agent` request header matches `^ably-pubsub-php/\d+\.\d+\.\d+(\S*)? php/\S+ ably-pubsub-server$`, contains the versionless `ably-pubsub-server` token, and contains **no** `ably-pubsub-server/` form (the `name/undefined` regression ably-js#2297 guards against — in PHP the equivalent trap is `'ably-pubsub-server' => ''` or `null` rendering as `ably-pubsub-server/`). - - A bare `new AblyRest(...)` from the new package declares **no** side (asserts the door is the only stamping path). - - `agents => ['my-sdk' => '1.0']` survives and precedes the side entry; `agents => ['ably-pubsub-server' => 'x']` cannot override the side entry (renders versionless). - - Rewrite the RSC7d assertions at `tests/HttpTest.php:61` and `:80` for the new family identifier and the option-based (not static) wrapper form. -14c. **Add packaging-invariant tests** (PHPUnit, cheap, run on every PR): - - `composer.json` `name` is `ably/pubsub-server`; PSR-4 maps only `Ably\\PubSub\\`; no file under `src/` declares `namespace Ably;` or any namespace outside `Ably\PubSub\` (a missed file in the move would silently collide with the legacy package in a mixed install). - - `Defaults::LIB_VERSION` matches the top `CHANGELOG.md` entry (the release pre-flight checks this against the tag too; this catches it at PR time). - - Every class name in `src/` is loadable through Composer's generated autoloader (`composer dump-autoload -o` then `class_exists` over the map) — the PHP analogue of "the wheel contains the files it should". - -## Phase 4 — Release tooling: auto-publishing - -15. **Add `release.yml`** — PHP has no upload step (Packagist indexes git tags), so "publishing" is: push the release commit and version tag to the mirror, tag and release here, and prove Packagist saw it. `workflow_dispatch` with a required `version` input: - - **Pre-flight before anything is pushed:** version input is valid semver; equals `Defaults::LIB_VERSION`; equals the top `CHANGELOG.md` heading; `composer.json` `name` is `ably/pubsub-server` (refuses the legacy layout — this is what makes PR 3 inert on `main`); `composer validate --strict`; neither `pubsub-server/` here nor `` on the mirror already exists. Run the unit-level tests (mocked HTTP) as part of the pre-flight; the sandbox matrix already ran on the merged PR. - - **Publish to the mirror:** push the dispatched commit to the mirror's `main` and create the annotated tag `` there (this is the only place a Composer-valid `2.x` tag ever exists). Then create the namespaced tag `pubsub-server/` here and a GitHub release on it with the CHANGELOG section as body (`contents: write`). Prereleases (`-rc1`) are GitHub prereleases; Composer treats them as non-stable. - - **Post-publish check:** poll `https://repo.packagist.org/p2/ably/pubsub-server.json` until the version appears, with a bounded timeout; fail loudly if it does not. This is the automated proof that the mirror's Packagist webhook fired and the package resolved. - - **Re-run safety:** each step checks for its own artifact (mirror tag, local tag, GitHub release) and skips if present, so a run that failed part-way is completed by re-running it at the same version. - - **Guard against the one thing that must never happen:** the pre-flight also fails if a Composer-valid tag (`^v?\d+\.\d+\.\d+`) exists in this repo at or above `2.0.0` — that would mean someone tagged a 2.x release here by hand and `ably/ably-php` is now serving it. The `release-dry-run` job checks the same on every PR. -15b. **Make the workflow dispatchable early.** `workflow_dispatch` only registers once the file exists on the **default branch** — and this work merges to `integration/v2` until Phase 7 — so **cherry-pick `release.yml` to `main`** (PR 3), inert there because the pre-flight refuses `ably/ably-php`. Add a `release-dry-run` job to `check.yml` that runs the same pre-flight logic (minus pushes) on every PR so version-site/CHANGELOG drift surfaces continuously. Deliberately test the guardrails from a throwaway branch: mismatched version (aborts, nothing pushed), and a re-run at an existing version (skips to the poll). -15c. **Create the mirror and register `ably/pubsub-server` on Packagist — this can happen before the integration branch merges.** Because the mirror is its own repository, its default branch can carry `name: ably/pubsub-server` from day one: seed it with the first dispatch from `integration/v2` (a `2.0.0-rc1` or a throwaway `0.0.1-alpha1`), submit the mirror URL on packagist.org, confirm the derived name, add the SDK-team maintainers, and confirm the webhook updates it. This decouples the Packagist path from the `main` flip (step 22) and lets the prerelease (step 21) be a real Packagist install. -16. **Rewrite `CONTRIBUTING.md`'s release process** for the new flow (bump `LIB_VERSION` + CHANGELOG in a PR → merge → dispatch `release.yml`; no manual tagging), and state plainly **why no one may ever push a plain `2.x.y` tag to this repository** (the legacy Packagist package would serve it) and that 1.x maintenance tags stay plain because the legacy package depends on them. (The current CONTRIBUTING has no release section at all — it is the five-line fork/branch/PR list.) - -## Phase 5 — Repo rename and publishing rebind - -17. **Rename the repo `ably-php` → `ably-pubsub-php`** (direct rename; GitHub redirects cover clones and web links). The IAM prerequisite (infrastructure#13005) is merged, so this can happen any time — the natural slot is after PRs 1–2 merge to `integration/v2`, coordinated with the programme's cross-SDK rename freeze window (check with Evgenii whether renames are batched; ably-ruby is already renamed), and before Packagist is touched. Announce the freeze window to the SDK team first. In the same pass: - - Merge PR #222 (`features.yml` `repository-name: ably-pubsub-php`, README licence link) immediately after the rename, then merge `main` forward into `integration/v2`. On the integration branch also update README badges/links, `CONTRIBUTING.md`, `.ably/capabilities.yaml` links and `composer.json` `support.source`/`issues` if added. Leave historic `CHANGELOG.md` links alone (redirects cover them). - - **Legacy `ably/ably-php` Packagist entry — verify, do not rebind.** The repo webhook survives the rename and its payload carries the new URL plus the unchanged GitHub repository ID; Packagist falls back to matching by that ID, rewrites the stored URL itself and schedules a full re-crawl (verified in `ApiController::findGitHubPackagesByRepository`). So the PDR-091b line "Packagist … needs re-pointing" is satisfied automatically for PHP. Push a no-op commit to `maintenance/1.x` after the rename and confirm the package page shows the new repository URL and a fresh "last updated". Only if it does not, edit the URL by hand (allowed for a same-ID rename despite the download threshold). Update PR #222's description, which currently tells the reader to expect a stale URL. - - **Never recreate a repo named `ably/ably-php` afterwards.** A new repo under the old name destroys GitHub's redirect for every existing clone and link. PHP is a direct-rename SDK (the maintenance branch lives in the renamed repo), so the old name stays vacant forever. - - **Done when:** clone and web redirects verified (`git ls-remote` on an existing checkout, plus web URLs for the repo, a PR, and a file permalink); CI fully green post-rename (`check.yml` matrix on both `main` and `integration/v2`); `features.yml` runs clean with the new repository-name **and its upload lands** at `sdk.ably.com/builds/ably/ably-pubsub-php/main/…` (proves the new IAM role — this failure mode is otherwise silent); Packagist `ably/ably-php` shows the new repository URL and auto-updated on the no-op push. The mirror is unaffected by the rename. - -## Phase 6 — Public API pass (⚠️091d) — last change on the integration branch - -18. **This is deliberately the final code change before the integration branch merges**, gated on PDR-091d being approved — everything in Phases 2–5 is built and kept green on the current names, then this lands as one mechanical pass on top: - - Rename `Ably\PubSub\AblyRest` → `Ably\PubSub\HttpClient` (the door's return type follows). Owner decides on `Channel`/`Channels`/`Presence` → `HttpChannel`/`HttpChannels`/`HttpPresence` (091d's `RestChannel → HttpChannel` row; PHP's classes carry no prefix today, so this is consistency, not a rename of a wrong name). `PaginatedResult`, `HttpPaginatedResponse`, models and `Push*` classes stay. - - Produce the PHP deprecated-surface deletion list and delete rather than port. Known today: `Auth::authorise` (deprecated since 1.0), `AblyRest::setLibraryFlavourString` (deprecated) — plus the step-13 owner decision to drop the static `setAblyAgentHeader`, and the step-11 deletions (`ably-loader.php`, `demo/`, `Procfile`). Anything already deprecated is a deletion candidate by default; keeping it requires a stated reason. Nothing not currently deprecated is silently dropped. - - Re-run the full Phase 3 pass after the rename. - - Record the full old-name → new-name table in PDR-091b2's PHP section (it feeds the migration guide, step 20, and the Laravel wrappers' type hints, step 27). - - If 091d is **declined**, skip this phase and ship the surface as-is (the door returns `AblyRest`). If it is **still undecided** when the rest of the branch is done, escalate to the programme before merging: renaming later costs another major. - -## Phase 7 — Docs, prerelease, GA - -19. Rewrite `README.md` for the new package: `composer require ably/pubsub-server`; a single door quickstart (`use Ably\PubSub\Server; $ably = Server::createHttpClient(['key' => …]);`); a **Package** section saying what "server" means here (trusted runtime, API-key auth, MAU-exempt, the wire agent shape); the Laravel section pointing at the new wrapper majors; supported PHP versions table on the new floor; a "Migrating from `ably/ably-php` 1.x" section pointing at `UPDATING.md`; badges moved to `ably/pubsub-server` (they resolve once the package exists on Packagist, step 15c). Add one line for 1.x users pointing at `maintenance/1.x`, since the legacy Packagist page renders this README after step 22. Keep the "REST only, MQTT for realtime" note — it is still true. -20. **Migration guide** (new `UPDATING.md`, section "1.x (`ably/ably-php`) → 2.0.0 (`ably/pubsub-server`)"): machine-applicable mapping table — - - | 1.x (`ably/ably-php`) | 2.0 (`ably/pubsub-server`) | - | --- | --- | - | `composer require ably/ably-php` | `composer require ably/pubsub-server` | - | `use Ably\AblyRest;` / `new AblyRest($opts)` | `use Ably\PubSub\Server;` / `Server::createHttpClient($opts)` | - | `use Ably\Models\Message;` (any type) | `use Ably\PubSub\Models\Message;` | - | `AblyRest::setAblyAgentHeader('x', 'v')` | `Server::createHttpClient(['agents' => ['x' => 'v'], …])` | - | `require 'ably-loader.php'` | removed — use Composer's autoloader | - | ⚠️091d: `\Ably\AblyRest` type hints | `\Ably\PubSub\HttpClient` | - - Plus the deleted-API list, the statement that the returned client is the same REST client (channels, history, presence, auth, push admin unchanged), the MAU forcing function, and the EOL date. All samples rewritten onto the new package; the 23 PHP-bearing pages in the `docs` repo (`getting-started/php.mdx`, `getting-started/laravel.mdx`, `api/rest-sdk*.mdx`, auth/*, channels/*, …) and LLM-facing docs are the docs team's cross-SDK pass — hand them the table. -21. **Prerelease `2.0.0-rc1`** via `release.yml` from `integration/v2` — a real Packagist release through the mirror (step 15c), so the consumer-side check is the real path: in a clean container, `composer require ably/pubsub-server:2.0.0-rc1` from Packagist (no VCS repository stanza), construct through the door and assert the `Ably-Agent` value against a sandbox app. Confirm `composer require ably/pubsub-server` with default stability does **not** pick the rc. Install it **alongside** `ably/ably-php:1.1.12` in one project and confirm both autoload without collision (the step-2 rationale). Check the `ably/ably-php` Packagist page afterwards: it must show **no** `2.0.0-rc1` (proves the namespaced-tag rule in step 1 holds). -22. **Merge `integration/v2` to `main`.** New development continues on `main` under the renamed repo. From this moment the legacy `ably/ably-php` Packagist entry indexes `main` as `dev-main` carrying 2.x code (Composer forces the default branch's name onto every version, so this is not a mismatch it can detect), and its package page renders the 2.0 README. Mitigate: a maintainer pulls the `dev-main` version from `ably/ably-php` on packagist.org once (pulled rows stay pulled), and the 2.0 README carries a line pointing 1.x users at `maintenance/1.x`. Stable 1.x versions are unaffected — verify on the page, and in Phase 8 by the trivial 1.x patch release (step 25). -23. **GA release `2.0.0`, coordinated.** Dispatch `release.yml` at `main`; the Packagist poll is the proof. Released in the org-wide coordinated window with the other SDKs (rollout date is set by the programme, keyed to the MAU pricing date — do not GA unilaterally). GitHub release notes (on `pubsub-server/2.0.0` here) point at `UPDATING.md`. - -## Phase 8 — Maintenance window for the old `ably/ably-php` package - -24. Cut a maintenance branch from the last 1.x release (`1.1.12`, which is today's `main` less PR #222; naming TBC org-wide, e.g. `maintenance/1.x`) **before PR 3 (step 10b) merges to `main`**. Nothing on PHP's `main` breaks legacy releases as early as it does for Python (there is no tag-triggered publish to replace), but cutting it first keeps the rule uniform across SDKs and means a 1.x fix never has to be untangled from split work. The `ably/ably-php` package's future security/critical fixes are tagged from this branch only (`1.1.13`, …), by the existing manual process (or a copy of `release.yml` with the pre-flight pinned to `ably/ably-php`, owner's choice). No new features, **no deprecation notice in code, no runtime side-detection** — 091b says the existing packages are "not touched at all"; once MAU pricing is live the old constructor is rejected server-side, which is the intended forcing function. Its agent string stays `ably-php/1.x`, its namespace stays `Ably\`, its `composer.json` name stays `ably/ably-php`. -25. Cut a trivial patch release (`1.1.13`, plain tag) from the maintenance branch **after** steps 17 and 22 and confirm it appears on Packagist under `ably/ably-php` **and not** under `ably/pubsub-server` (the mirror never receives maintenance commits) — this proves the post-rename webhook path (step 17) and the two-repository separation (step 1) with a harmless release rather than at the first real security fix. -26. Publish the support policy: README banner + CHANGELOG entry on the maintenance branch and on `main` stating maintenance-only status, the EOL date (GA date + 1 year), and a link to the migration guide. When the programme issues deprecation notices (PDR-091 deferred decision D3, after MAU is live), mark `ably/ably-php` **abandoned on Packagist with `ably/pubsub-server` as the replacement** — Composer then prints "Package ably/ably-php is abandoned, you should avoid using it. Use ably/pubsub-server instead." on every install, which is the most effective migration nudge PHP has. In ably-common, add the sunset for `ably-php` (all versions) at the EOL date. The Laravel wrappers' 1.x lines EOL on the same date (step 27). - -## Phase 9 — Dependent Ably packages (PDR-091c; outside this repo, but this repo's owner drives them) - -27. **`ably/ably-php-laravel` and `ably/laravel-broadcaster` each ship a new major** depending on `ably/pubsub-server: ^2.0` (091c: unified, no split, dependency change only). Changes in each: replace `AblyRest::setAblyAgentHeader(...)` + `new AblyRest($config)` with `Server::createHttpClient($config + ['agents' => ['laravel' => $laravelVersion, 'laravel-broadcaster' => LIB_VERSION]])`; `laravel-broadcaster` retypes `AblyBroadcaster::__construct(AblyRest $ably, …)` to the new class (⚠️091d: `HttpClient`); READMEs and the `getting-started/laravel.mdx` page follow. Their agent identifiers keep their names (secondary identifiers). Their current majors enter the same 1-year maintenance window. Sequence: open their PRs once PR 1 here is merged and the namespace is fixed; release them in the same GA window, pinned to `2.0.0`, so a Laravel user never sees a wrapper that cannot resolve its dependency. - ---- - -## Cross-cutting checklist - -- [ ] Single-package shape (no `ably/pubsub-core`), the distribution mirror, and the namespaced-tag rule recorded in PDR-091b2; the "Packagist version discovery is repo-bound" finding raised with the programme (step 1) -- [ ] Mirror repo created, push credential in place, Packagist webhook on the mirror (step 9) -- [ ] Namespace `Ably\PubSub\` recorded in PDR-091b2 (step 2) -- [ ] Door signature `Server::createHttpClient(...)`, constructor `@internal`, static-agent removal recorded; Owner field filled (steps 3, 13) -- [ ] Version `2.0.0`, PHP floor `^8.1` (or owner's alternative) decided (steps 4, 5) -- [ ] `ably-pubsub-server` + `ably-pubsub-php` present in ably-common registry with the renamed `source`; submodule bumped (step 7; ably-common#361) -- [ ] MAU release date known; GA window agreed with programme (steps 8, 23) -- [ ] Packagist `ably` account access confirmed (step 9) -- [ ] ⚠️091d outcome tracked; Phase 6 executed, skipped, or escalated accordingly (step 18) -- [ ] Agent assertions in CI fail loudly, incl. the no-`ably-pubsub-server/` regression and the bare-constructor-declares-nothing check (step 14b) -- [ ] `release.yml` on `main` (inert), dry-run job on every PR, guardrails deliberately tested (steps 15, 15b) -- [ ] `ably/pubsub-server` registered on Packagist against the mirror; webhook proven (step 15c) -- [ ] Post-rename: redirects, CI, features upload (IAM), Packagist `ably/ably-php` URL auto-followed and auto-update proven; PR #222 description corrected (step 17) -- [ ] `2.0.0-rc1` proven via a real Packagist install, default-stability check, side-by-side install, and absence from `ably/ably-php` (step 21) -- [ ] `dev-main` pulled from `ably/ably-php` after the `main` flip (step 22) -- [ ] Maintenance branch cut before PR 3 merges; `1.1.13` proves the legacy Packagist path; EOL policy published; registry sunset added (steps 24–26) -- [ ] Laravel wrapper majors opened and released in the GA window (step 27) - -## Sequencing notes / risks - -- **Packagist is the PHP-specific trap, and it is about tags, not names.** Every package bound to a repository indexes every Composer-valid tag of that repository (Composer rewrites each version's name to the default branch's; Packagist stamps its own). So `ably/ably-php` and `ably/pubsub-server` cannot share this repository, the new package ships from a distribution mirror, and **a plain `2.x.y` tag must never be pushed here** — the `release.yml` pre-flight and the dry-run job refuse if one exists. The only residual leak is `dev-main` of the legacy package after the `main` flip, handled by a one-time maintainer pull (step 22). -- **The rename itself is benign for Packagist.** Auto-update follows the repository ID and self-corrects the URL; PR #222's warning about stale URLs is over-cautious and its description should be corrected. The rename still needs the freeze-window choreography for the features upload (IAM role), and a no-op push to prove the webhook path (step 17). -- **The mirror is one more thing to own:** a push credential, a webhook, a README banner, and the rule that issues and PRs are never accepted there. Put all four in `CONTRIBUTING.md`. -- **The agent-registry PR (step 7) is the only step worth doing immediately**; everything else flows through the integration branch. The `-server` suffix must never be changed without preserving the suffix (billing classifies by it) — the comment on the constant says so. -- **Namespace clobbering.** The new package must not declare anything under bare `Ably\`; the packaging test (14c) asserts it at PR time and the side-by-side install (21) proves it against real Composer resolution. -- **Static agent state goes away.** Both Laravel wrappers break on the new package until their own majors ship (step 27) — this is intended (they are being re-released regardless), but it means the wrapper PRs must be open before the PHP GA, not after. -- **PHP has one version site and one package**, so the lockstep pre-flight is small; the value of `release.yml` here is auto-publishing (PDR-091b's forcing function), the mirror push that no human should do by hand, and the Packagist visibility proof, not lockstep. -- **091d is the only rename that touches consumers' type hints** (`AblyRest` in `laravel-broadcaster`'s constructor and in every customer's DI container). Keep Phase 6 last so the wrapper PRs can be opened against a stable namespace and only the class name changes if 091d lands. -- **No realtime, no UTS, no device package**: PHP is the smallest split in the programme. The risk is not code volume, it is the registry/rename choreography above and getting the Laravel wrappers out in the same window. diff --git a/src/Auth.php b/src/Auth.php index caf6ba2..1584698 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -9,7 +9,7 @@ use Ably\PubSub\Models\TokenRequest; /** - * Provides authentification methods for AblyRest instances + * Provides authentification methods for PubSubHttpClient instances * @property-read string|null $clientId ClientId currently in use. Null if not * authenticated yet or when using anonymous auth. */ @@ -23,7 +23,7 @@ class Auth { protected $ably; const TOKEN_EXPIRY_MARGIN = 15000; // a token is considered expired a bit earlier to prevent race conditions - public function __construct( AblyRest $ably, ClientOptions $options ) { + public function __construct( PubSubHttpClient $ably, ClientOptions $options ) { $this->defaultAuthOptions = new AuthOptions($options); $this->defaultTokenParams = $options->defaultTokenParams; $this->ably = $ably; @@ -31,7 +31,7 @@ public function __construct( AblyRest $ably, ClientOptions $options ) { $this->basicAuth = empty( $this->defaultAuthOptions->useTokenAuth ) && $this->defaultAuthOptions->key; if ( $this->defaultAuthOptions->key && $this->defaultAuthOptions->clientId == '*' ) { - throw new AblyException ('Instantiating AblyRest with a wildcard clientId (`*`) not allowed.', 40012, 400); + throw new AblyException ('Instantiating PubSubHttpClient with a wildcard clientId (`*`) not allowed.', 40012, 400); } // Basic authentication diff --git a/src/Channel.php b/src/Channel.php index 4db9b1a..f089ca8 100644 --- a/src/Channel.php +++ b/src/Channel.php @@ -29,12 +29,12 @@ class Channel { /** * Constructor - * @param AblyRest $ably Ably API instance + * @param PubSubHttpClient $ably Ably API instance * @param string $name Channel's name * @param ChannelOptions|array|null $options Channel options (for encrypted channels) * @throws AblyException */ - public function __construct( AblyRest $ably, $name, $options = [] ) { + public function __construct( PubSubHttpClient $ably, $name, $options = [] ) { $this->ably = $ably; $this->name = $name; $this->channelPath = "/channels/" . urlencode( $name ); diff --git a/src/Channels.php b/src/Channels.php index 8d6c12d..55bb092 100644 --- a/src/Channels.php +++ b/src/Channels.php @@ -8,9 +8,9 @@ class Channels { /** * Constructor - * @param AblyRest $ably Ably API instance + * @param PubSubHttpClient $ably Ably API instance */ - public function __construct( AblyRest $ably ) { + public function __construct( PubSubHttpClient $ably ) { $this->ably = $ably; } diff --git a/src/Http.php b/src/Http.php index fba81ae..240404f 100644 --- a/src/Http.php +++ b/src/Http.php @@ -1,7 +1,7 @@ ably = $ably; $this->model = $model; diff --git a/src/Presence.php b/src/Presence.php index c62e232..867ec3d 100644 --- a/src/Presence.php +++ b/src/Presence.php @@ -10,10 +10,10 @@ class Presence { /** * Constructor - * @param AblyRest $ably Ably API instance + * @param PubSubHttpClient $ably Ably API instance * @param Channel $channel Associated channel */ - public function __construct( AblyRest $ably, Channel $channel ) { + public function __construct( PubSubHttpClient $ably, Channel $channel ) { $this->ably = $ably; $this->channel = $channel; } diff --git a/src/AblyRest.php b/src/PubSubHttpClient.php similarity index 98% rename from src/AblyRest.php rename to src/PubSubHttpClient.php index 1b8a6ce..78560bd 100644 --- a/src/AblyRest.php +++ b/src/PubSubHttpClient.php @@ -13,7 +13,7 @@ /** * Ably REST client */ -class AblyRest { +class PubSubHttpClient { public $options; @@ -138,7 +138,7 @@ public function time() { /** * Does a GET request, automatically injecting auth headers and handling fallback on server failure - * @see AblyRest::request() + * @see PubSubHttpClient::request() */ public function get( $path, $headers = [], $params = [], $returnHeaders = false, $auth = true ) { return $this->requestInternal( 'GET', $path, $headers, $params, $returnHeaders, $auth ); @@ -146,7 +146,7 @@ public function get( $path, $headers = [], $params = [], $returnHeaders = false, /** * Does a POST request, automatically injecting auth headers and handling fallback on server failure - * @see AblyRest::request() + * @see PubSubHttpClient::request() */ public function post( $path, $headers = [], $params = [], $returnHeaders = false, $auth = true ) { return $this->requestInternal( 'POST', $path, $headers, $params, $returnHeaders, $auth ); @@ -154,7 +154,7 @@ public function post( $path, $headers = [], $params = [], $returnHeaders = false /** * Does a PUT request, automatically injecting auth headers and handling fallback on server failure - * @see AblyRest::request() + * @see PubSubHttpClient::request() */ public function put( $path, $headers = [], $params = [], $returnHeaders = false, $auth = true ) { return $this->requestInternal( 'PUT', $path, $headers, $params, $returnHeaders, $auth ); @@ -162,7 +162,7 @@ public function put( $path, $headers = [], $params = [], $returnHeaders = false, /** * Does a DELETE request, automatically injecting auth headers and handling fallback on server failure - * @see AblyRest::request() + * @see PubSubHttpClient::request() */ public function delete( $path, $headers = [], $params = [], $returnHeaders = false, $auth = true ) { return $this->requestInternal( 'DELETE', $path, $headers, $params, $returnHeaders, $auth ); diff --git a/src/Push.php b/src/Push.php index 907ff9f..caed260 100644 --- a/src/Push.php +++ b/src/Push.php @@ -8,9 +8,9 @@ class Push { /** * Constructor - * @param AblyRest $ably Ably API instance + * @param PubSubHttpClient $ably Ably API instance */ - public function __construct( AblyRest $ably ) { + public function __construct( PubSubHttpClient $ably ) { $this->ably = $ably; $this->admin = new PushAdmin( $ably ); } diff --git a/src/PushAdmin.php b/src/PushAdmin.php index 7f6a5bc..de04ff5 100644 --- a/src/PushAdmin.php +++ b/src/PushAdmin.php @@ -9,9 +9,9 @@ class PushAdmin { /** * Constructor - * @param AblyRest $ably Ably API instance + * @param PubSubHttpClient $ably Ably API instance */ - public function __construct( AblyRest $ably ) { + public function __construct( PubSubHttpClient $ably ) { $this->ably = $ably; $this->deviceRegistrations = new PushDeviceRegistrations( $ably ); $this->channelSubscriptions = new PushChannelSubscriptions ( $ably ); diff --git a/src/PushChannelSubscriptions.php b/src/PushChannelSubscriptions.php index cee026e..1f0531b 100644 --- a/src/PushChannelSubscriptions.php +++ b/src/PushChannelSubscriptions.php @@ -10,9 +10,9 @@ class PushChannelSubscriptions { /** * Constructor - * @param AblyRest $ably Ably API instance + * @param PubSubHttpClient $ably Ably API instance */ - public function __construct( AblyRest $ably ) { + public function __construct( PubSubHttpClient $ably ) { $this->ably = $ably; } diff --git a/src/PushDeviceRegistrations.php b/src/PushDeviceRegistrations.php index 8328f6c..e478155 100644 --- a/src/PushDeviceRegistrations.php +++ b/src/PushDeviceRegistrations.php @@ -10,9 +10,9 @@ class PushDeviceRegistrations { /** * Constructor - * @param AblyRest $ably Ably API instance + * @param PubSubHttpClient $ably Ably API instance */ - public function __construct( AblyRest $ably ) { + public function __construct( PubSubHttpClient $ably ) { $this->ably = $ably; } diff --git a/src/Server.php b/src/Server.php index cdf3b71..b425e55 100644 --- a/src/Server.php +++ b/src/Server.php @@ -49,10 +49,10 @@ private function __construct() {} * ClientOptions instance, or a string holding an API key or a token. * * @param \Ably\PubSub\Models\ClientOptions|array|string $options - * @return \Ably\PubSub\AblyRest + * @return \Ably\PubSub\PubSubHttpClient */ public static function createHttpClient( $options = [] ) { - return new AblyRest( self::withSideAgent( $options ) ); + return new PubSubHttpClient( self::withSideAgent( $options ) ); } /** diff --git a/tests/AppStatsTest.php b/tests/AppStatsTest.php index a12dac7..475a395 100644 --- a/tests/AppStatsTest.php +++ b/tests/AppStatsTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); diff --git a/tests/AuthTest.php b/tests/AuthTest.php index 6d6a0f4..c992597 100644 --- a/tests/AuthTest.php +++ b/tests/AuthTest.php @@ -1,6 +1,6 @@ expectException(AblyException::class); $this->expectExceptionCode(40103); - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'tls' => false, ] ) ); @@ -48,21 +48,21 @@ public function testNoAuthParams() { $this->expectException(AblyException::class); $this->expectExceptionCode(40103); - $ably = new AblyRest( ); + $ably = new PubSubHttpClient( ); } /** * Init library with a token */ public function testAuthWithToken() { - $ably_for_token = new AblyRest( array_merge( self::$defaultOptions, [ + $ably_for_token = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $tokenDetails = $ably_for_token->auth->requestToken(); $this->assertNotNull($tokenDetails->token, 'Expected token id' ); - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $tokenDetails, ] ) ); @@ -75,7 +75,7 @@ public function testAuthWithToken() { * Init library with a key, force use of token with useTokenAuth */ public function testAuthWithKeyForceToken() { - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'key' => 'fake.key:totallyFake', 'useTokenAuth' => true, ] ); @@ -90,7 +90,7 @@ public function testAuthEmptyForceToken() { $this->expectException(AblyException::class); $this->expectExceptionCode(40103); - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'useTokenAuth' => true, ] ); } @@ -99,14 +99,14 @@ public function testAuthEmptyForceToken() { * Verify than token auth works without TLS */ public function testTokenWithoutTLS() { - $ably_for_token = new AblyRest( array_merge( self::$defaultOptions, [ + $ably_for_token = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $tokenDetails = $ably_for_token->auth->requestToken(); $this->assertNotNull($tokenDetails->token, 'Expected token id' ); - $ablyInsecure = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyInsecure = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $tokenDetails, 'tls' => false, ] ) ); @@ -122,7 +122,7 @@ public function testTokenWithoutTLS() { public function testTokenRequestWithAuthCallbackReturningSignedRequest() { $callbackCalled = false; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authCallback' => function( $tokenParams ) use( &$callbackCalled ) { $callbackCalled = true; @@ -149,7 +149,7 @@ public function testTokenRequestWithAuthCallbackReturningSignedRequest() { public function testTokenRequestWithAuthCallbackReturningTokenDetails() { $callbackCalled = false; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authCallback' => function( $tokenParams ) use( &$callbackCalled ) { $callbackCalled = true; @@ -175,7 +175,7 @@ public function testTokenRequestWithAuthCallbackReturningTokenDetails() { public function testTokenRequestWithAuthCallbackReturningTokenString() { $callbackCalled = false; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authCallback' => function( $tokenParams ) use( &$callbackCalled ) { $callbackCalled = true; @@ -197,7 +197,7 @@ public function testTokenRequestWithAuthCallbackReturningTokenString() { public function testTokenRequestWithAuthUrlReturningSignedRequest() { $method = 'POST'; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authUrl' => 'https://TEST/tokenRequest', 'httpClass' => 'authTest\HttpMock', ] ) ); @@ -212,7 +212,7 @@ public function testTokenRequestWithAuthUrlReturningSignedRequest() { * Init library with an authUrl that returns TokenDetails */ public function testTokenRequestWithAuthUrlReturningTokenDetails() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authUrl' => 'https://TEST/tokenDetails', 'httpClass' => 'authTest\HttpMock', ] ) ); @@ -227,7 +227,7 @@ public function testTokenRequestWithAuthUrlReturningTokenDetails() { * Init library with an authUrl that returns a token string */ public function testTokenRequestWithAuthUrlReturningTokenString() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authUrl' => 'https://TEST/tokenString', 'httpClass' => 'authTest\HttpMock', ] ) ); @@ -250,7 +250,7 @@ public function testTokenRequestWithAuthUrlParams() { $expectedAuthParams = [ 'param1' => 'value1', 'test' => 1, 'ttl' => 360000 ]; $method = 'POST'; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authUrl' => 'https://TEST/tokenRequest', 'authHeaders' => $headers, 'authParams' => $authParams, @@ -285,7 +285,7 @@ public function testTokenRequestWithAuthUrlParams() { * and checks if ttl can be left blank */ public function testCreateTokenRequestValidity() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); @@ -298,7 +298,7 @@ public function testCreateTokenRequestValidity() { $timestamp = $ably->time(); - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'authCallback' => function( $tokenParams ) use( $tokenRequest ) { return $tokenRequest; }, @@ -327,7 +327,7 @@ private function stripTokenRequestVariableParams($tokenRequest) { * Verify that createTokenRequest() supports tokenparams, authparams and overrides values correctly */ public function testCreateTokenRequestParams() { - $ablyKey = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyKey = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'httpClass' => 'authTest\HttpMock', 'clientId' => 'libClientId', @@ -413,7 +413,7 @@ public function testCreateTokenRequestParams() { * keeps using the same token, and renews it when forced */ public function testAuthorize() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'authClass' => 'authTest\AuthMock' ] ) ); @@ -440,7 +440,7 @@ public function testAuthorize() { * Verify that all the parameters are supported and saved as defaults */ public function testAuthorizeParams() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'authClass' => 'authTest\AuthMock' ] ) ); @@ -516,7 +516,7 @@ public function testAuthorizeParams() { * Verify that authorize() stores the provided parameters and uses them as defaults from then on */ public function testAuthorizeRememberDefaults() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'clientId' => 'originalClientId', ] ) ); @@ -541,7 +541,7 @@ public function testAuthorizeRememberDefaults() { */ public function testHTTPHeadersKey() { $fakeKey = 'fake.key:totallyFake'; - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'key' => $fakeKey, 'httpClass' => 'authTest\HttpMock', ] ); @@ -556,7 +556,7 @@ public function testHTTPHeadersKey() { */ public function testHTTPHeadersToken() { $fakeToken = 'fakeToken'; - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'token' => $fakeToken, 'httpClass' => 'authTest\HttpMock', ] ); diff --git a/tests/ChannelHistoryTest.php b/tests/ChannelHistoryTest.php index 915eaf5..76f21bf 100644 --- a/tests/ChannelHistoryTest.php +++ b/tests/ChannelHistoryTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); } diff --git a/tests/ChannelIdempotentTest.php b/tests/ChannelIdempotentTest.php index 08367c9..a8e23ee 100644 --- a/tests/ChannelIdempotentTest.php +++ b/tests/ChannelIdempotentTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'idempotentRestPublishing' => true, ] ) ); @@ -163,7 +163,7 @@ public function testIdempotentMixedIds() { * RSL1k4 */ public function testIdempotentLibraryGeneratedPublish() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'idempotentRestPublishing' => true, 'httpClass' => 'tests\HttpMockIdempotent', diff --git a/tests/ChannelMessagesTest.php b/tests/ChannelMessagesTest.php index 3c253a4..638ec98 100644 --- a/tests/ChannelMessagesTest.php +++ b/tests/ChannelMessagesTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); } @@ -192,7 +192,7 @@ public function testMessageArraySingleRequest() { $messages[] = $msg; } - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'httpClass' => 'tests\HttpMockMsgCounter', ] ) ); @@ -327,7 +327,7 @@ public function testPublishExtras() { public function testEncryptedMessageUnencryptedHistory() { $errorLogged = false; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'logHandler' => function( $level, $args ) use ( &$errorLogged ) { if ( $level == Log::ERROR ) { @@ -378,7 +378,7 @@ public function testUnencryptedMessageEncryptedHistory() { public function testEncryptionKeyMismatch() { $errorLogged = false; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'logHandler' => function( $level, $args ) use ( &$errorLogged ) { if ( $level == Log::ERROR ) { @@ -552,7 +552,7 @@ public function testEncodingInteroperabilityAblyToRaw() { * Test if null name and data elements are allowed when publishing messages */ public function testNullData() { - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'httpClass' => 'tests\HttpSaveWrapper', ] ) ); diff --git a/tests/ChannelStatusTest.php b/tests/ChannelStatusTest.php index 2c1ac7b..178f364 100644 --- a/tests/ChannelStatusTest.php +++ b/tests/ChannelStatusTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest(array_merge(self::$defaultOptions, [ + self::$ably = new PubSubHttpClient(array_merge(self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ])); } diff --git a/tests/ClientIdTest.php b/tests/ClientIdTest.php index 533dde6..7f70f1f 100644 --- a/tests/ClientIdTest.php +++ b/tests/ClientIdTest.php @@ -1,6 +1,6 @@ self::$testApp->getAppKeyDefault()->string, 'clientId' => 'testClientId', ] ) ); @@ -48,7 +48,7 @@ public function testInitWithWildcardClientId() { $this->expectException(AblyException::class); $this->expectExceptionCode(40012); - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'clientId' => '*', ] ) ); @@ -59,7 +59,7 @@ public function testInitWithWildcardClientId() { */ public function testGetClientIdNull() { // no clientId provided anywhere, should be null - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); @@ -69,7 +69,7 @@ public function testGetClientIdNull() { $this->assertNull( $ably->auth->clientId, 'Expected clientId to be null' ); // test not yet authorised lib without a clientId specified on ClientOptions - $ablyImplicitCId = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyImplicitCId = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'defaultTokenParams' => new TokenParams( [ 'clientId' => 'testClientId', @@ -89,13 +89,13 @@ public function testGetClientIdNull() { */ public function testGetClientIdNonNull() { // test wildcard clientId provided via tokenDetails - $ablyKey = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyKey = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $wildcardToken = $ablyKey->auth->requestToken( [ 'clientId' => '*' ] ); - $ablyWildcard = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyWildcard = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $wildcardToken, ] ) ); @@ -104,7 +104,7 @@ public function testGetClientIdNonNull() { $this->assertEquals( '*', $ablyWildcard->auth->clientId, 'Expected clientId to be *' ); // test specified clientId specified in ClientOptions - $ablyCid = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyCid = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'clientId' => 'testClientId', ] ) ); @@ -124,13 +124,13 @@ public function testGetClientIdNonNull() { * Check if messages can be assigned a clientId with a wildcard lib instance */ public function testWildcardClientIdMsg() { - $ablyKey = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyKey = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $wildcardToken = $ablyKey->auth->requestToken( [ 'clientId' => '*' ] ); - $ablyWildcard = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyWildcard = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $wildcardToken, ] ) ); @@ -157,7 +157,7 @@ public function testWildcardClientIdMsg() { public function testClientIdLib() { $clientId = 'testClientId'; - $ablyCId = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyCId = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'clientId' => $clientId, 'useTokenAuth' => true, @@ -201,7 +201,7 @@ public function testClientIdLib() { * operations */ public function testClientIdPrecedence() { - $ablyCId = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyCId = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'useTokenAuth' => true, 'clientId' => 'overriddenClientId', @@ -227,13 +227,13 @@ public function testClientIdPrecedence() { * have a clientId. Check that Auth#clientId is null */ public function testRSA8f1() { - $ablyMain = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyMain = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $tokenDetails = $ablyMain->auth->requestToken(); - $ablyClient = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyClient = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $tokenDetails, ] ) ); @@ -250,13 +250,13 @@ public function testRSA8f1() { * publish a message with an explicit clientId value, and ensure that the message is rejected */ public function testRSA8f2() { - $ablyMain = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyMain = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $tokenDetails = $ablyMain->auth->requestToken(); - $ablyClient = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyClient = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $tokenDetails, ] ) ); @@ -273,13 +273,13 @@ public function testRSA8f2() { * a clientId. Check that Auth#clientId is a string with value '*'. */ public function testRSA8f3() { - $ablyMain = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyMain = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $tokenDetails = $ablyMain->auth->requestToken( [ 'clientId' => '*' ] ); - $ablyClient = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyClient = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $tokenDetails, ] ) ); @@ -297,13 +297,13 @@ public function testRSA8f3() { * the provided clientId */ public function testRSA8f4() { - $ablyMain = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyMain = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); $tokenDetails = $ablyMain->auth->requestToken( [ 'clientId' => '*' ] ); - $ablyClient = new AblyRest( array_merge( self::$defaultOptions, [ + $ablyClient = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'tokenDetails' => $tokenDetails, ] ) ); diff --git a/tests/HttpTest.php b/tests/HttpTest.php index e40c7f4..132b57c 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -1,7 +1,7 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); } @@ -38,7 +38,7 @@ public function testVersionHeaderPresence() { 'key' => 'fake.key:totallyFake', 'httpClass' => 'tests\HttpMock', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $curlParams = $ably->http->getCurlLastParams(); @@ -84,13 +84,13 @@ private static function expectedPrefix() { * Verify proper agent header is set as per RSC7d */ public function testAblyAgentHeader() { - $ably = new AblyRest( self::mockOptions() ); + $ably = new PubSubHttpClient( self::mockOptions() ); $this->assertSame( self::expectedPrefix(), self::sentAgentHeader( $ably ), 'Expected Ably agent header in HTTP request' ); // a second client renders the same header: no state leaks between instances - $ably = new AblyRest( self::mockOptions() ); + $ably = new PubSubHttpClient( self::mockOptions() ); $this->assertSame( self::expectedPrefix(), self::sentAgentHeader( $ably ), 'Expected Ably agent header in HTTP request' ); @@ -101,7 +101,7 @@ public function testAblyAgentHeader() { * process-global static state (RSC7d). */ public function testAblyAgentHeaderWithAgentsOption() { - $ably = new AblyRest( self::mockOptions( [ + $ably = new PubSubHttpClient( self::mockOptions( [ 'agents' => [ 'laravel' => null, 'customLib' => '2.3.5' ], ] ) ); @@ -159,7 +159,7 @@ public function sideAgentVersionProvider() { * the constructor directly declares none. */ public function testBareConstructorDeclaresNoSide() { - $agentHeader = self::sentAgentHeader( new AblyRest( self::mockOptions() ) ); + $agentHeader = self::sentAgentHeader( new PubSubHttpClient( self::mockOptions() ) ); $this->assertStringNotContainsString( 'ably-pubsub-server', $agentHeader, 'A directly constructed client must not declare the server side' ); @@ -220,7 +220,7 @@ public function testGET() { 'clientId' => 'test', ]; - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'key' => 'fake.key:totallyFake', 'authUrl' => 'http://test.test/tokenRequest', 'authParams' => $authParams, @@ -250,7 +250,7 @@ public function testPOST() { 'clientId' => 'test', ]; - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'key' => 'fake.key:totallyFake', 'authUrl' => 'http://test.test/tokenRequest', 'authParams' => $authParams, @@ -271,7 +271,7 @@ public function testPOST() { } /** - * RSC19 Test basic AblyRest::request functionality + * RSC19 Test basic PubSubHttpClient::request functionality */ public function testRequestBasic() { $ably = self::$ably; @@ -313,7 +313,7 @@ public function testRequestBasic() { * RSC19 - Test that Response handles various returned structures properly */ public function testRequestReturnValues() { - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'key' => 'fake.key:totallyFake', 'httpClass' => 'tests\HttpMockReturnData', ] ); diff --git a/tests/LogTest.php b/tests/LogTest.php index 8fd6a57..766ce59 100644 --- a/tests/LogTest.php +++ b/tests/LogTest.php @@ -1,6 +1,6 @@ 'fake.key:totallyFake' ] ); } @@ -33,7 +33,7 @@ public function testLogDefault() { $out .= $args[0] . "\n"; }, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $this->logMessages(); @@ -58,7 +58,7 @@ public function testLogVerbose() { }, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $this->logMessages(); $this->assertIsInt( strpos($out, 'This is a test warning.'), 'Expected warning level to be logged.' ); @@ -80,7 +80,7 @@ public function testLogNone() { }, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $this->logMessages(); $this->assertFalse( $called, 'Log handler incorrectly called' ); } diff --git a/tests/PresenceTest.php b/tests/PresenceTest.php index b2cc0a7..f674841 100644 --- a/tests/PresenceTest.php +++ b/tests/PresenceTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); diff --git a/tests/AblyRestRequestTest.php b/tests/PubSubHttpClientRequestTest.php similarity index 93% rename from tests/AblyRestRequestTest.php rename to tests/PubSubHttpClientRequestTest.php index 81a87d5..24a9f2b 100644 --- a/tests/AblyRestRequestTest.php +++ b/tests/PubSubHttpClientRequestTest.php @@ -1,9 +1,9 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); } diff --git a/tests/AblyRestTest.php b/tests/PubSubHttpClientTest.php similarity index 92% rename from tests/AblyRestTest.php rename to tests/PubSubHttpClientTest.php index f2890e2..0436bca 100644 --- a/tests/AblyRestTest.php +++ b/tests/PubSubHttpClientTest.php @@ -1,6 +1,6 @@ assertTrue( $ably->auth->isUsingBasicAuth(), 'Expected basic auth to be used' ); } @@ -40,7 +40,7 @@ public function testInitLibWithKeyString() { */ public function testInitLibWithKeyOption() { $key = 'fake.key:veryFake'; - $ably = new AblyRest( ['key' => $key ] ); + $ably = new PubSubHttpClient( ['key' => $key ] ); $this->assertTrue( $ably->auth->isUsingBasicAuth(), 'Expected basic auth to be used' ); } @@ -49,7 +49,7 @@ public function testInitLibWithKeyOption() { */ public function testInitLibWithTokenString() { $token = 'fake_token'; // token string never contains a colon - $ably = new AblyRest( $token ); + $ably = new PubSubHttpClient( $token ); $this->assertFalse( $ably->auth->isUsingBasicAuth(), 'Expected token auth to be used' ); } @@ -57,7 +57,7 @@ public function testInitLibWithTokenString() { * Init library with a token string in options */ public function testInitLibWithTokenOption() { - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'token' => "this_is_not_really_a_token", ] ); @@ -68,7 +68,7 @@ public function testInitLibWithTokenOption() { * Init library with a tokenDetails in options */ public function testInitLibWithTokenDetailsOption() { - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'tokenDetails' => new TokenDetails( "this_is_not_really_a_token" ), ] ); @@ -84,7 +84,7 @@ public function testInitLibWithSpecifiedHost() { 'restHost' => 'some.other.host', 'httpClass' => 'tests\HttpMockInitTest', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertMatchesRegularExpression( '/^https?:\/\/some\.other\.host/', $ably->http->lastUrl, 'Unexpected host mismatch' ); } @@ -99,7 +99,7 @@ public function testInitLibWithSpecifiedPort() { 'tlsPort' => 999, 'httpClass' => 'tests\HttpMockInitTest', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertStringContainsString( 'https://' . $opts['restHost'] . ':' . $opts['tlsPort'], @@ -114,7 +114,7 @@ public function testInitLibWithSpecifiedPort() { 'tls' => false, 'httpClass' => 'tests\HttpMockInitTest', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertStringContainsString( 'http://' . $opts['restHost'] . ':' . $opts['port'], @@ -127,7 +127,7 @@ public function testInitLibWithSpecifiedPort() { * Init library with specified environment */ public function testInitLibWithSpecifiedEnv() { - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'key' => 'fake.key:veryFake', 'environment' => 'sandbox', 'httpClass' => 'tests\HttpMockInitTest', @@ -144,7 +144,7 @@ public function testTLSDefaultIsTrue() { 'key' => 'fake.key:veryFake', 'httpClass' => 'tests\HttpMockInitTest', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertMatchesRegularExpression( '/^https:\/\/rest\.ably\.io/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); } @@ -158,7 +158,7 @@ public function testTLSCanBeFalse() { 'httpClass' => 'tests\HttpMockInitTest', 'tls' => false, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertMatchesRegularExpression( '/^http:\/\/rest\.ably\.io/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); } @@ -172,7 +172,7 @@ public function testTLSExplicitTrue() { 'httpClass' => 'tests\HttpMockInitTest', 'tls' => true, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertMatchesRegularExpression( '/^https:\/\/rest\.ably\.io/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); } @@ -189,7 +189,7 @@ public function testMaxRetryCount() { 'httpMaxRetryCount' => 2, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); try { $ably->time(); // make a request $this->fail('Expected the request to fail'); @@ -208,7 +208,7 @@ public function testFallbackHosts() { 'httpClass' => 'tests\HttpMockInitTestTimeout', 'httpMaxRetryCount' => 5, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); try { $ably->time(); // make a request $this->fail('Expected the request to fail'); @@ -237,7 +237,7 @@ public function testEnvFallbackHosts() { 'httpMaxRetryCount' => 5, 'environment' => 'alpha' ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); try { $ably->time(); // make a request $this->fail('Expected the request to fail'); @@ -266,7 +266,7 @@ public function testNoFallbackOnEmptyCustomFallbacks() { 'restHost' => 'custom.host.com', 'fallbackHosts' => [], ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); try { $ably->time(); // make a request $this->fail('Expected the request to fail'); @@ -296,7 +296,7 @@ public function testCustomHostAndFallbacks() { 'httpClass' => 'tests\HttpMockInitTestTimeout', 'httpMaxRetryCount' => 3, ]); - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); try { $ably->time(); // make a request $this->fail('Expected the request to fail'); @@ -323,7 +323,7 @@ public function testNoFallbackOnClientError() { 'httpClass' => 'tests\HttpMockInitTestTimeout', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->http->httpErrorCode = 401; $ably->http->errorCode = 40101; // auth error @@ -346,7 +346,7 @@ public function testNoFallbackOnCustomHost() { 'httpClass' => 'tests\HttpMockInitTestTimeout', 'restHost' => 'custom.host.com', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); try { $ably->time(); // make a request $this->fail('Expected the request to fail'); @@ -366,7 +366,7 @@ public function testFallbackHostsFailFirst3() { 'httpClass' => 'tests\HttpMockInitTestTimeout', 'httpMaxRetryCount' => 5, ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $ably->http->hostFailures = 3; $data = $ably->time(); // make a request @@ -390,7 +390,7 @@ public function testFallbackHostHeader() { 'key' => 'fake.key:veryFake', 'httpClass' => 'tests\HttpMockInitTestTimeout' ]); - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); try { $ably->time(); // make a request $this->fail('Expected the request to fail'); @@ -408,7 +408,7 @@ public function testFallbackHostHeader() { */ public function testCachedFallback() { $fallbackCacheTimeoutInMs = 1999; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, 'fallbackRetryTimeout' => $fallbackCacheTimeoutInMs, 'httpClass' => 'tests\HttpMockCachedFallback', @@ -440,14 +440,14 @@ public function testCachedFallback() { /** * Verify accuracy of time (to within 2 seconds of actual time) * - * RSC16 RestClient#time function sends a get request to rest.ably.io/time + * RSC16 PubSubHttpClient#time function sends a get request to rest.ably.io/time * and returns the server time in milliseconds since epoch */ public function testTimeAndAccuracy() { $opts = [ 'key' => 'fake.key:veryFake', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $time = $ably->time(); $this->assertIsInt( $time ); @@ -465,7 +465,7 @@ public function testActiveInternetConnection() { $opts = [ 'key' => 'fake.key:veryFake', ]; - $ably = new AblyRest( $opts ); + $ably = new PubSubHttpClient( $opts ); $this->assertTrue($ably ->hasActiveInternetConnection()); } @@ -473,7 +473,7 @@ public function testActiveInternetConnection() { * Verify that time fails without valid host */ public function testTimeFailsWithInvalidHost() { - $ablyInvalidHost = new AblyRest( [ + $ablyInvalidHost = new PubSubHttpClient( [ 'key' => 'fake.key:veryFake', 'restHost' => 'this.host.does.not.exist', ]); @@ -487,11 +487,11 @@ public function testTimeFailsWithInvalidHost() { * Connection/open timeout not reliably testable. */ public function testHttpTimeout() { - $ably = new AblyRest( [ + $ably = new PubSubHttpClient( [ 'key' => 'fake.key:veryFake', ]); - $ablyTimeout = new AblyRest( [ + $ablyTimeout = new PubSubHttpClient( [ 'key' => 'fake.key:veryFake', 'httpRequestTimeout' => 20, // 20 ms ]); diff --git a/tests/PushAdminTest.php b/tests/PushAdminTest.php index 3293399..0c28dc0 100644 --- a/tests/PushAdminTest.php +++ b/tests/PushAdminTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); } diff --git a/tests/PushChannelSubscriptionsTest.php b/tests/PushChannelSubscriptionsTest.php index caa826b..252edf8 100644 --- a/tests/PushChannelSubscriptionsTest.php +++ b/tests/PushChannelSubscriptionsTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); } diff --git a/tests/PushDeviceRegistrationsTest.php b/tests/PushDeviceRegistrationsTest.php index b85dd8f..ee5f358 100644 --- a/tests/PushDeviceRegistrationsTest.php +++ b/tests/PushDeviceRegistrationsTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); } diff --git a/tests/TokenTest.php b/tests/TokenTest.php index 074761c..74e1072 100644 --- a/tests/TokenTest.php +++ b/tests/TokenTest.php @@ -1,6 +1,6 @@ getOptions(); - self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ + self::$ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => self::$testApp->getAppKeyDefault()->string, ] ) ); @@ -143,7 +143,7 @@ public function testTokenGenerationWithSpecifiedKey() { 'key' => $key->string, ]; - $ably = new AblyRest( array_merge( self::$defaultOptions, [ + $ably = new PubSubHttpClient( array_merge( self::$defaultOptions, [ 'key' => 'fake.key:veryFake', ] ) ); @@ -211,7 +211,7 @@ public function testTokenRenewalKnownExpiration() { } ] ); - $ablyTokenAuth = new AblyRest( $options ); + $ablyTokenAuth = new PubSubHttpClient( $options ); $ablyTokenAuth->auth->authorize(); $tokenBefore = $ablyTokenAuth->auth->getTokenDetails()->token; @@ -248,7 +248,7 @@ public function testTokenRenewalUnknownExpiration() { } ] ); - $ablyTokenAuth = new AblyRest( $options ); + $ablyTokenAuth = new PubSubHttpClient( $options ); $ablyTokenAuth->auth->authorize(); $tokenBefore = $ablyTokenAuth->auth->getTokenDetails()->token; @@ -285,7 +285,7 @@ public function testTokenRenewalUnknownExpirationFailure() { } ] ); - $ablyTokenAuth = new AblyRest( $options ); + $ablyTokenAuth = new PubSubHttpClient( $options ); $channel = $ablyTokenAuth->channel( 'testchannel' ); // do an authorised request with the valid token @@ -312,7 +312,7 @@ public function testFailingTokenRenewalUnknownExpiration() { $options = array_merge( self::$defaultOptions, [ 'token' => $tokenDetails->token, ] ); - $ablyTokenAuth = new AblyRest( $options ); + $ablyTokenAuth = new PubSubHttpClient( $options ); $channel = $ablyTokenAuth->channel( 'testchannel' ); $channel->publish( 'test', 'test' ); // this should work diff --git a/tests/TypesTest.php b/tests/TypesTest.php index 7f71bbe..ec3196f 100644 --- a/tests/TypesTest.php +++ b/tests/TypesTest.php @@ -1,6 +1,6 @@