atoms / symfony
Symfony bundle for Atoms: AtomsClient, the inbound callback stack, PSR-17/18 resolution, a Messenger queue bridge, and console wrappers around the atoms binary.
Package info
Type:symfony-bundle
pkg:composer/atoms/symfony
Requires
- php: ^8.3
- atoms/client: ^0.6
- psr/container: ^1.1 || ^2.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^2.0 || ^3.0
- symfony/config: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/http-foundation: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
- symfony/routing: ^6.4 || ^7.0
Requires (Dev)
None
Suggests
- guzzlehttp/guzzle: Default PSR-18 client used when no atoms.http_client / ClientInterface service is configured
- guzzlehttp/psr7: Default PSR-17 factory used when psr17_factory is not configured
- symfony/console: Registers the atoms:deploy / atoms:rollback / atoms:list console wrappers
- symfony/messenger: Routes AtomJob callbacks through Messenger (MessengerQueueBridge); without it, NullQueueBridge throws on first use
Provides
None
Conflicts
None
Replaces
None
README
The supported Symfony bundle for Atoms wires Atoms\Client\AtomsClient, the
inbound callback stack (HMAC verification, Methods dispatch, AtomJob
reconstruction), a Messenger-backed queue bridge, and thin console wrappers
around the atoms binary — all on top of atoms/client alone.
atoms/client is deliberately framework-free, so
this bundle depends on atoms/client and Symfony components only; it never
needs Atoms\Laravel\* or Illuminate\* (tests/LayeringTest.php is the
mechanical check, and docs/conventions.md the rule it enforces).
Install
config/bundles.php:
return [ // ... Atoms\Symfony\AtomsBundle::class => ['all' => true], ];
config/packages/atoms.yaml:
atoms: environment: '%env(APP_ENV)%' endpoint: https://atoms.your-subdomain.workers.dev # your own deployed Worker shared_secret: '%env(ATOMS_SHARED_SECRET)%' # base64 of 32 random bytes, identical on the Worker # shared_secret_previous: '%env(ATOMS_SHARED_SECRET_PREVIOUS)%' # add during a rotation overlap timeout: 10.0 max_attempts: 3 callback_path: /atoms/callback callback_timestamp_window: 300 # seconds of clock skew a callback's timestamp may deviate before rejection http_client: null # service id, or null to auto-detect / fall back to Guzzle psr17_factory: null # service id, or null to fall back to Guzzle (no auto-detection — see "What's wired") methods_classes: - App\Atoms\GameRoom\Methods # only needed for #[MethodsFor] overrides
shared_secret is the root of the app ↔ Worker boundary: the same base64 of
32 random bytes the Worker holds as ATOMS_SHARED_SECRET. Keep it on your
hosts — requests carry a bearer derived from it with HKDF-SHA256, and inbound
callbacks are verified with a second derived key; atoms token prints the
bearer for hand-issued requests. To rotate, set shared_secret_previous to
the outgoing value on both sides: callbacks signed under either secret verify
while the overlap lasts, and outbound requests use the current one.
Mount the callback route
Import the atoms resource type from your own routing, e.g.
config/routes/atoms.yaml:
atoms: resource: . type: atoms
There is no vendor path to import — Atoms\Symfony\Routing\AtomsRouteLoader
is registered as a routing.loader-tagged service and resolves the atoms
type itself. It always mounts at the bundle's current atoms.callback_path,
so reconfiguring that value moves the route with it; nothing else needs to
change.
What's wired
Atoms\Client\AtomsClient— the RPC stub-proxy client, service idatoms.client(public; theAtomsClient::classservice itself is private).- The callback stack —
HmacVerifier,InMemoryNonceStore,MethodsResolver,CallbackKernel— plusAtoms\Symfony\Controller\CallbackController, which converts SymfonyRequest/Responseto/from PSR-7 by hand against the plainServerRequestFactoryInterface/StreamFactoryInterface(nopsr/http-message-bridgedependency); routed automatically as described above. - A PSR-17 factory (
ServerRequestFactoryInterface+StreamFactoryInterfaceResponseFactoryInterfacein one —GuzzleHttp\Psr7\HttpFactoryandNyholm\Psr7\Factory\Psr17Factoryboth qualify) resolved, in order: the service id inpsr17_factory, else a bundledGuzzleHttp\Psr7\HttpFactory(clear exception ifguzzlehttp/psr7isn't installed). No auto-detection: PSR-17 spans several factory interfaces, so the bundle never guesses which app service should serve all of them — name it explicitly inpsr17_factoryor accept Guzzle. BacksCallbackKernel,CallbackControllerandAtomsClientalike. Resolved in a compiler pass so bundle registration order never matters.
- A PSR-18 client resolved, in order: the service id in
atoms.http_client, else an app-definedPsr\Http\Client\ClientInterfaceservice, else aGuzzleHttp\Client(clear exception ifguzzlehttp/guzzleisn't installed). Resolved in a compiler pass so bundle registration order never matters. Atoms\Client\Callback\QueueBridge:Atoms\Symfony\Messenger\MessengerQueueBridgewhensymfony/messengeris installed and the app has a message bus service, wrapping dispatchedAtomJobs asAtomJobMessage(normalized, JSON-safe constructor args only) and handling them back viaAtomJobHandler; otherwiseAtoms\Client\Callback\NullQueueBridge, which throws a clear "configure a QueueBridge" exception on first use.- Methods resolution: every
methods_classesentry is registered on theMethodsResolverby name (for#[MethodsFor]overrides) and as an autowired service, collected into aSymfony\Component\DependencyInjection\ServiceLocatorthatCallbackKernelconsults first — so a listed Methods class can itself take app services as constructor dependencies. Anything not listed is instantiated withnew $class(). - A logger:
CallbackKerneltakes the app'sloggerservice if one exists (Monolog via FrameworkBundle, or anything else bound to that id) andnullotherwise — never a hard dependency onpsr/log's presence being wired up. atoms:deploy,atoms:rollback,atoms:list— thin console wrappers that shell out to the realatomsbinary (discovered viavendor/bin/atoms, then$PATH, thenpackages/cli/bin/atoms); registered only whensymfony/consoleis present.
Supplying your own
Every auto-detected collaborator has an explicit override point:
- HTTP client — set
http_clientto a service id, or just register your ownPsr\Http\Client\ClientInterfaceservice and leave it null. Binding theatoms.http_clientservice id yourself outranks both, and is the way to override the resolution outright. - PSR-17 factory — set
psr17_factoryto a service id implementingServerRequestFactoryInterface,StreamFactoryInterfaceandResponseFactoryInterface. As with the HTTP client, bindingatoms.psr17_factoryyourself takes precedence over the config key. - Queue bridge — install
symfony/messengerand register a message bus, or bind your ownAtoms\Client\Callback\QueueBridgeimplementation to that service id directly. - Nonce store — the bundle wires
Atoms\Client\Callback\InMemoryNonceStore(process-local, not shared across workers); aliasAtoms\Client\Callback\NonceStoreto your own implementation (e.g. Redis-backed) for a multi-process deployment. - Methods classes — list them under
methods_classesto get container resolution (and#[MethodsFor]support); anything else resolves by naming convention withnew $class(). - Logger — register any service under the id
logger(FrameworkBundle does this for you when Monolog is installed).
Not yet supported
Methods classes resolve from the container only when listed under
methods_classes; anything not listed there is instantiated with
new $class() rather than autowired, even if the class itself would
otherwise be autowirable.
Development and support
This package is developed in the Atoms monorepo. Its standalone repository is a read-only distribution mirror; report issues and send pull requests to the monorepo. Full documentation is at docs.atomsphp.dev. Licensed under the MIT License.