Split Pub/Sub SDK: ably/pubsub-server package, Ably\PubSub namespace, server door - #223
Draft
umair-ably wants to merge 8 commits into
Draft
Split Pub/Sub SDK: ably/pubsub-server package, Ably\PubSub namespace, server door#223umair-ably wants to merge 8 commits into
umair-ably wants to merge 8 commits into
Conversation
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) <noreply@anthropic.com>
…nge) 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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
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<string, string|null>). 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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/<anything>` 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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 9, 2026
umair-ably
added a commit
that referenced
this pull request
Sep 9, 2026
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 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements PDR-091b (PubSub package split) for the PHP SDK — plan steps 11, 12, 13, 14, 14b, 14c of
plan.md, which is row 1 of the PR stack in step 10b. The PHP surface is fixed by PDR-091b2, whose PHP row reads verbatim: "Repo: ably-php → ably-pubsub-php. New package:ably/pubsub-server. Factory door:createHttpClient(...)only."Siblings: ably-js#2293, ably-ruby#453, ably-python#683.
What and why
Today an application installs
ably/ably-phpand gets one package regardless of where it runs. PDR-091 makes the package name the declaration of the side: the client you reach for is the one whose package matches where your code runs, and — because a server connection is exempt from monthly-active-user counting — that declaration has to reach the wire, not just the README.PDR-091b settles how: new majors on a new core, not thin wrappers over the old package.
ably/ably-phpis not touched, is not re-exported, and reaches EOL a year after GA; fixes for it ship from a maintenance branch.So this repo stops publishing
ably/ably-phpand starts publishingably/pubsub-server, at2.0.0, on a PHP^8.1floor.Why one package and no
ably/pubsub-core(deviation from the other SDKs)Every other SDK in this programme ships a core plus one or two side packages. PHP ships one package, and this is a decision rather than an omission — PDR-091b2's PHP row already names only
ably/pubsub-server. Five reasons, in ascending order of how hard they are to argue with:export … from; the nearest thing isclass_alias, which does not satisfy a type hint written against the aliased name in a way any static analyser will follow. So a separate core would force consumers to writeuse Ably\PubSub\Core\Models\Message;in their own code — type-hinting classes out of a package whose description says "internal, do not depend on this". A boundary that every consumer has to reach across is not a boundary.ably/ably-php-laravelandably/laravel-broadcasterare server-side by construction, so they depend onably/pubsub-server: ^2.0and construct through the door.composer.jsonname says. Composer'sVcsRepository::preProcessdeliberately overwrites each version'snamewith 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'sUpdaterthen stamps its own package name on every version. There is no name-based filtering of versions anywhere in the path. So two Composer packages cannot share one repository, and each one needs its own distribution mirror. A core would mean a second mirror repo, a subtree split to populate it, and a second Packagist entry to operate — for no consumer benefit.core//server/directory split either. With one package it would be cosmetic (Packagist needscomposer.jsonat the root) and it would enlarge the move diff for no packaging gain.Consequence for this repository
ably/pubsub-serveris published from a read-only distribution mirror, not from here.ably/ably-phpstays bound to this repo (its ~8.7M downloads put it behind Packagist'sPopularPackageSafetyValidator, so its URL cannot be moved to a different repository without Packagist support, and a repo-ID change would freeze it) and keeps indexing1.xtags from the maintenance branch.Which gives the one rule that must never be broken here:
Releases here are tagged
pubsub-server/2.0.0, which Composer skips as an invalid version name (VcsRepository::validateTag); the plain2.0.0tag exists only on the mirror. PR 2 enforces this in the release pre-flight and in arelease-dry-runjob on every PR.Layout
Same flat layout, new identity:
Two things worth calling out:
Ably\PubSub\, notAbly\. The legacy package owns the PSR-4 prefixAbly\→src/. Composer will installably/ably-phpandably/pubsub-serverside by side — a Laravel app mid-migration, or a transitive dependency still on 1.x — and if both declaredAbly\the autoloader would resolveAbly\AblyRestto whichever prefix path happens to be searched first. A packaging test asserts no file undersrc/declares a namespace outsideAbly\PubSub.AblyReststaysAblyRest, so this diff is a restructure and nothing else. The 091d public-API rename (AblyRest→HttpClient) is a later, mechanical PR (step 18) and is not blocking on this.The door
final class Ably\PubSub\Server, private constructor, one static method. It accepts exactly what the client constructor accepts — an options array, aClientOptionsinstance, or a string holding an API key or a token — reusing the same normalisation rather than a copy of it: the constructor's inlinestrpos(':')disambiguation moved toClientOptions::normalizeConstructorArgument()and both call it. It stamps the side entry, constructs, returns. Nothing else; it is a door, not a room.No
createRealtimeClientand no device door, per PDR-091b2: PHP has no realtime client, so a stub would only invite the question.The client constructor stays public — a
finalstatic door over a private constructor is not idiomatic for a class this library and its tests construct internally — but is marked@internalpointing at the door. The consequence, stated in the docblock and asserted in a test: a barenew AblyRest(...)declares no side, and will be rejected on MAU accounts once pricing is live.Agent header
with a wrapper:
Note the ordering: plan step 13 sketches the wrapper case with the side entry before the wrapper entries, but the rule it also states — the side entry is merged last so it wins a collision — puts it after them. The rule wins; the sketch was illustrative. Order carries no meaning to the classifier, only presence does.
ably-php→ably-pubsub-php, still versioned withLIB_VERSION. It lands here, before any prerelease, so even prerelease traffic partitions cleanly from legacyably-php/*traffic. The maintenance branch keepsably-php.ClientOptionsgains an additiveagentsoption (array<string, string|null>).ablyAgentHeader()becomes an instance method reading it, rendering each entry asname/versionor as a barenamewhen the version isnullor''— matching how the registry records entries that carry no version of their own, likebrowser.src/AblyRest.phpstill sends the header from exactly one place.ably-pubsub-serverwith no version, applied last so it wins a collision on its own identifier: the side is the package's to declare, not the caller's to redefine. Caller entries are otherwise preserved in order, so an SDK layered on top keeps its attribution. AClientOptionsargument is cloned rather than mutated.ably-pubsub-server/token is ever emitted, for a caller version of'x',''andnull— the PHP shape of thename/undefinedregression ably-js#2297 guards against.-serversuffix is load-bearing: realtime grants the MAU server exemption on API-key auth by matching an agent entry ending in it. There is a comment saying exactly that whereSERVER_AGENT_IDENTIFIERis defined, copied in spirit from ably-ruby'sserver.rband ably-python'sserver/__init__.py, and a test asserts the suffix survives.Both identifiers are registered in ably-common#361 (open).
.ably/capabilities.yamlalready declaresAgent Identifier: Agents, so it needed no change — that declaration is now true per-client rather than per-process.What was removed, and why
AblyRest::$agents,setAblyAgentHeader()agentsoption. Not currently@deprecated, so this is a stated owner decision, recorded in plan step 13 and in the migration table.AblyRest::setLibraryFlavourString()@deprecated; superseded by the same option.ably-loader.phpAbly\prefix. Composer is the only supported install path (the README already says so) and the prefix has moved.demo/,Procfilenew \Ably\AblyRestdirectly — exactly the shape the door replaces.The only known callers of the static setters are the two Laravel wrappers, which are being re-released against this package anyway (plan step 27) — so their PRs must be open before PHP GA, not after.
rybakit/msgpackalso gains an upper bound (^0.9.1 || ^1.0). The old>=1.0style unbound constraint is whatcomposer validate --strictrejects, and CI now runs--strict.Commits
Deliberately split so each is diffable on its own:
Re-identify the package as ably/pubsub-server.gitattributesMove the namespace Ably\ -> Ably\PubSub\Remove the hand-rolled autoloader, the Heroku demo and its ProcfileMake the agent header per-client and rename the SDK identifieragentsoption, static removal,ably-pubsub-php,LIB_VERSIONAdd the factory door Ably\PubSub\Server::createHttpClient()Assert the agent header and the packaging invariantsCI: test 8.1-8.5, cover integration/v2, validate strictlyThe namespace commit leaves the tree temporarily unloadable between commits 1 and 2 (the composer PSR-4 root moves first), and the agent assertions are red between commits 4 and 6. Both are deliberate: keeping the mechanical move free of behaviour is worth more to a reviewer than every commit building.
Verification
Run on macOS via Docker (
composer:2,php:8.3-cli,php:8.1-cli) againstsandbox.composer validate --strict— passes. It failed first on the unboundrybakit/msgpackconstraint, which is why that changed.composer install— clean on the^8.1floor.PROTOCOL=json— 212 tests, 1331 assertions, 0 failures onphp:8.3-cli, in about 3m02s. That is the whole sandbox suite:TestAppcreates a real app againstABLY_ENV=sandbox, so REST publish, history, presence, auth, token issuing, push admin, crypto and the stats fixtures from theably-commonsubmodule all ran.PROTOCOL=msgpack— 212 tests, 1331 assertions, 0 failures, in about 3m24s.php:8.1-cli) — 212 tests, 1331 assertions, 0 failures (PROTOCOL=json), in about 3m19s — the new floor is real, not just declared.tests/HttpTest.php, 10 tests / 26 assertions): the plain header on the new identifier; the same header from a second client (no state leaks now that the static map is gone); wrapper attribution via theagentsoption; the door's header against^ably-pubsub-php/\d+\.\d+\.\d+(\S*)? php/\S+ ably-pubsub-server$; noably-pubsub-server/token for a caller version of'x',''ornull; a barenew AblyRestdeclaring no side; caller entries surviving and preceding the side entry; the door accepting an array, aClientOptions, an API-key string and a token string, and not mutating aClientOptionsit was handed.tests/PackagingTest.php, 7 tests): composer name; PSR-4 mapping onlyAbly\PubSub\; no file undersrc/declaring a namespace outside it;Defaults::LIB_VERSIONmatching the top CHANGELOG heading; every class undersrc/resolving through the generated autoloader; the door final, non-instantiable and still-server-suffixed. Each was checked to fail when violated, not just to pass — e.g. dropping anamespace Ably;file intosrc/reports it by name.grep -rn 'Ably\\[A-Z]' src tests | grep -v 'Ably\\PubSub'— empty, so the move left nothing behind in the bare namespace.Out of scope / follow-ups
release.ymlwith the mirror push, the namespaced tag here, the Packagist visibility poll, and the pre-flight that refuses a Composer-valid2.xtag in this repo; plus therelease-dry-runjob and theCONTRIBUTING.mdrelease section. This repo has no release workflow at all today — publishing is manual and tag-driven.release.ymlonmain, soworkflow_dispatchis registered while the split still lives onintegration/v2. Gated on the 1.x maintenance branch being cut first (step 24).README.md,UPDATING.mdand the 2.0.0 CHANGELOG body are untouched here. This PR adds only the## [2.0.0] (unreleased)heading, because the version-site invariant test reads it.AblyRest→HttpClientand the deprecated-surface deletions, gated on that DR being decided.ably-php→ably-pubsub-php(step 17) and #222.composer.json'ssupport.source/support.issuesalready point at the renamed URL, which resolves once the rename happens.ably/ably-php-laravelandably/laravel-broadcasterdepending onably/pubsub-server: ^2.0, released in the same GA window.🤖 Generated with Claude Code