Database Connection
How database connections are created and managed
Connecting to Data
Every web app needs to store data. DALT.PHP supports two database drivers:
- SQLite - File-based, zero setup (default)
- PostgreSQL - Production-ready, powerful
Let's see how connections are created and managed.
The Database Configuration
Configuration lives in config/database.php:
return [
'database' => [
'driver' => $_ENV['DB_DRIVER'] ?? 'sqlite',
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
'port' => $_ENV['DB_PORT'] ?? 5432,
'dbname' => $_ENV['DB_NAME'] ?? 'dalt_php_app',
'username' => $_ENV['DB_USERNAME'] ?? 'postgres',
'password' => $_ENV['DB_PASSWORD'] ?? '',
'charset' => $_ENV['DB_CHARSET'] ?? 'utf8',
'database' => $_ENV['DB_DATABASE'] ?? base_path('database/app.sqlite'),
]
];Why Environment Variables?
$_ENV['DB_DRIVER'] ?? 'sqlite'This reads from .env file:
DB_DRIVER=sqlite
DB_DATABASE=database/app.sqliteBenefits:
- ✅ Different settings per environment (dev/staging/prod)
- ✅ Secrets not in code (passwords, hosts)
- ✅ Easy to change without editing code
The ?? operator:
- If
$_ENV['DB_DRIVER']exists → use it - If not → use
'sqlite'(default)
The DatabaseManager
The DatabaseManager class handles driver-specific setup:
class DatabaseManager
{
private $config;
private $database;
public static function create($config)
{
$manager = new self($config);
return $manager->getDatabase();
}
public function __construct($config)
{
$this->config = $config;
$this->setupDatabase();
}
private function setupDatabase()
{
$driver = $this->config['driver'] ?? 'sqlite';
switch ($driver) {
case 'sqlite':
$this->setupSQLite();
break;
case 'pgsql':
$this->setupPostgreSQL();
break;
default:
throw new \RuntimeException("Unsupported database driver: {$driver}");
}
$this->database = new Database($this->config);
$this->ensureTablesExist();
}
public function getDatabase()
{
return $this->database;
}
}The Creation Flow
1. Static factory method
public static function create($config)
{
$manager = new self($config);
return $manager->getDatabase();
}This is a "factory pattern" - a method that creates objects.
Why static?
- Can be called without an instance
- Clean API:
DatabaseManager::create($config)
2. Constructor does the work
public function __construct($config)
{
$this->config = $config;
$this->setupDatabase();
}Steps:
- Store config
- Setup driver-specific things
- Create Database instance
- Run migrations if needed
Driver Setup
Different databases need different preparation:
private function setupDatabase()
{
$driver = $this->config['driver'] ?? 'sqlite';
switch ($driver) {
case 'sqlite':
$this->setupSQLite();
break;
case 'pgsql':
$this->setupPostgreSQL();
break;
}
}SQLite Setup
private function setupSQLite()
{
if (isset($this->config['database']) && !str_starts_with($this->config['database'], '/')) {
$this->config['database'] = BASE_PATH . $this->config['database'];
}
$dbPath = $this->config['database'];
$dbDir = dirname($dbPath);
if (!is_dir($dbDir)) {
mkdir($dbDir, 0755, true);
}
if (!file_exists($dbPath)) {
touch($dbPath);
}
}What this does:
1. Make path absolute
if (!str_starts_with($this->config['database'], '/')) {
$this->config['database'] = BASE_PATH . $this->config['database'];
}Converts database/app.sqlite → /full/path/to/database/app.sqlite
2. Create directory
$dbDir = dirname($dbPath);
if (!is_dir($dbDir)) {
mkdir($dbDir, 0755, true);
}If database/ folder doesn't exist, create it.
3. Create empty file
if (!file_exists($dbPath)) {
touch($dbPath);
}SQLite needs an actual file to exist.
PostgreSQL Setup
private function setupPostgreSQL()
{
}PostgreSQL doesn't need file setup - it's a server.
The Database Class
This wraps PHP's PDO (PHP Data Objects):
class Database
{
protected $connection;
protected $statement;
public function getConnection(): PDO
{
return $this->connection;
}
public function __construct($config)
{
$dsn = $this->buildDsn($config);
$username = $config['username'] ?? null;
$password = $config['password'] ?? null;
try {
$this->connection = new PDO($dsn, $username, $password, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
} catch (PDOException $e) {
throw new \RuntimeException(
"Database connection failed...\n" .
"Driver: {$config['driver']}\n" .
"DSN: {$dsn}\n" .
"Error: " . $e->getMessage()
);
}
}
}What is PDO?
PDO is PHP's standard database interface. It works with many databases:
- SQLite
- PostgreSQL
- SQL Server
- Oracle
Benefits:
- Same API for all databases
- Prepared statements (security)
- Error handling
- Transactions
The Constructor Breakdown
1. Build DSN
$dsn = $this->buildDsn($config);DSN = Data Source Name. It's a connection string.
2. Get credentials
$username = $config['username'] ?? null;
$password = $config['password'] ?? null;SQLite doesn't need these, but PostgreSQL does.
3. Create PDO connection
$this->connection = new PDO($dsn, $username, $password, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);Options explained:
-
PDO::FETCH_ASSOC- Return rows as associative arrays['id' => 1, 'title' => 'Hello'] // Instead of: [0 => 1, 1 => 'Hello', 'id' => 1, 'title' => 'Hello'] -
PDO::ERRMODE_EXCEPTION- Throw exceptions on errors// Instead of returning false, throw an exception // Makes errors easier to catch and debug
4. Catch connection errors
catch (PDOException $e) {
throw new \RuntimeException(
"Database connection failed...\n" .
"Driver: {$config['driver']}\n" .
"DSN: {$dsn}\n" .
"Error: " . $e->getMessage()
);
}This provides helpful error messages:
Database connection failed...
Driver: pgsql
DSN: pgsql:host=127.0.0.1;port=5432;dbname=myapp
Error: SQLSTATE[08006] Connection refusedBuilding the DSN
The DSN format is different for each driver:
private function buildDsn($config)
{
$driver = $config['driver'] ?? 'sqlite';
switch ($driver) {
case 'pgsql':
return sprintf(
'pgsql:host=%s;port=%s;dbname=%s',
$config['host'],
$config['port'],
$config['dbname']
);
case 'sqlite':
return 'sqlite:' . $config['database'];
default:
throw new \Exception("Unsupported database driver: {$driver}");
}
}DSN Examples
PostgreSQL:
pgsql:host=127.0.0.1;port=5432;dbname=dalt_php_appSQLite:
sqlite:/full/path/to/database/app.sqliteAuto-Migration
After creating the database, the manager checks if tables exist:
private function ensureTablesExist()
{
try {
$this->database->query("SELECT 1 FROM users LIMIT 1");
} catch (\Exception $e) {
$msg = strtolower($e->getMessage());
if (str_contains($msg, 'no such table') || str_contains($msg, 'does not exist')) {
$this->runMigrations();
} else {
throw $e;
}
}
}How It Works
1. Try to query users table
$this->database->query("SELECT 1 FROM users LIMIT 1");If this succeeds, tables exist.
2. If it fails, run migrations
catch (\Exception $e) {
$this->runMigrations();
}This is "auto-migration" - first time you run the app, it creates tables automatically.
Why check users specifically?
- It's a core table every app needs
- Simple indicator that migrations ran
- If it exists, assume others do too
Binding to the Container
Remember from Part 3.1, the database is bound during bootstrap:
// framework/Core/bootstrap.php
$container->bind('Core\Database', function () use ($dbConfig) {
return DatabaseManager::create($dbConfig['database']);
});What happens:
- Closure captures
$dbConfig - When resolved, calls
DatabaseManager::create() - DatabaseManager:
- Sets up driver
- Creates Database instance
- Runs migrations if needed
- Returns the Database object
Usage anywhere:
$db = App::resolve(Database::class);Connection Lifecycle
App Starts
↓
Bootstrap runs
↓
Container binds Database (recipe only)
↓
... app runs ...
↓
Controller needs database
↓
App::resolve(Database::class)
↓
Container calls the closure
↓
DatabaseManager::create()
↓
Setup driver (create files, etc.)
↓
new Database($config)
↓
new PDO($dsn, ...)
↓
Connection established
↓
Check if tables exist
↓
Run migrations if needed
↓
Return Database instance
↓
Controller uses itKey Takeaways
- Config uses environment variables - Different per environment
- DatabaseManager handles setup - Driver-specific preparation
- Database wraps PDO - Standard PHP database interface
- DSN format varies by driver - Connection string
- Auto-migration on first run - Convenient for development
What's Good Here
✅ Supports multiple databases
✅ Clear error messages on connection failure
✅ Auto-migration for convenience
✅ Environment-based configuration
✅ Simple enough to understand and modify
Design Note
Features like connection pooling and retry logic are intentionally omitted. The container already provides singleton caching (one connection per request), which is sufficient for a learning framework. These optimizations would add complexity without teaching new concepts.
Next, explore how queries are executed and results are fetched.