A Symfony bundle that wires AGTP handlers into your Symfony app's
service container. The pattern matches the rest of Symfony: register
your handler as a service, tag it with agtp.endpoint, and you're
done.
Pairs with:
agtp-php— the language library and themod_phpruntime client (wrapped by theagtp:serveSymfony Console command).
Same reasons that apply to Drupal — see the agtp-drupal README for the full pitch. The short version:
- One Symfony kernel boot per worker, not per request. AGTP
handlers run inside a long-lived
bin/console agtp:serveworker. Kernel boot is paid once. Subsequent requests are dispatch + handler logic. - Identity, scope, and attribution at the protocol level. Your
handler receives
$ctx->agentIdalready verified and$ctx->authorityScopealready scope-checked. The daemon emits a signed Attribution-Record per invocation. - No conflict with your HTTP app. AGTP runs on 4480 via
agtpd. Your HTTP controllers continue to answer on 80/443 as before.
- Symfony 6.4+ or 7+
- PHP 8.1+
agtpdrunning locally or on the same host
| Environment | Long-lived workers? | Status |
|---|---|---|
| Self-hosted (VPS, bare metal, Kubernetes, Docker Compose) | Yes — systemd, Supervisor, k8s Deployment |
Supported |
| Platform.sh | Yes — native worker containers (same pattern as Symfony Messenger workers) | Should work; recipe pending |
| Heroku-style PaaS with worker dynos | Yes — declare in Procfile |
Should work |
| Serverless / FaaS (Lambda, Cloud Run jobs) | No | Not supported. AGTP needs a persistent process. |
The bundle is self-hosted-first. The Symfony Messenger
deployment model translates almost verbatim: anywhere you can run a
bin/console messenger:consume worker, you can run bin/console agtp:serve.
composer require agtp/agtp-symfonyThen enable the bundle in config/bundles.php:
return [
// ...
Agtp\Symfony\AgtpBundle::class => ['all' => true],
];(Symfony Flex auto-enables the bundle via the extra.symfony.bundles
declaration in composer.json; on a non-Flex setup, add the line
manually.)
namespace App\Agtp;
use Agtp\AgtpEndpoint;
use Agtp\EndpointContext;
use Agtp\EndpointError;
use Agtp\EndpointResponse;
use Doctrine\ORM\EntityManagerInterface;
final class RoomHandlers
{
public function __construct(
private readonly EntityManagerInterface $em,
) {}
#[AgtpEndpoint(
method: 'BOOK',
path: '/room',
errors: ['room_unavailable'],
requiredScopes: ['booking:write'],
)]
public function book(EndpointContext $ctx): EndpointResponse|EndpointError
{
$room = $this->em->getRepository(Room::class)->findOneBy([
'type' => $ctx->input['room_type'] ?? 'double',
]);
if ($room === null) {
return new EndpointError(
code: 'room_unavailable',
message: 'No rooms available.',
details: ['room_type' => $ctx->input['room_type'] ?? null],
);
}
return new EndpointResponse(body: [
'reservation_id' => 'res-' . $room->getId() . '-' . $ctx->agentId,
]);
}
}In config/services.yaml:
services:
App\Agtp\RoomHandlers:
arguments:
$em: '@doctrine.orm.entity_manager'
tags:
- { name: agtp.endpoint }Or, with Symfony's autoconfiguration, tag the class via attribute:
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('agtp.endpoint')]
final class RoomHandlers { /* ... */ }After authoring handlers, project the #[AgtpEndpoint] attributes
into daemon-side endpoint TOML files:
# Write one TOML per handler into the agtpd endpoints directory
bin/console agtp:export-manifest --output=/etc/agtpd/endpoints
# Or preview to stdout
bin/console agtp:export-manifest --dry-runThe attribute is the source of truth. Re-run the command after every handler change.
bin/console agtp:serve --gateway-socket=/var/run/agtpd/gateway.sockProduction via systemd:
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/example.com
ExecStart=/usr/bin/php bin/console agtp:serve --gateway-socket=/var/run/agtpd/gateway.sock
Environment=APP_ENV=prod
Restart=on-failure
RestartSec=5sFor higher concurrency, run multiple unit copies. agtpd accepts
multiple module connections.
use Agtp\Testing;
public function testBookSuccess(): void
{
$em = $this->createMock(EntityManagerInterface::class);
// ... stub repository etc.
$handler = new RoomHandlers($em);
$ctx = Testing::makeContext(input: ['room_type' => 'double']);
$response = Testing::assertOk($handler->book($ctx));
$this->assertArrayHasKey('reservation_id', $response->body);
}- Does not route AGTP traffic through Symfony's HTTP kernel.
- Does not expose handlers to anonymous traffic; authentication
happens at
agtpd. - Does not provide an admin UI.
agtp-php— the underlying PHP library and runtimeagtp-drupal— Drupal equivalent (Drupal's DI is forked from Symfony's, so the patterns are nearly identical)