Vortos
Persistence

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 UnitOfWork

Supported Drivers

ConnectionFactory::fromDsn() maps DSN scheme to DBAL driver automatically:

DSN SchemeDriverDatabase
pgsql:// or postgres://pdo_pgsqlPostgreSQL
mysql://pdo_mysqlMySQL / MariaDB
sqlite:///pdo_sqliteSQLite
sqlsrv://pdo_sqlsrvSQL Server
oci8://oci8Oracle

Standard Connection (DSN)

# .env
DATABASE_URL=pgsql://postgres:secret@write_db:5432/myapp
config/persistence.php
$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 established

This 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                  ← atomic

If 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.

DatabaseModeExample
PostgreSQLDedicated vortos schemavortos.user_roles
MySQL, SQLite, SQL Server, Oraclevortos_ prefixvortos_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=true

This 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.

On this page