DBAL
Connection factory, supported DSN drivers, lazy connection, SSL/TLS, and advanced DBAL configuration.
DBAL
vortos-persistence-dbal provides Doctrine DBAL integration for the write side. It registers a shared Connection instance and the UnitOfWork transaction boundary.
What Gets Registered
Connection::class — shared, lazy, built from DATABASE_URL
UnitOfWork::class — transaction boundary
UnitOfWorkInterface — aliased to UnitOfWorkSupported Drivers
ConnectionFactory::fromDsn() maps DSN scheme to DBAL driver automatically:
| DSN Scheme | Driver | Database |
|---|---|---|
pgsql:// or postgres:// | pdo_pgsql | PostgreSQL |
mysql:// | pdo_mysql | MySQL / MariaDB |
sqlite:/// | pdo_sqlite | SQLite |
sqlsrv:// | pdo_sqlsrv | SQL Server |
oci8:// | oci8 | Oracle |
Standard Connection (DSN)
# .env
DATABASE_URL=pgsql://postgres:secret@write_db:5432/myapp$config->writeDsn($_ENV['DATABASE_URL']);The connection is lazy — no TCP handshake occurs until the first query. Container construction never fails due to the database being unreachable.
Advanced Connection (fromParams)
For configurations that a DSN string cannot express — SSL certificates, Unix sockets, custom PDO attributes:
use Vortos\PersistenceDbal\Connection\ConnectionFactory;
use Doctrine\DBAL\Connection;
// In a custom extension or service factory:
$connection = ConnectionFactory::fromParams([
'driver' => 'pdo_pgsql',
'host' => 'db.example.com',
'port' => 5432,
'dbname' => 'myapp',
'user' => 'app',
'password' => 'secret',
'driverOptions' => [
'sslmode' => 'verify-full',
'sslrootcert' => '/etc/ssl/certs/rds-ca.pem',
],
]);SSL/TLS for AWS RDS
$connection = ConnectionFactory::fromParams([
'driver' => 'pdo_pgsql',
'url' => $_ENV['DATABASE_URL'],
'driverOptions' => [
\PDO::PGSQL_ATTR_SSL_MODE => 'verify-full',
'sslrootcert' => '/etc/ssl/certs/rds-ca-2019-root.pem',
],
]);Unix Socket
$connection = ConnectionFactory::fromParams([
'driver' => 'pdo_pgsql',
'host' => '/var/run/postgresql',
'dbname' => 'myapp',
'user' => 'app',
]);Lazy Connection
The Connection is registered with ->setLazy(true):
Container constructed
│
├── Connection object created (immediately)
│ └── NO database connection yet
│
▼
First query executed
│
├── TCP handshake to PostgreSQL
├── Authentication
└── Connection establishedThis means your application boots instantly even if the database is temporarily unavailable. The connection is only established when the first query runs.
Connection is Shared
The Connection is registered with setShared(true) — the entire application uses a single instance. This is critical for transaction atomicity:
UnitOfWork::run() → beginTransaction() on Connection A
│
├── UserRepository::save() uses Connection A ← same transaction
└── OutboxWriter::store() uses Connection A ← same transaction
│
└── commit() on Connection A ← atomicIf the connection were not shared, each service would have its own transaction — atomicity would be lost.
Never setShared(false) on Connection
Changing Connection registration to setShared(false) breaks transaction atomicity silently. The aggregate save and outbox write would be in separate transactions — one could succeed while the other fails.
Connection is Public
Connection::class is registered as setPublic(true) so that other extensions (like MessagingExtension) can reference it via new Reference(Connection::class) across package boundaries.
Framework Table Namespace
Vortos separates its own tables from your application tables using a platform-specific namespace resolved at container compile time — zero runtime cost.
| Database | Mode | Example |
|---|---|---|
| PostgreSQL | Dedicated vortos schema | vortos.user_roles |
| MySQL, SQLite, SQL Server, Oracle | vortos_ prefix | vortos_user_roles |
The correct prefix is derived from your DATABASE_URL DSN automatically. No configuration is needed.
Opting Out of Schema Mode (PostgreSQL)
If you prefer prefix mode on PostgreSQL — for example, if your deployment does not allow creating new schemas — add ?vortos_prefix=true to your DSN:
# .env
DATABASE_URL=pgsql://postgres:secret@db:5432/myapp?vortos_prefix=trueThis is the only change required. All framework table names switch to vortos_ prefix automatically.
Container parameter
The resolved prefix is stored as the vortos.db.framework_table_prefix container parameter ('vortos.' or 'vortos_'). It is set once at compile time by DbalPersistenceExtension and baked into every service definition that references a framework table — there is no runtime branching.
PostgreSQL Schema Bootstrap
When using schema mode, vortos:migrate automatically runs CREATE SCHEMA IF NOT EXISTS vortos before executing any migrations. You do not need to create the schema manually.
vortos:migrate:fresh drops the vortos schema completely before re-running from scratch, giving a guaranteed clean state.
QueryBuilder
Always use createQueryBuilder() for custom queries in repositories — never raw SQL strings:
// CORRECT
$this->connection()->createQueryBuilder()
->select('*')
->from('users')
->where('email = :email')
->setParameter('email', $email)
->executeQuery()
->fetchAssociative();
// WRONG — never do this
$this->connection()->executeQuery(
"SELECT * FROM users WHERE email = '" . $email . "'"
);QueryBuilder handles parameter escaping, is type-safe, and works across all supported DBAL drivers.