Symfony AI is approaching its 1.0 release — so let’s take a tour. In this first post of this series, we introduce the Platform component — the foundation layer that connects your PHP application to the world of AI.


The AI landscape is fragmented. Every provider ships its own API, its own SDK, its own conventions. If you've ever wired up an LLM integration in a PHP project, you know the pain: vendor-specific code scattered everywhere, tightly coupled to a single provider, and a rewrite waiting the moment you want to switch models.

The goal of Symfony AI’s Platform component is to solve this by providing a single, unified interface to all of them. And not only LLMs, but AI model inference in general.

Vendor-Agnostic by Design

At its core, the Platform is a thin abstraction over provider APIs. You configure a platform once, and every call goes through the same invoke() method — regardless of what’s behind it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\AI\Platform\Bridge\OpenAi\Factory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

$platform = Factory::createPlatform($apiKey);

$messages = new MessageBag(
    Message::forSystem('You are a helpful assistant.'),
    Message::ofUser('What is the Symfony framework?'),
);

$result = $platform->invoke('gpt-5.5', $messages);

echo $result->asText();

That single example already shows the whole mental model — three building blocks that reappear in every snippet below. You assemble a MessageBag of Message objects, hand it to a Platform through invoke(), and read back a Result whose as*() methods turn the response into what you need: text, a typed object, a file, or a stream. Learn those three nouns once and every provider, every modality works the same way.

Want to switch to Anthropic? Swap the factory and the model name:

1
2
3
4
use Symfony\AI\Platform\Bridge\Anthropic\Factory;

$platform = Factory::createPlatform($apiKey);
$result = $platform->invoke('claude-opus-4-8', $messages);

Your application code — the messages, the options, the result handling — stays identical. This isn’t just convenience; it’s an architectural boundary that keeps your domain logic free from vendor lock-in.

The list of supported platforms is extensive: OpenAI, Anthropic, Google Gemini, Azure OpenAI, AWS Bedrock, Mistral, Ollama, ElevenLabs, Cartesia, Decart, any OpenAI-compatible endpoint through the Generic or Open Responses bridge, and more.

Decoupled Model Catalogs with models.dev

AI models change faster than library releases — new ones appear almost daily, old ones get deprecated — so hardcoding model lists inside a framework is a maintenance trap. The fix is to decouple the model lifecycle from the framework lifecycle. Symfony AI does this with the models.dev bridge, which comes in two parts: the symfony/models-dev data package ships a daily-updated snapshot of the models.dev registry as a plain JSON file, and the symfony/ai-models-dev-platform bridge turns that data into a live model catalog. New models arrive with a plain composer update — no API calls at runtime, no catalogs to maintain by hand.

1
composer require symfony/ai-models-dev-platform symfony/models-dev

This becomes especially powerful in multi-provider setups. Each provider encapsulates its own connection — API key, HTTP client, model catalog — and a single Platform routes every call to the right one automatically:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\AI\Platform\Bridge\Anthropic\Factory as AnthropicFactory;
use Symfony\AI\Platform\Bridge\ModelsDev\ModelCatalog;
use Symfony\AI\Platform\Bridge\OpenAi\Factory as OpenAiFactory;
use Symfony\AI\Platform\Platform;

$platform = new Platform([
    OpenAiFactory::createProvider($openAiApiKey, modelCatalog: new ModelCatalog('openai')),
    AnthropicFactory::createProvider($anthropicApiKey, modelCatalog: new ModelCatalog('anthropic')),
]);

$platform->invoke('gpt-5.5', $messages);         // → OpenAI
$platform->invoke('claude-opus-4-8', $messages); // → Anthropic

No manual routing, no model-to-provider mapping: the catalog knows which provider serves which model, and the router does the rest. The same seam opens the door to load balancing, failover, and input-based model selection — all at the Platform level.

A continuously updated catalog is only one of several ways to keep models current — you can also override a single model per call or declare models through the bundle configuration. The Working with Model Catalogs guide walks through all three approaches and when to reach for each.

Multi-Modal: Beyond Text

Modern AI models handle more than text. They analyze images, transcribe audio, generate speech, and process PDFs. Symfony AI treats all of these as first-class content types:

1
2
3
4
5
6
7
8
9
10
// Image analysis
$messages = new MessageBag(
    Message::ofUser(
        'Describe this image',
        Image::fromFile('/path/to/photo.jpg'),
    ),
);
$result = $platform->invoke('gpt-5.5', $messages);

echo $result->asText();

For speech generation and transcription, dedicated models like ElevenLabs and OpenAI Whisper are available through the same invoke() interface — no MessageBag needed, just pass the content directly:

1
2
3
4
5
6
7
8
9
10
// Text to Speech
$result = $platform->invoke('eleven_multilingual_v2', new Text('Hello!'), [
    'voice' => 'pqHfZKP75CvOlQylNhV4', // Voice "Bill"
]);
$result->asFile('output.mp3');

// Speech to Text
$result = $platform->invoke('whisper-1', Audio::fromFile('recording.mp3'));

echo $result->asText();

No different libraries for different modalities — one API, one mental model.

Embeddings: From Text to Vectors

Not every model returns a chat response. Embedding models turn text into numeric vectors that capture meaning — the foundation of semantic search, clustering, and retrieval-augmented generation (RAG). They run through the exact same invoke() call; only the result accessor changes:

1
2
3
4
$result = $platform->invoke('text-embedding-3-small', 'Once upon a time...');

$vectors = $result->asVectors();
echo $vectors[0]->getDimensions(); // e.g. 1536

