Core Database Layer

Core Database Layer

The Core database layer provides a modular approach to database management with SQLite as the primary storage engine.

Architecture

Modular Database Design

Each module/domain has its own SQLite database file:

var/db/
├── user.sqlite      # User management and authentication
└── page.sqlite      # Page content and CMS data

Benefits:

  • Clear separation of concerns
  • Independent module development
  • Easier backup and migration
  • Reduced lock contention
  • Module-specific optimization

DatabaseConnectionFactory

Basic Usage

use Minimal\Core\Database\Connection\DatabaseConnectionFactory;

// Initialize with database directory
$factory = new DatabaseConnectionFactory('var/db');

// Get connection for specific module
$userDb = $factory->getConnection('user');   // var/db/user.sqlite
$pageDb = $factory->getConnection('page');   // var/db/page.sqlite

// Connections are pooled and reused
$sameUserDb = $factory->getConnection('user'); // Returns same instance

Configuration Options

// Standard file-based databases
$factory = new DatabaseConnectionFactory('var/db');

// In-memory database (for testing)
$testFactory = new DatabaseConnectionFactory(':memory:');

// Custom database names
$factory = new DatabaseConnectionFactory('var/db');
$customDb = $factory->getConnection('analytics', 'custom_analytics.sqlite');

Connection Features

Automatic Configuration:

  • WAL mode for better concurrency
  • Foreign key constraints enabled
  • UTF-8 encoding
  • Optimized pragma settings

Error Handling:

try {
    $db = $factory->getConnection('user');
} catch (RuntimeException $e) {
    // Handle connection errors
    error_log("Database connection failed: " . $e->getMessage());
}

MigrationRunner

Migration Structure

// migrations/user/001_create_users_table.php
<?php

return [
    'up' => "
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email VARCHAR(255) UNIQUE NOT NULL,
            username VARCHAR(100) UNIQUE NOT NULL,
            password_hash VARCHAR(255) NOT NULL,
            first_name VARCHAR(100),
            last_name VARCHAR(100),
            email_verified_at DATETIME,
            email_verification_token VARCHAR(255),
            password_reset_token VARCHAR(255),
            password_reset_expires_at DATETIME,
            role VARCHAR(50) DEFAULT 'user',
            status VARCHAR(20) DEFAULT 'active',
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
        );
        
        CREATE INDEX idx_users_email ON users(email);
        CREATE INDEX idx_users_username ON users(username);
        CREATE INDEX idx_users_status ON users(status);
    ",
    'down' => "
        DROP TABLE IF EXISTS users;
    "
];

Running Migrations

use Minimal\Core\Database\Migration\MigrationRunner;

$runner = new MigrationRunner($connectionFactory, 'migrations');

// Run migrations for specific module
$results = $runner->runMigrations('user');
foreach ($results as $migration => $result) {
    echo "Migration {$migration}: " . ($result ? 'SUCCESS' : 'FAILED') . "\n";
}

// Run all pending migrations
$allResults = $runner->runAllMigrations();

// Check migration status
$pending = $runner->getPendingMigrations('user');
$applied = $runner->getAppliedMigrations('user');

Creating Migrations

// Create new migration file
$migrationFile = $runner->createMigration('user', 'add_two_factor_auth');

// Generated file: migrations/user/002_add_two_factor_auth.php
<?php

return [
    'up' => "
        ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(255);
        ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT 0;
    ",
    'down' => "
        ALTER TABLE users DROP COLUMN two_factor_secret;
        ALTER TABLE users DROP COLUMN two_factor_enabled;
    "
];

Migration Tracking

Migrations are tracked in a migrations table:

CREATE TABLE migrations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    module VARCHAR(100) NOT NULL,
    migration VARCHAR(255) NOT NULL,
    executed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(module, migration)
);

QueryBuilder

Basic Queries

use Minimal\Core\Database\Query\QueryBuilder;

$builder = new QueryBuilder($pdo);

// Simple SELECT
$users = $builder->table('users')->get();

// SELECT with columns
$users = $builder->table('users')
    ->select(['id', 'email', 'username'])
    ->get();

// Single record
$user = $builder->table('users')
    ->where('id', '=', 123)
    ->first();

WHERE Conditions

// Single condition
$activeUsers = $builder->table('users')
    ->where('status', '=', 'active')
    ->get();

// Multiple conditions (AND)
$recentActiveUsers = $builder->table('users')
    ->where('status', '=', 'active')
    ->where('created_at', '>', '2024-01-01')
    ->get();

// Different operators
$builder->where('age', '>', 18)
        ->where('email', 'LIKE', '%@example.com')
        ->where('role', 'IN', ['admin', 'moderator']);

Ordering and Limiting

// Order by single column
$users = $builder->table('users')
    ->orderBy('created_at', 'DESC')
    ->get();

// Multiple order columns
$users = $builder->table('users')
    ->orderBy('last_name', 'ASC')
    ->orderBy('first_name', 'ASC')
    ->get();

