symfony/ai-type-safe-platform
Symfony AI Type-Safe Platform adds type safety to AI-powered Symfony apps by mapping model responses to typed PHP objects. It integrates with Symfony AI and the Serializer to validate structured outputs, reducing brittle array handling and making LLM integrations more predictable.
Install: composer require symfony/ai-type-safe-platform. This is a Symfony component (no Laravel service provider, no auto-discovery), so you wire it yourself. Check the composer.json for the PHP and symfony/type-info constraints — 0.x releases move fast and requirements bump often.
Look here first:
Symfony\AI\Platform\PlatformInterface / PlatformFactory — the entry point you invoke models through.#2549) — this is the piece that turns a PHP class into a JSON schema and hydrates the model's answer back into that class. Read the bridge's README and its tests before anything else; the tests are the most accurate documentation at 0.x.Result/response object's conversion methods (asObject(), asJson(), asText()) — pick one per call site and stick to it.First use case (10 minutes): a typed extraction DTO.
final class SupportTicket
{
public function __construct(
public string $summary,
public TicketPriority $priority, // backed enum
/** @var list<string> */
public array $tags,
public ?string $customerEmail = null,
) {}
}
Then bind the platform once in a service provider and invoke it with that class as the response format. The bridge derives the schema from the constructor signature — no hand-written JSON schema.
Laravel wiring (do this before your first call): register the platform as a singleton in AppServiceProvider reading from config/services.php / .env. Do not build a new platform per request; the underlying HTTP client and model catalogue are worth reusing within a request and across a queue worker's lifetime.
?nullable types and /** @var list<Foo> */ docblocks all map cleanly. Anything you can express in symfony/type-info you get for free — that is the whole point of the TypeSafe bridge.TicketClassifier), inject the platform, and let controllers/jobs depend on your service. When the schema or model changes you edit one class, and you can swap in a fake in tests.Validator or a small domain check (enum already covers most of it) and treat a failure as a retry-with-repair case rather than an exception thrown at the user.$timeout and $tries explicitly on the job — Laravel's defaults are shorter than a large structured completion.#2566): score answers now require a probabilities field. If you were reading score/label only and tolerating a missing probabilities key, model your score DTO with a non-nullable probabilities array (map of label ⇒ float, or list of {label, probability}) and normalise/validate that the values sum to ~1.0 before acting on them. Do not silently ?? [] it — an absent field now signals a bad response you should retry.response_format variants) in config keyed by provider, not in the DTO.PlatformInterface that returns a canned JSON string for the hydrated DTO, or (b) snapshot the generated schema in a unit test so an accidental property rename that breaks your production prompt fails CI. A schema snapshot test is cheap and catches the most common regression in this package's usage."symfony/ai-type-safe-platform": "0.14.*" in composer.json. Minor releases here have shipped behaviour changes (v0.14.0 makes probabilities mandatory in score answers, and adds a whole bridge). A ^0.14 constraint will happily hand you v0.15 with a changed DTO contract.@var docblock descriptions, and cache repeated identical calls (Cache::remember on a hash of prompt + model + schema version) where the domain allows it.mixed, recursive types, and associative arrays without a documented value type may produce an invalid schema that one provider rejects with a 400 and another silently ignores. Test your DTO against every provider you actually deploy to.array without a docblock item type is a trap — you get [] back and hydration surprises. Always annotate with /** @var list<string> */ or a typed collection.config/services.php. php artisan about won't help; tinker and app(PlatformInterface::class) will.$result->asText() or the provider payload) before conversion. Once hydration throws, the raw JSON is gone. A Log::debug in your wrapper service at the raw-response boundary has saved more debugging sessions than any other line in this integration.config:cache or worker restart is required after changing env — the usual Laravel singleton-in-worker caveat, but it bites harder here because the failure is a silent wrong-model call.> 0.8 rule trigger anything irreversible.How can I help you explore Laravel packages today?