Repository navigation
phpcent 7 is a rewrite to the Centrifugo server SDK specification, version 0.4, whose reference implementation is gocent. It needs PHP 8.3 or newer and lives in the Centrifugal\Phpcent namespace; it works with Centrifugo v6 and Centrifugo PRO v6 - the map and shared poll methods need v6.8.0 or newer, ApiException::$temporary and TooManyRequestsException v6.10.0 or newer. Migrating from phpcent 6: see MIGRATING.md.
Generated from Centrifugo's protos at e21aeb50 on its master, after v6.10.2: the same messages as v6.10.2, with the comments the constants are generated from, and ConnectResult.profile for the connect proxy, which Centrifugo PRO newer than v6.10.2 reads.
Client and calls
- A method for every API method, the map, shared poll and Centrifugo PRO ones included, generated from Centrifugo's
api.protowith its documentation. Each takes its request's fields as named arguments -$client->publish(channel: 'news', data: $data)- and returns a typed result.rpcis not offered: it calls server API extensions, which Centrifugo does not provide. - No dependencies but the PSR HTTP interfaces. curl by default, keeping connections alive between calls (PHP 8.5's persistent curl share keeps them across requests of a PHP-FPM worker), or any PSR-18 client (#55): Guzzle, Symfony HttpClient, a fake in tests. A client you pass is never changed: credentials and headers go on each request.
- Options checked when the client is created, with
InvalidConfigExceptionnaming the option and the fix:apiEndpointorapiEndpointFunc,apiKeyorbearerTokenFunc,httpClientandhttpFactory,curlOptions,requestTimeout,headers(aUser-Agentreplacesphpcent/7),onRequest. An endpoint ending in a method name,/api/publish, is refused with a hint. apiEndpointFuncandbearerTokenFuncare called for each request. An endpoint failingapiEndpoint's rules, or an empty or malformed token, fails the call withInvalidConfigExceptionbefore sending.- A field the protos mark "Required." is a named argument without a default - in the nested messages' constructors too:
new RateLimitPolicy(rate: 1, intervalMs: 1000)- and is checked for a value before sending, in a call, a batch and a command:channel: '',version: 0or an emptynotificationthrowsInvalidRequestExceptionnaming the field's path,limit_strategy.rate_limit.policies[0].rate is required. A field whose empty value has a meaning -invalidateUserTokens(user: ''), the device updates' values - is not checked; a payload's JSON null,new RawJson('null'), is a value. - A nested message set but empty is not sent - an empty
limitStrategyis no limit strategy. The presence wrappers are sent whenever set, even empty:deviceUpdate(userUpdate: new DeviceUserUpdate(user: ''))detaches the devices from their user, an empty timezone, locale or meta update clears it.BoolValuefields are?bool:nullleaves the server's setting alone. - Redirects are not followed; the error says Centrifugo's API never redirects a call, and to check the endpoint's scheme and path.
close()refuses further calls withClosedException; a broadcast being retried stops at its next attempt, which fails withClosedException. - Results render with Centrifugo's field names under
json_encode-num_clients- and errors in them as{code, message}.
Timeouts
requestTimeout, 10 seconds by default, bounds every call: connecting, sending and the whole reply. A call's owntimeoutreplaces it, longer or shorter. PHP has no deadline a call runs under, so nothing else shortens it. Zero is refused, as a negative is: it is not "no timeout". Any timeout throwsTimeoutException.- Guzzle and Symfony's
Psr18Clientget the call's deadline through their own API - Guzzle'stimeoutrequest option, Symfony'smax_duration- and a reply whose body ended past it is aTimeoutException, not a cut reply. Another PSR-18 client has its own timeouts, which throwTimeoutExceptiontoo.
Errors
- Every call throws unless Centrifugo did everything asked, all exceptions implementing
CentException:ApiException, withgetCode(),$serverMessage,$temporaryand a subclass per known code (UnknownChannelException,BadRequestException,NotFoundException,NotAvailableException,TooManyRequestsException,UnrecoverablePositionException,ConflictException,InternalException...), an unknown code still anApiException;HttpExceptionwith$statusCode,$bodyand$headers,UnauthorizedExceptionfor refused credentials;DecodeException;InvalidRequestException;InvalidConfigException;NetworkException;TimeoutException;ClosedException. FunctionException: whatapiEndpointFuncorbearerTokenFuncthrew, its message naming the function,$functionthe option, the function's exception its previous one.- Hints in messages: a Centrifugo PRO method answered by Centrifugo OSS, a 404 from an endpoint without the API prefix, a 401. Centrifugo's transport error mode (
http_api.error_mode: transport) gives the same exceptions as a 200 reply.
Retries and idempotency
Retry::retryable($e): temporary API errors, network failures, timeouts, 429 and 5xx, a partial failure with a retryable part; aFunctionExceptionby the function's own exception. Not retryable: the request's own errors,InvalidConfigException,ClosedException,DecodeException, refused credentials and TLS certificates.Retry::exponentialBackoff($base, $limit), with jitter. The docs explain idempotency keys: one per publication or broadcast, kept by the memory, Redis and PostgreSQL brokers, ignored by NATS.
Broadcast and batch
- A broadcast or batch in which some parts failed throws
BroadcastFailedChannelsExceptionorBatchFailedCommandsException, whose$resultholds every part's outcome;$result->failedand$result->repliedare properties. A partial failure is never theApiExceptionof its parts, also with one channel. A failure as a whole throws its own exception, the network's say. retryableChannels()names the channels a retry should go to.broadcastWithRetryretries only those, under oneidempotencyKey, made when missing:maxAttempts,attemptTimeout- each attempt's own timeout, replacingrequestTimeoutfor it, the call'stimeoutstill ending it sooner - andonAttemptError, which gets aBroadcastAttemptand returns the wait or throws to stop.$client->newBatch(parallel: ...)has the client's methods, each returning aPending;send()once,BatchSentExceptionafter,BatchNotSentExceptionreading aPendingbefore. An invalid command fails the batch before sending, naming its index.- No automatic batching: a PHP request makes its calls one after another.
Pages and positions
historyAll,deviceListAll,deviceTopicListAll,userTopicListAllandmapReadStateAlltake the method's arguments and return aGeneratorof every item, a page read when the loop reaches it. The request is checked at the call:historyAllrefuses alimitbelow 1 andreverse.historyAllyieldsHistoryItems - the publication and its stream's epoch, withposition()- andmapReadStateAllyieldsMapStateItems - the entry and thestateposition it was read at.mapReadStateAllreads every page after the first at the first one's revision, so that a map reset while reading throwsUnrecoverablePositionExceptioninstead of mixing two states.- The iterators take
pageTimeout, bounding each page's request in place ofrequestTimeout, and notimeout: an iteration lasts as long as the loop. position()on the results ofpublish,history,mapReadState,mapReadStream,mapPublishandmapRemovegives theStreamPositionto read on from, assinceorrecoverSince.
Constants
- A class of string constants per field with listed values, named as the spec's table pins them:
Provider::FCM,Platform::IOS,Op::ADD,Status::DELIVERED,KeyMode::IF_NEW,FilterNodeOp::AND,Cmp::EQ; inProxy,FilterNodeOp,Cmp,Protocol::JSON,Encoding::BINARY,Allow::SUB,CapabilityMatch::REGEX,ChannelEventType::OCCUPIED,KeyMode. The fields stay strings: a value phpcent does not name is still sent.
Payloads
- Payloads are PHP values - arrays,
stdClass, scalars,JsonSerializable- orRawJson, sent as its text, also inside a value. Replies givestdClass, which goes back as the same JSON value,{}and{"0":"a"}included;Payload::toArray()for arrays. NaN, infinities, strings which are not UTF-8 and invalid JSON are refused before sending.
Tokens and JWKS
Token::connectionandToken::subscriptionwith every claim,profileincluded; HS256, RS256 (RSA keys of 2048 bits or more), ES256/384/512 (other curves refused) and EdDSA keys (Key::hmac,rsa,ecdsa,ed25519,fromPem). An expiry is required: a time, orExpiry::Never.Token::jwksand theJwksendpoint publish the public keys, for key rotation without restarting Centrifugo.- Centrifugo PRO server API tokens:
Token::api, andApiTokenSource, whosetokenmethod is a client'sbearerTokenFunc, reusing a token for half its lifetime.
Endpoints, shared poll, proxy, commands
- Endpoints client SDKs call:
ConnectionToken,SubscriptionToken,SharedPollSignature,Jwks. Their function throwsForbiddenExceptionto answer 403, which client SDKs report as theirUnauthorizedError. SharedPoll::signfor shared poll signatures, its expiry required.- A handler for every proxy type, generated from
proxy.proto: connect, refresh, subscribe, sub refresh, publish, RPC, map publish and remove, shared poll refresh, and Centrifugo PRO's channel state and cache empty notifications. The function returns a result, or throwsProxyErrororDisconnect; anything else is a 500 reported toonError. The docs say how to keep proxy endpoints reachable by Centrifugo only: a network only Centrifugo reaches, a secret header from the proxy'sstatic_headerschecked before the handler, TLS where the traffic leaves a trusted network. - Every handler and endpoint is a framework-free core with PSR-15 (
psr15(),handlePsr()) and plain PHP (serve()) adapters, with a body limit. Command::publish(...)and the other methods, for Centrifugo's async consumers: checked as a call is,methodandpayloadfor an outbox table,toJson()orjson_encodefor a whole message.
Observability and testing
onRequestgets aRequestInfoafter every request: method, commands, failed, duration, status, error.Pending::resolvedand the public constructors of results and partial-failure exceptions make test doubles; a fake PSR-18 client tests code using phpcent without Centrifugo.