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.1
- 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
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
README
The supported Symfony bundle for Atoms wires Atoms\Client\AtomsClient, the
inbound callback stack (Ed25519 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 (integration-plan §5.3), 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 api_key: '%env(ATOMS_API_KEY)%' # null when the Worker's ATOMS_APP_KEY is unset timeout: 10.0 max_attempts: 3 platform_public_key: '%env(ATOMS_PLATFORM_PUBLIC_KEY)%' 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
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 —
Ed25519Verifier,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. - PSR-17 factory — set
psr17_factoryto a service id implementingServerRequestFactoryInterface,StreamFactoryInterfaceandResponseFactoryInterface. - 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.