Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
centrifugal/phpcent
claude-code
cursor
websocket
messaging
realtime
jwt
pubsub
centrifugo
JWKS

phpcent is the PHP SDK for Centrifugo's HTTP server API, written to the Centrifugo server SDK specification, whose reference implementation is gocent. Applications call it on their hot paths - often once per web request - so it has to be correct and hard to misuse. Read this before changing code.

Install
php artisan boost:add-skill centrifugal/phpcent

Save this content to: AGENTS.md

# Working on phpcent

phpcent is the PHP SDK for Centrifugo's HTTP server API, written to the
[Centrifugo server SDK specification](https://github.com/centrifugal/gocent/blob/master/spec.md),
whose reference implementation is gocent. Applications call it on their hot
paths - often once per web request - so it has to be correct and hard to
misuse. Read this before changing code.

## Layout

| Path | What |
|---|---|
| `proto/api.proto`, `proto/proxy.proto` | Copies of Centrifugo's protos (`CENTRIFUGO_REF=vX.Y.Z composer sync-proto`): a released tag, or a commit of master named in the changelog |
| `gen/required_fields.txt` | The fields the protos mark "Required.", made required named arguments and constructor arguments, and checked for a value before sending (but those whose empty value has a meaning). Generation fails when the protos' list differs: a newly required field breaks every call without it, so review the change - fix the proto upstream if Centrifugo accepts the field absent - then `php gen/generate.php --accept-required` |
| `gen/` | The generator: `Proto.php` parses the protos, `Rules.php` reads their comments ("Required.", presence wrappers, "One of:"), `constants_*.txt` pin the constants' names, `generate.php` writes `generated/`, `api_docs.php` writes `API.md` |
| `generated/` | Generated - never edited: the result and nested message types; `ClientMethods`, `BatchMethods`, `CommandMethods`, the methods taking each request's fields as named arguments; `Internal/` the request classes they build; `Proxy/` types |
| `API.md` | The API reference, generated from the docblocks by `composer docs` - never edited |
| `src/Client.php` | The client: configuration, sending, broadcast, `broadcastWithRetry` |
| `src/Internal/` | Encoding requests (`Encode`), decoding replies (`Decode`, `Reply`), validation, the curl and PSR-18 transports |
| `src/Exception/` | The exceptions; `src/Retry.php` has `retryable` and the backoff |
| `src/Batch.php`, `src/Pending.php`, `src/BroadcastResult.php` | Batches and broadcast results |
| `src/Http/` | The HTTP handlers' framework-free core and its PSR-15 and plain PHP adapters |
| `src/Endpoints/`, `src/Proxy/`, `src/Token/`, `src/SharedPoll.php` | Endpoints for client SDKs, proxy handlers, tokens, shared poll signatures |
| `tests/`, `tests/Integration/` | Unit tests against a fake Centrifugo; integration tests against a real one |
| `testdata/` | The Centrifugo config and the Centrifugo PRO compose file the integration tests use |

## Invariants

These are not preferences. A change that breaks one is wrong even if every test
passes; if you find yourself about to break one, stop and say so instead.

1. **The generated files are never edited.** Change `proto/` or `gen/`, then
   `composer generate`; `composer generate-check` fails when they are stale.
   After a change to a docblock or the public API, `composer docs`
   regenerates `API.md`; `composer check` fails when it is stale.
2. **A call throws unless it did everything asked.** A broadcast or batch in
   which some parts failed throws `BroadcastFailedChannelsException` or
   `BatchFailedCommandsException`, whose `result` holds every part's outcome.
   A partial error is never an `ApiException` of its parts - also with one
   channel.
3. **Errors follow Centrifugo's model.** A Centrifugo error is an
   `ApiException`, thrown as the subclass of its code when the code is known,
   and as `ApiException` itself otherwise. Centrifugo's transport error mode
   gives the same exception as a 200 reply.
4. **One deadline per call.** Its `timeout`, or `requestTimeout`, bounds the
   whole call: curl's `CURLOPT_TIMEOUT_MS` covers connecting, sending and the
   reply. PSR-18 cannot give a request a deadline: Guzzle and Symfony's
   `Psr18Client` get it through their own API (`Psr18Transport::send`), and
   any other client the caller passed has its own timeouts, which the docs
   say. Any timeout throws `TimeoutException`.
5. **A payload is carried exactly.** Replies are decoded as objects
   (`json_decode($json, false)`), never associative arrays, so that a payload
   sent back unchanged is the same JSON value - `{}` and `{"0":"a"}`
   included. `RawJson` is sent as its text, also inside a value; NaN,
   infinities, strings which are not UTF-8 and invalid JSON never leave the
   client. Payloads are encoded apart and spliced into the request's JSON.
6. **A caller's HTTP client is never changed**: credentials and headers go on
   each request.
7. **A handler answers with exactly one of result, error and disconnect,** as
   its proxy type can carry them; anything else is a 500 reported to
   `onError`.
8. **PHP 8.3 is the floor.** PHPStan checks at `phpVersion: 80300`; no
   function or constant newer than 8.3 without a check (`array_any` is 8.4,
   `curl_share_init_persistent` 8.5 - guarded).

## Running things

```
composer install          # the dev tools
composer check            # generated code, API.md, php-cs-fixer, PHPStan at max, unit tests: what CI runs
composer test-integration # against Centrifugo on :8100
```

The integration tests need a Centrifugo set up as `testdata/` says:

```
docker compose -f testdata/centrifugo-pro.compose.yml up -d --wait
PHPCENT_TEST_PRO=1 composer test-integration
```

They serve the proxy handlers on port 3100 and an API JWKS on 3101 of the host,
with `php -S`, which Centrifugo reaches as `host.docker.internal`.

## Updating to a new Centrifugo API

1. `CENTRIFUGO_REF=vX.Y.Z composer sync-proto` - a released tag, or, when a release needs changes no tag has yet, a commit of Centrifugo's master, which the release's changelog entry names (spec section 2).
2. Read the diff of `proto/`: a removed field is a breaking change.
3. `composer check`, then the integration tests against the new release.
4. Bump the Centrifugo image in `.github/workflows/ci.yml` and
   `testdata/centrifugo-pro.compose.yml`, and the versions table in the README
   when a feature needs the new release.

## Releasing

Push a tag, `v7.0.0` say: Packagist picks it up through its GitHub hook, and
`release.yml` checks the tag against the code, then creates the GitHub
release. `composer.json` has no `version`: the tag is the version.

The release notes are the version's `CHANGELOG.md` entry, pasted into the
GitHub release as they are: its links are absolute.

## Things that look wrong but are deliberate

- **Named arguments, not request objects**: spec 0.2's "unified meaning,
  idiomatic spelling" - `$client->publish(channel: ..., data: ...)`,
  `$batch->publish(...)`, `Command::publish(...)`, generated with the same
  parameters. The request classes are internal. PHP cannot make arguments
  named-only: the docs say their order is not part of the API, and a field
  added to a proto may shift it - before `$timeout`, too.
- **Payloads in replies are `stdClass`.** Arrays would turn `{}` into `[]`
  when sent on: spec 3.4's PHP row.
- **curl by default, PSR-18 optional.** curl enforces the one deadline and
  keeps connections alive; PSR-18 is how applications bring their own client,
  and how they test code using phpcent (issue #55). `Client` stays `final`.
- **A call's `timeout` replaces `requestTimeout`, longer or shorter**, and
  `broadcastWithRetry`'s `attemptTimeout` does for each attempt, its call's
  `timeout` still ending it sooner (spec 0.3, 3.2 and 7.3). PHP has no
  deadline a call runs under - no context, no cancellation - so nothing else
  shortens it.
- **No automatic batching and no async client**: a PHP request makes its calls
  one after another (spec 8 allows leaving it out).
- **`ext-curl` is suggested, not required**: an application passing its own
  PSR-18 client needs no curl. Without either, creating a client fails with the
  fix in the message.
- **`rpc` is not offered**: it calls server API extensions, which Centrifugo
  does not provide.
- **`BoolValue` fields are `?bool`.** `null` leaves the server's setting alone,
  which `false` cannot say. The other presence wrappers - `deviceUpdate`'s
  update messages - stay nullable objects, sent whenever set, even as `{}`
  (spec 0.3, 2); every other message set but empty is left out.
- **Constants are final classes of strings, not enums**: a value Centrifugo
  adds must stay sendable. Their names are the spec's table (3.3), pinned in
  `gen/constants_api.txt` and `gen/constants_proxy.txt`, never derived: a
  "One of:" field the lists lack fails the generation - name it there and in
  the spec.
- **Required-field errors name wire paths** (`limit_strategy.rate_limit`),
  as the encoding errors do, not the camelCase arguments.
- **The `<method>All` iterators are not generator functions**: they check the
  request and return a `Generator`, so an invalid request throws at the call,
  not on the first iteration.
- **An oversized body is a 400**, as in gocent, not a 413.

Working on phpcent

phpcent is the PHP SDK for Centrifugo's HTTP server API, written to the Centrifugo server SDK specification, whose reference implementation is gocent. Applications call it on their hot paths - often once per web request - so it has to be correct and hard to misuse. Read this before changing code.

Layout

Path What
proto/api.proto, proto/proxy.proto Copies of Centrifugo's protos (CENTRIFUGO_REF=vX.Y.Z composer sync-proto): a released tag, or a commit of master named in the changelog
gen/required_fields.txt The fields the protos mark "Required.", made required named arguments and constructor arguments, and checked for a value before sending (but those whose empty value has a meaning). Generation fails when the protos' list differs: a newly required field breaks every call without it, so review the change - fix the proto upstream if Centrifugo accepts the field absent - then php gen/generate.php --accept-required
gen/ The generator: Proto.php parses the protos, Rules.php reads their comments ("Required.", presence wrappers, "One of:"), constants_*.txt pin the constants' names, generate.php writes generated/, api_docs.php writes API.md
generated/ Generated - never edited: the result and nested message types; ClientMethods, BatchMethods, CommandMethods, the methods taking each request's fields as named arguments; Internal/ the request classes they build; Proxy/ types
API.md The API reference, generated from the docblocks by composer docs - never edited
src/Client.php The client: configuration, sending, broadcast, broadcastWithRetry
src/Internal/ Encoding requests (Encode), decoding replies (Decode, Reply), validation, the curl and PSR-18 transports
src/Exception/ The exceptions; src/Retry.php has retryable and the backoff
src/Batch.php, src/Pending.php, src/BroadcastResult.php Batches and broadcast results
src/Http/ The HTTP handlers' framework-free core and its PSR-15 and plain PHP adapters
src/Endpoints/, src/Proxy/, src/Token/, src/SharedPoll.php Endpoints for client SDKs, proxy handlers, tokens, shared poll signatures
tests/, tests/Integration/ Unit tests against a fake Centrifugo; integration tests against a real one
testdata/ The Centrifugo config and the Centrifugo PRO compose file the integration tests use

Invariants

These are not preferences. A change that breaks one is wrong even if every test passes; if you find yourself about to break one, stop and say so instead.

  1. The generated files are never edited. Change proto/ or gen/, then composer generate; composer generate-check fails when they are stale. After a change to a docblock or the public API, composer docs regenerates API.md; composer check fails when it is stale.
  2. A call throws unless it did everything asked. A broadcast or batch in which some parts failed throws BroadcastFailedChannelsException or BatchFailedCommandsException, whose result holds every part's outcome. A partial error is never an ApiException of its parts - also with one channel.
  3. Errors follow Centrifugo's model. A Centrifugo error is an ApiException, thrown as the subclass of its code when the code is known, and as ApiException itself otherwise. Centrifugo's transport error mode gives the same exception as a 200 reply.
  4. One deadline per call. Its timeout, or requestTimeout, bounds the whole call: curl's CURLOPT_TIMEOUT_MS covers connecting, sending and the reply. PSR-18 cannot give a request a deadline: Guzzle and Symfony's Psr18Client get it through their own API (Psr18Transport::send), and any other client the caller passed has its own timeouts, which the docs say. Any timeout throws TimeoutException.
  5. A payload is carried exactly. Replies are decoded as objects (json_decode($json, false)), never associative arrays, so that a payload sent back unchanged is the same JSON value - {} and {"0":"a"} included. RawJson is sent as its text, also inside a value; NaN, infinities, strings which are not UTF-8 and invalid JSON never leave the client. Payloads are encoded apart and spliced into the request's JSON.
  6. A caller's HTTP client is never changed: credentials and headers go on each request.
  7. A handler answers with exactly one of result, error and disconnect, as its proxy type can carry them; anything else is a 500 reported to onError.
  8. PHP 8.3 is the floor. PHPStan checks at phpVersion: 80300; no function or constant newer than 8.3 without a check (array_any is 8.4, curl_share_init_persistent 8.5 - guarded).

Running things

composer install          # the dev tools
composer check            # generated code, API.md, php-cs-fixer, PHPStan at max, unit tests: what CI runs
composer test-integration # against Centrifugo on :8100

The integration tests need a Centrifugo set up as testdata/ says:

docker compose -f testdata/centrifugo-pro.compose.yml up -d --wait
PHPCENT_TEST_PRO=1 composer test-integration

They serve the proxy handlers on port 3100 and an API JWKS on 3101 of the host, with php -S, which Centrifugo reaches as host.docker.internal.

Updating to a new Centrifugo API

  1. CENTRIFUGO_REF=vX.Y.Z composer sync-proto - a released tag, or, when a release needs changes no tag has yet, a commit of Centrifugo's master, which the release's changelog entry names (spec section 2).
  2. Read the diff of proto/: a removed field is a breaking change.
  3. composer check, then the integration tests against the new release.
  4. Bump the Centrifugo image in .github/workflows/ci.yml and testdata/centrifugo-pro.compose.yml, and the versions table in the README when a feature needs the new release.

Releasing

Push a tag, v7.0.0 say: Packagist picks it up through its GitHub hook, and release.yml checks the tag against the code, then creates the GitHub release. composer.json has no version: the tag is the version.

The release notes are the version's CHANGELOG.md entry, pasted into the GitHub release as they are: its links are absolute.

Things that look wrong but are deliberate

  • Named arguments, not request objects: spec 0.2's "unified meaning, idiomatic spelling" - $client->publish(channel: ..., data: ...), $batch->publish(...), Command::publish(...), generated with the same parameters. The request classes are internal. PHP cannot make arguments named-only: the docs say their order is not part of the API, and a field added to a proto may shift it - before $timeout, too.
  • Payloads in replies are stdClass. Arrays would turn {} into [] when sent on: spec 3.4's PHP row.
  • curl by default, PSR-18 optional. curl enforces the one deadline and keeps connections alive; PSR-18 is how applications bring their own client, and how they test code using phpcent (issue #55). Client stays final.
  • A call's timeout replaces requestTimeout, longer or shorter, and broadcastWithRetry's attemptTimeout does for each attempt, its call's timeout still ending it sooner (spec 0.3, 3.2 and 7.3). PHP has no deadline a call runs under - no context, no cancellation - so nothing else shortens it.
  • No automatic batching and no async client: a PHP request makes its calls one after another (spec 8 allows leaving it out).
  • ext-curl is suggested, not required: an application passing its own PSR-18 client needs no curl. Without either, creating a client fails with the fix in the message.
  • rpc is not offered: it calls server API extensions, which Centrifugo does not provide.
  • BoolValue fields are ?bool. null leaves the server's setting alone, which false cannot say. The other presence wrappers - deviceUpdate's update messages - stay nullable objects, sent whenever set, even as {} (spec 0.3, 2); every other message set but empty is left out.
  • Constants are final classes of strings, not enums: a value Centrifugo adds must stay sendable. Their names are the spec's table (3.3), pinned in gen/constants_api.txt and gen/constants_proxy.txt, never derived: a "One of:" field the lists lack fails the generation - name it there and in the spec.
  • Required-field errors name wire paths (limit_strategy.rate_limit), as the encoding errors do, not the camelCase arguments.
  • The <method>All iterators are not generator functions: they check the request and return a Generator, so an invalid request throws at the call, not on the first iteration.
  • An oversized body is a 400, as in gocent, not a 413.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums