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:
use SymfonyAIPlatformBridgeOpenAiFactory;
use SymfonyAIPlatformMessageMessage;
use SymfonyAIPlatformMessageMessageBag;
$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:
use SymfonyAIPlatformBridgeAnthropicFactory;
$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.
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:
use SymfonyAIPlatformBridgeAnthropicFactory as AnthropicFactory;
use SymfonyAIPlatformBridgeModelsDevModelCatalog;
use SymfonyAIPlatformBridgeOpenAiFactory as OpenAiFactory;
use SymfonyAIPlatformPlatform;
$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:
// 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:
// 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:
$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:
use SymfonyAIPlatformBridgeMistralFactory;
use SymfonyAIPlatformStructuredOutputPlatformSubscriber;
$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:
$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:
use SymfonyAIPlatformBridgeHuggingFaceFactory;
use SymfonyAIPlatformBridgeHuggingFaceTask;
$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:
# 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:
$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:
$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:
$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:
composer require symfony/ai-bundle
# 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:
use SymfonyAIPlatformMessageMessage;
use SymfonyAIPlatformMessageMessageBag;
use SymfonyAIPlatformPlatformInterface;
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.