Those vectors are what feed the Store component to build RAG pipelines — a topic we’ll come back to later in this series.

Structured Output: LLMs That Return PHP Objects

One of the most practical features for application developers is structured output. Instead of parsing JSON strings yourself, you can tell the platform to return a typed PHP object. Describe the shape you want as a plain PHP class — here a MathReasoning with typed properties for the solution steps and the final answer — and hand it over as the response_format:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\AI\Platform\Bridge\Mistral\Factory;
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new PlatformSubscriber());

$platform = Factory::createPlatform($apiKey, eventDispatcher: $dispatcher);

$messages = new MessageBag(
    Message::forSystem('You are a helpful math tutor.'),
    Message::ofUser('How can I solve 8x + 7 = -23'),
);

$result = $platform->invoke('mistral-small-latest', $messages, [
    'response_format' => MathReasoning::class,
]);

$reasoning = $result->asObject(); // Returns a MathReasoning instance

The platform automatically generates a JSON Schema from your PHP class, passes it to the LLM, and deserializes the response back into a typed object. This works with both PHP classes and array structures.

Even more powerful: you can pass an existing object instance — here a City with only its name set — to have the AI populate just the missing fields:

1
2
3
4
5
6
7
8
9
$city = new City(name: 'Berlin');

$result = $platform->invoke('gpt-5.5', $messages, [
    'template_vars' => ['city' => $city],
    'response_format' => $city,
]);

// Same instance, now with populated fields
assert($city === $result->asObject());

This is invaluable whenever the LLM’s output needs to feed directly into your application logic — data extraction, classification, enrichment.

Hugging Face: Millions of Models at Your Fingertips

While the major providers cover general-purpose needs, sometimes you need something specialized — a fine-tuned sentiment classifier, an object detection model, or a domain-specific text generator. Hugging Face hosts more than a million models for every AI task imaginable, and Symfony AI gives you direct access through its Hugging Face bridge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\AI\Platform\Bridge\HuggingFace\Factory;
use Symfony\AI\Platform\Bridge\HuggingFace\Task;

$platform = Factory::createPlatform($apiKey);

// Sentiment analysis with a specialized financial model
$result = $platform->invoke('ProsusAI/finbert', 'Revenue exceeded expectations.', [
    'task' => Task::TEXT_CLASSIFICATION,
]);
dump($result->asObject());

// Image generation with Stable Diffusion
$result = $platform->invoke(
    'stabilityai/stable-diffusion-xl-base-1.0',
    'Astronaut riding a horse',
    ['task' => Task::TEXT_TO_IMAGE],
);
echo $result->asDataUri();

Hugging Face offers a free tier for their Inference API, so you can experiment at no cost. And to help you find the right model, Symfony AI ships with CLI commands to list and filter available models:

1
2
3
4
5
6
# List warm models for a specific task
php huggingface/_model.php ai:huggingface:model-list \
    --task=object-detection --warm

# Get detailed model information
php huggingface/_model.php ai:huggingface:model-info google/vit-base-patch16-224

Streaming, Token Usage, and Concurrency

A few more features round out the Platform. Streaming lets you display responses token by token, exactly like ChatGPT does:

1
2
3
4
5
$result = $platform->invoke('gpt-5.5', $messages, ['stream' => true]);

foreach ($result->asTextStream() as $chunk) {
    echo $chunk;
}

And token usage tracking gives you visibility into costs:

1
2
3
4
5
6
$result = $platform->invoke('gpt-5.5', $messages);
$usage = $result->getMetadata()->get('token_usage');

echo $usage->getPromptTokens();      // Input tokens
echo $usage->getCompletionTokens();  // Output tokens
echo $usage->getTotalTokens();       // Grand total

Where the provider reports them, you also get cached tokens, remaining rate-limit tokens, and thinking/reasoning tokens.

Finally, concurrency comes for free. invoke() returns immediately with a lazy result; the HTTP response is only awaited the moment you read it. Fire off a batch of calls in a loop, then read them back — they run in parallel without any extra plumbing:

1
2
3
4
5
6
7
8
$results = [];
foreach (range('A', 'D') as $letter) {
    $results[] = $platform->invoke('gpt-5.5', $messages->with(Message::ofUser($letter)));
}

foreach ($results as $result) {
    echo $result->asText().\PHP_EOL; // responses resolved as you read them
}

Symfony Integration: The AI Bundle

Install the AI Bundle and configure your preferred platform:

1
composer require symfony/ai-bundle
1
2
3
4
5
# config/packages/ai.yaml
ai:
    platform:
        openai:
            api_key: '%env(OPENAI_API_KEY)%'

The bundle registers the configured platform as a service, so you can autowire it straight into your own services through the constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\PlatformInterface;

final class AssistantService
{
    public function __construct(
        private readonly PlatformInterface $platform,
    ) {
    }

    public function ask(string $question): string
    {
        $messages = new MessageBag(
            Message::forSystem('You are a helpful assistant.'),
            Message::ofUser($question),
        );

        return $this->platform->invoke('gpt-5.5', $messages)->asText();
    }
}

The bundle does much more than wire up a single platform — see the AI Bundle documentation for the full set of configuration options.

That’s it. You now have a vendor-agnostic, multi-modal AI platform inside your Symfony application, ready to send prompts, analyze images, generate speech, and extract structured data.

Every snippet in this post maps to a runnable example — grab the examples directory and try them against your own API keys. And for the full API, options, and design rationale, head to the Platform component reference.