// Pagination
$users = $builder->table('users')
    ->orderBy('id')
    ->limit(20)
    ->offset(40)
    ->get();

INSERT Operations

// Single insert
$userId = $builder->table('users')
    ->insert([
        'email' => 'john@example.com',
        'username' => 'johndoe',
        'password_hash' => password_hash('secret', PASSWORD_DEFAULT),
        'first_name' => 'John',
        'last_name' => 'Doe'
    ]);

// Batch insert
$userIds = $builder->table('users')
    ->insertBatch([
        [
            'email' => 'user1@example.com',
            'username' => 'user1',
            'password_hash' => password_hash('pass1', PASSWORD_DEFAULT)
        ],
        [
            'email' => 'user2@example.com',
            'username' => 'user2',
            'password_hash' => password_hash('pass2', PASSWORD_DEFAULT)
        ]
    ]);

UPDATE Operations

// Update single record
$affected = $builder->table('users')
    ->where('id', '=', 123)
    ->update([
        'last_login' => date('Y-m-d H:i:s'),
        'login_count' => 'login_count + 1' // Raw SQL
    ]);

// Update multiple records
$affected = $builder->table('users')
    ->where('status', '=', 'pending')
    ->where('created_at', '<', date('Y-m-d', strtotime('-30 days')))
    ->update(['status' => 'expired']);

DELETE Operations

// Delete single record
$deleted = $builder->table('users')
    ->where('id', '=', 123)
    ->delete();

// Delete multiple records
$deleted = $builder->table('users')
    ->where('status', '=', 'inactive')
    ->where('last_login', '<', date('Y-m-d', strtotime('-1 year')))
    ->delete();

Advanced Usage

// Count records
$count = $builder->table('users')
    ->where('status', '=', 'active')
    ->count();

// Check if record exists
$exists = $builder->table('users')
    ->where('email', '=', 'test@example.com')
    ->exists();

// Raw SQL in SELECT
$users = $builder->table('users')
    ->select(['*', 'UPPER(username) as username_upper'])
    ->get();

// Complex WHERE with raw SQL
$builder->whereRaw('DATE(created_at) = ?', [date('Y-m-d')]);

Best Practices

Connection Management

// Use dependency injection
class UserRepository
{
    public function __construct(
        private DatabaseConnectionFactory $connectionFactory
    ) {}
    
    private function getConnection(): PDO
    {
        return $this->connectionFactory->getConnection('user');
    }
}

Transaction Handling

$db = $factory->getConnection('user');

try {
    $db->beginTransaction();
    
    // Multiple operations
    $builder->table('users')->insert($userData);
    $builder->table('user_profiles')->insert($profileData);
    
    $db->commit();
} catch (Exception $e) {
    $db->rollBack();
    throw $e;
}

Error Handling

try {
    $users = $builder->table('users')->get();
} catch (PDOException $e) {
    // Log database errors
    error_log("Database query failed: " . $e->getMessage());
    
    // Return empty result or throw application exception
    return [];
}

Performance Optimization

// Use indexes for frequently queried columns
"CREATE INDEX idx_users_email ON users(email);"
"CREATE INDEX idx_users_status_created ON users(status, created_at);"

// Use LIMIT for large datasets
$users = $builder->table('users')
    ->limit(1000)
    ->get();

// Use specific columns instead of SELECT *
$users = $builder->table('users')
    ->select(['id', 'email', 'username'])
    ->get();

Testing

Test Database Setup

class DatabaseTestCase extends TestCase
{
    protected DatabaseConnectionFactory $factory;
    
    protected function setUp(): void
    {
        $this->factory = new DatabaseConnectionFactory(':memory:');
        $this->runTestMigrations();
    }
    
    private function runTestMigrations(): void
    {
        $runner = new MigrationRunner($this->factory, 'tests/migrations');
        $runner->runMigrations('user');
    }
}

Query Builder Testing

class QueryBuilderTest extends DatabaseTestCase
{
    public function testInsertAndSelect(): void
    {
        $db = $this->factory->getConnection('user');
        $builder = new QueryBuilder($db);
        
        $userId = $builder->table('users')
            ->insert(['email' => 'test@example.com']);
            
        $this->assertIsInt($userId);
        
        $user = $builder->table('users')
            ->where('id', '=', $userId)
            ->first();
            
        $this->assertEquals('test@example.com', $user['email']);
    }
}

Troubleshooting

Common Issues

Database Lock Errors:

// Enable WAL mode (done automatically)
$db->exec('PRAGMA journal_mode=WAL;');

// Set busy timeout
$db->exec('PRAGMA busy_timeout=30000;');

Permission Issues:

// Check directory permissions
if (!is_writable('var/db')) {
    throw new RuntimeException('Database directory is not writable');
}

Migration Failures:

// Check migration syntax
$migration = include 'migrations/user/001_create_users.php';
if (!isset($migration['up']) || !isset($migration['down'])) {
    throw new RuntimeException('Invalid migration format');
}