symfony/ai-doctrine-message-store
symfony/ai-doctrine-message-store integrates Doctrine DBAL with Symfony AI Chat to provide database-backed message storage. It lets AI chat applications persist conversation messages through Doctrine, with resources and contributions handled in the main Symfony AI repository.
This is a Symfony AI bridge, not a Laravel package. For a Laravel developer, the honest starting point is: use it only if you are already running symfony/ai Agent/Chat components inside a Laravel app and want DBAL-backed conversation persistence. If you are building on Laravel-native AI tooling or Eloquent, a small native repository will usually be simpler.
Minimal steps:
Install DBAL and the bridge, pinned to the same Symfony AI minor version:
composer require doctrine/dbal symfony/ai-doctrine-message-store
Read the source before wiring anything:
vendor/symfony/ai-doctrine-message-store/src/DoctrineDBALMessageStore.phpcomposer.json for DBAL/Symfony AI version constraintsFind the expected table shape. The README is intentionally thin; the source is the documentation. Check the table name, column names, ID strategy, and whether messages are serialized objects or plain arrays. Do not assume a schema.
Create the table with a Laravel migration rather than letting DBAL create it in production. Keep the migration in your app so schema changes are reviewable.
Build a DBAL connection from Laravel’s database config and bind the store as a singleton:
use Doctrine\DBAL\DriverManager;
use Symfony\AI\DoctrineMessageStore\DoctrineDBALMessageStore;
$this->app->singleton(DoctrineDBALMessageStore::class, function () {
$pdo = DB::connection()->getPdo();
// Verify against your DBAL version; the 'pdo' parameter works on
// common DBAL 3.x/4.x setups, but confirm in vendor code.
$connection = DriverManager::getConnection(['pdo' => $pdo]);
return new DoctrineDBALMessageStore($connection);
});
First use case: persist one short agent conversation, then reload it in a second request and assert the message order and content. Do this against SQLite in tests and MySQL/Postgres in staging before trusting it.
Where to look first: the store class constructor, the interface it implements, and the Symfony AI message DTOs. The package is small enough that reading it end-to-end is faster than searching for docs.
Wrap the Symfony store behind an application interface. This is the most important Laravel pattern: do not let Symfony AI types leak through your app. Define a ConversationStore or MessageRepository contract, then adapt the bridge behind it.
interface ConversationStore
{
public function append(string $conversationId, array $messages): void;
public function load(string $conversationId, int $limit = 50): array;
}
final class DoctrineConversationStore implements ConversationStore
{
public function __construct(
private DoctrineDBALMessageStore $store,
) {}
public function append(string $conversationId, array $messages): void
{
// Delegate to the package; transform app DTOs to Symfony AI messages
// and back at this boundary.
}
public function load(string $conversationId, int $limit = 50): array
{
// Load and map to your own DTOs.
}
}
This keeps the rest of the Laravel app swappable to Eloquent, Redis, or a fake in tests.
Reuse Laravel’s PDO where possible. A separate DBAL connection means separate transaction state, locks, and query visibility. Passing Laravel’s PDO into DBAL avoids duplicate credentials and surprises. Test transaction behavior explicitly; SQLite especially will complain about locks if two connections write concurrently.
Persist after each agent turn, load before prompting. A typical flow:
Use jobs and console commands carefully. Queue payloads should contain conversation IDs and primitive data, never DBAL connections or serialized Symfony AI agent objects. Resolve the store inside handle().
Keep reporting queries separate from the write path. Laravel’s query builder or a read-only Eloquent model can query the same table for dashboards and analytics, provided your migration remains the source of truth. Do not let DBAL and Eloquent each try to own schema changes.
Test with in-memory SQLite, but do not rely on RefreshDatabase for DBAL writes. Run your migration, truncate the table between tests, and test reload ordering, empty conversations, and malformed stored payloads.
Use a decorator for cross-cutting behavior. Tenant scoping, PII redaction, encryption, caching, and audit logging belong in a decorator around the store, not in forked package code.
messages or ai_messages to avoid collisions.composer why doctrine/dbal and test the exact combination Laravel resolves. A version conflict with package constraints is plausible.DB::transaction() will not cover DBAL writes, and DB::listen/Telescope may not show DBAL queries. Prefer sharing the PDO; otherwise enable DBAL logging locally and treat the database table as the debugging source.conversation_id plus a timestamp/order column, paginate history loads, and archive old conversations. Use Redis for hot-path state if latency matters.DoctrineDBALMessageStore to add tenant filters, logging, redaction, or caching. A fork is justified only if the package becomes unmaintained and you need deep fixes.How can I help you explore Laravel packages today?