The release of the Symfony AI initiative marks a paradigm shift for the PHP ecosystem. We no longer need to rely on heavy external adapters or raw HTTP requests to build intelligent applications. With the AI Agent Component, Symfony provides a native, robust and “Symfony-way” framework for orchestrating Large Language Models (LLMs), managing context and executing tools.
In this article, we will explore the best practices for implementing symfony/ai-agent in a Symfony 7.4 application. We will focus on clean architecture, type safety and testability — ensuring your AI features are as reliable as your core business logic.
Installation & Architecture
While you can install the standalone symfony/ai-agent library, the Best Practice for a Symfony application is to use the AI Bundle. This bundle integrates the agent, platform and store components directly into the service container, enabling powerful configuration and dependency injection.
composer require symfony/ai-bundle symfony/ai-agent
Configuration
Instead of manually instantiating Agent classes in your controllers, configure them in config/packages/ai.yaml. This centralizes your model selection and API key management.
ai:
# Define the platforms (OpenAI, Anthropic, Ollama, etc.)
platform:
openai:
api_key: '%env(OPENAI_API_KEY)%'
# Define your Agents
agent:
default:
model: 'gpt-4o-mini'
# Best Practice: Define a system prompt here to keep code clean
prompt: 'You are a helpful Symfony assistant. concise and technical.'
# specialized agent
support_bot:
model: 'gpt-4o'
prompt: 'You are a customer support agent. Be polite and empathetic.'
Dependency Injection & Basic Usage
Never use new Agent(…) inside your application code. Rely on Symfony’s autowiring. The bundle aliases your configured agents to the AgentInterface.
namespace App\Service;
use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
final readonly class SupportAssistant
{
// Inject the specific agent if you have multiple, or the default AgentInterface
public function __construct(
#[Autowire(service: 'ai.agent.support_bot')]
private AgentInterface $agent
) {}
public function ask(string $userQuestion): string
{
// Use a MessageBag to maintain structure (System, User, Assistant)
$messages = new MessageBag(
Message::ofUser($userQuestion)
);
// The 'call' method is the primary entry point
$response = $this->agent->call($messages);
return $response->getContent();
}
}
The Power of Tools: Extending Intelligence
The true power of AI Agents lies in Tools — functions the AI can “call” to perform actions or retrieve data.
Best Practice: The #[AsTool] Attribute
Avoid manually registering tools array-by-array. Use the #[AsTool] attribute. This allows the component to automatically generate the JSON Schema required by the LLM.
Best Practice: Type Safety with Enums
One of the strongest features of the Symfony AI component is its ability to infer validation rules from PHP types. Use Backed Enums to strictly limit the choices an AI can make. This prevents the “hallucination” of invalid parameters.
namespace App\Enum;
enum OrderRegion: string
{
case US = 'us';
case EU = 'eu';
case ASIA = 'asia';
}
namespace App\AI\Tool;
use App\Enum\OrderRegion;
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(tags: ['ai.tool'])]
#[AsTool(
name: 'get_order_status',
description: 'Retrieves the current status of a customer order.'
)]
final readonly class OrderStatusTool
{
public function __invoke(
string $orderId,
OrderRegion $region,
): string {
return sprintf(
"Order %s in region %s is currently: SHIPPED",
$orderId,
$region->value
);
}
}
When you use this tool, the Agent component translates the OrderRegion enum into a strict JSON schema enum list for the LLM. If the LLM tries to call it with “Antarctica”, the component (or the LLM platform) will catch the validation error before your code even runs.
Advanced Validation with #[With]
For constraints that aren’t types (like regex patterns or ranges), use the #[With] attribute from the JsonSchema contract.
namespace App\AI\Tool;
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
use Symfony\AI\Platform\Contract\JsonSchema\Attribute\With;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(tags: ['ai.tool'])]
#[AsTool(
name: 'set_discount',
description: 'Sets a discount percentage for a product or order.'
)]
final readonly class DiscountTool
{
public function __invoke(
#[With(minimum: 0, maximum: 50)]
int $percentage
): string {
return sprintf("Discount set to %d%%.", $percentage);
}
}
Processing Pipelines: Input & Output
Sometimes you need to intercept messages before they go to the AI (e.g., to add dynamic context) or after they return (e.g., to sanitize output).
Context Injection (RAG Lite)
Use an Input Processor to inject relevant data into the context window dynamically.
namespace App\AI\Processor;
use App\Entity\User;
use Symfony\AI\Agent\Input;
use Symfony\AI\Agent\InputProcessorInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\SystemMessage;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(tags: ['ai.input_processor'])]
final readonly class UserContextProcessor implements InputProcessorInterface
{
public function __construct(private Security $security) {}
public function processInput(Input $input): void
{
/** @var User|null $user */
$user = $this->security->getUser();
if ($user) {
$input->setMessageBag(
(new MessageBag(
new SystemMessage(sprintf("Current user is %s (ID: %d)", $user->getEmail(), $user->getId()))
))->merge($input->getMessageBag())
);
}
}
}
Register this processor in your ai.yaml or wire it manually if you are building complex chains.
Testing: The MockAgent
Testing AI integrations is notoriously difficult due to non-deterministic outputs and API costs. The Symfony AI Agent component solves this with the MockAgent.
Best Practice: Never hit real APIs in your unit/feature tests.
namespace App\Tests\Service;
use App\Service\SupportAssistant;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Agent\MockAgent;
class SupportAssistantTest extends TestCase
{
public function testAskReturnsExpectedResponse(): void
{
$agent = new MockAgent([
'how do I reset my password?' => 'Go to settings and click reset.',
]);
$service = new SupportAssistant($agent);
$response = $service->ask('how do I reset my password?');
$this->assertEquals('Go to settings and click reset.', $response);
$agent->assertCalledWith('how do I reset my password?');
}
}
To verify your setup is working correctly in a Symfony 7.4 environment:
- Check Configuration: Run php bin/console debug:config ai to see your resolved agent configuration.
- Test the Agent: Create a simple console command src/Command/TestAgentCommand.php.
- Run: php bin/console app:test-agent. You should see a response from the LLM.
Conclusions
The symfony/ai-agent component brings the structure, stability and developer experience of Symfony to the chaotic world of AI development. By following these best practices — using the Bundle configuration, leveraging strict PHP types for Tools and utilizing MockAgent for testing — you can build production-ready AI applications today.
Don’t just write scripts; build Agents that are integrated, type-safe and maintainable.
Source Code: You can find the full implementation and follow the project’s progress on GitHub: [https://github.com/mattleads/AIAgentSample]
Let’s Connect!
If you found this helpful or have questions about the implementation, I’d love to hear from you. Let’s stay in touch and keep the conversation going across these platforms:
- LinkedIn: [https://www.linkedin.com/in/matthew-mochalkin/]
- X (Twitter): [https://x.com/MattLeads]
- Telegram: [https://t.me/MattLeads]
- GitHub: [https://github.com/mattleads]
Top comments (0)