DEV Community

Cover image for Building Intelligent PHP Applications: Best Practices for the Symfony AI Agent Component
Matt Mochalkin
Matt Mochalkin

Posted on

Building Intelligent PHP Applications: Best Practices for the Symfony AI Agent Component

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
Enter fullscreen mode Exit fullscreen mode

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.'
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode
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
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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?');
    }
}
Enter fullscreen mode Exit fullscreen mode

To verify your setup is working correctly in a Symfony 7.4 environment:

  1. Check Configuration: Run php bin/console debug:config ai to see your resolved agent configuration.
  2. Test the Agent: Create a simple console command src/Command/TestAgentCommand.php.
  3. 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:

Top comments (0)