Skip to content

Database

Lalaz provides a powerful database layer with connection pooling, query building, schema management, and an elegant ORM. It supports MySQL, PostgreSQL, and SQLite out of the box.

Configuration

Configure your database connection in config/database.php:

php
<?php declare(strict_types=1);

return [
    'database' => [
        'driver' => env('DB_CONNECTION', 'sqlite'),

        'connections' => [
            'sqlite' => [
                'database' => env('DB_DATABASE', __DIR__ . '/../storage/database.sqlite'),
            ],

            'mysql' => [
                'host' => env('DB_HOST', '127.0.0.1'),
                'port' => env('DB_PORT', 3306),
                'database' => env('DB_DATABASE', 'lalaz'),
                'username' => env('DB_USERNAME', 'root'),
                'password' => env('DB_PASSWORD', ''),
                'charset' => 'utf8mb4',
                'collation' => 'utf8mb4_unicode_ci',
            ],

            'postgres' => [
                'host' => env('DB_HOST', '127.0.0.1'),
                'port' => env('DB_PORT', 5432),
                'database' => env('DB_DATABASE', 'lalaz'),
                'username' => env('DB_USERNAME', 'postgres'),
                'password' => env('DB_PASSWORD', ''),
                'schema' => 'public',
            ],
        ],

        'pool' => [
            'min' => 0,
            'max' => 5,
            'timeout_ms' => 5000,
        ],
    ],
];

Environment Variables

dotenv
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret

Query Builder

The query builder provides a fluent interface for building and executing database queries.

Basic Queries

php
use Lalaz\Database\Connection;

// Get all records
$users = $connection->table('users')->get();

// Get first record
$user = $connection->table('users')->first();

// Get specific columns
$users = $connection->table('users')
    ->select('id', 'name', 'email')
    ->get();

// Count records
$count = $connection->table('users')->count();

Where Clauses

php
// Basic where
$users = $connection->table('users')
    ->where('status', 'active')
    ->get();

// With operator
$users = $connection->table('users')
    ->where('age', '>=', 18)
    ->get();

// Multiple conditions
$users = $connection->table('users')
    ->where('status', 'active')
    ->where('role', 'admin')
    ->get();

// Or conditions
$users = $connection->table('users')
    ->where('role', 'admin')
    ->orWhere('role', 'moderator')
    ->get();

// Array of conditions
$users = $connection->table('users')
    ->where([
        'status' => 'active',
        'verified' => true,
    ])
    ->get();

Advanced Where Clauses

php
// Where In
$users = $connection->table('users')
    ->whereIn('id', [1, 2, 3])
    ->get();

// Where Not In
$users = $connection->table('users')
    ->whereNotIn('status', ['banned', 'suspended'])
    ->get();

// Where Null
$users = $connection->table('users')
    ->whereNull('deleted_at')
    ->get();

// Where Not Null
$users = $connection->table('users')
    ->whereNotNull('email_verified_at')
    ->get();

// Where Between
$users = $connection->table('users')
    ->whereBetween('age', [18, 65])
    ->get();

// Where Exists
$users = $connection->table('users')
    ->whereExists(fn($query) => $query
        ->from('orders')
        ->whereColumn('orders.user_id', 'users.id')
    )
    ->get();

// Raw Where
$users = $connection->table('users')
    ->whereRaw('YEAR(created_at) = ?', [2024])
    ->get();

Nested Where Groups

php
$users = $connection->table('users')
    ->where('status', 'active')
    ->where(function ($query) {
        $query->where('role', 'admin')
              ->orWhere('role', 'moderator');
    })
    ->get();

// SQL: SELECT * FROM users WHERE status = 'active' AND (role = 'admin' OR role = 'moderator')

Ordering and Limiting

php
// Order by
$users = $connection->table('users')
    ->orderBy('name', 'asc')
    ->get();

// Multiple order by
$users = $connection->table('users')
    ->orderBy('role', 'asc')
    ->orderBy('name', 'asc')
    ->get();

// Order by descending
$users = $connection->table('users')
    ->orderByDesc('created_at')
    ->get();

// Limit and offset
$users = $connection->table('users')
    ->limit(10)
    ->offset(20)
    ->get();

// Latest / Oldest
$users = $connection->table('users')
    ->latest('created_at')
    ->get();

Joins

php
// Inner join
$posts = $connection->table('posts')
    ->join('users', 'posts.user_id', '=', 'users.id')
    ->select('posts.*', 'users.name as author')
    ->get();

// Left join
$users = $connection->table('users')
    ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
    ->select('users.*', $connection->raw('COUNT(posts.id) as post_count'))
    ->groupBy('users.id')
    ->get();

// Right join
$posts = $connection->table('posts')
    ->rightJoin('users', 'posts.user_id', '=', 'users.id')
    ->get();

// Cross join
$sizes = $connection->table('sizes')
    ->crossJoin('colors')
    ->get();

// Advanced join with closure
$posts = $connection->table('posts')
    ->join('users', function ($join) {
        $join->on('posts.user_id', '=', 'users.id')
             ->where('users.status', '=', 'active');
    })
    ->get();

Aggregates

php
$count = $connection->table('users')->count();
$max = $connection->table('orders')->max('total');
$min = $connection->table('orders')->min('total');
$avg = $connection->table('orders')->avg('total');
$sum = $connection->table('orders')->sum('total');

Grouping

php
$stats = $connection->table('orders')
    ->select('status', $connection->raw('COUNT(*) as count'))
    ->groupBy('status')
    ->get();

// With having
$users = $connection->table('users')
    ->select('country', $connection->raw('COUNT(*) as user_count'))
    ->groupBy('country')
    ->having('user_count', '>', 100)
    ->get();

Insert

php
// Single insert
$connection->table('users')->insert([
    'name' => 'John Doe',
    'email' => '[email protected]',
]);

// Get inserted ID
$id = $connection->table('users')->insertGetId([
    'name' => 'John Doe',
    'email' => '[email protected]',
]);

// Batch insert
$connection->table('users')->insert([
    ['name' => 'John', 'email' => '[email protected]'],
    ['name' => 'Jane', 'email' => '[email protected]'],
]);

Update

php
// Update matching records
$affected = $connection->table('users')
    ->where('id', 1)
    ->update(['status' => 'active']);

// Increment / Decrement
$connection->table('users')
    ->where('id', 1)
    ->increment('login_count');

$connection->table('products')
    ->where('id', 1)
    ->decrement('stock', 5);

Delete

php
// Delete matching records
$deleted = $connection->table('users')
    ->where('status', 'inactive')
    ->delete();

// Delete by ID
$connection->table('users')
    ->where('id', 1)
    ->delete();

Raw Queries

php
// Raw select
$users = $connection->select('SELECT * FROM users WHERE status = ?', ['active']);

// Raw insert
$connection->insert('INSERT INTO users (name, email) VALUES (?, ?)', ['John', '[email protected]']);

// Raw update
$affected = $connection->update('UPDATE users SET status = ? WHERE id = ?', ['active', 1]);

// Raw delete
$deleted = $connection->delete('DELETE FROM users WHERE status = ?', ['inactive']);

// Raw expressions in queries
$users = $connection->table('users')
    ->select('*', $connection->raw('DATEDIFF(NOW(), created_at) as days_since_joined'))
    ->get();

Transactions

php
// Using callback
$connection->transaction(function ($connection) {
    $connection->table('accounts')
        ->where('id', 1)
        ->decrement('balance', 100);

    $connection->table('accounts')
        ->where('id', 2)
        ->increment('balance', 100);
});

// Manual transactions
$connection->beginTransaction();

try {
    $connection->table('accounts')->where('id', 1)->decrement('balance', 100);
    $connection->table('accounts')->where('id', 2)->increment('balance', 100);

    $connection->commit();
} catch (\Exception $e) {
    $connection->rollBack();
    throw $e;
}

Schema Builder

Create and modify database tables using the schema builder.

Creating Tables

php
use Lalaz\Database\Schema\SchemaBuilder;

$schema->create('users', function ($table) {
    $table->increments('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->string('password');
    $table->boolean('is_admin')->default(false);
    $table->timestamps();
});

Available Column Types

php
$schema->create('examples', function ($table) {
    // Auto-incrementing primary key
    $table->increments('id');

    // Integer types
    $table->integer('votes');
    $table->bigInteger('big_number');

    // String types
    $table->string('name', 100);  // VARCHAR(100)
    $table->text('description');

    // Boolean
    $table->boolean('active');

    // Date/Time
    $table->timestamp('published_at');
    $table->timestamps();  // created_at, updated_at

    // UUID
    $table->uuid('uuid');

    // JSON
    $table->json('metadata');

    // Soft deletes
    $table->softDeletes();  // deleted_at column
});

Column Modifiers

php
$table->string('email')->nullable();
$table->string('name')->default('Guest');
$table->integer('position')->unsigned();
$table->increments('id');  // auto-increment + primary key

Indexes

php
$schema->create('posts', function ($table) {
    $table->increments('id');
    $table->string('slug');
    $table->integer('user_id');

    // Single column index
    $table->index('slug');

    // Unique index
    $table->unique('slug');

    // Composite index
    $table->index(['user_id', 'created_at']);
});

Foreign Keys

php
$schema->create('posts', function ($table) {
    $table->increments('id');
    $table->integer('user_id');

    $table->foreign('user_id', 'id', 'users')
        ->onDelete('cascade')
        ->onUpdate('cascade');
});

Modifying Tables

php
// Add column
$schema->table('users', function ($table) {
    $table->string('phone')->nullable();
});

// Rename column
$schema->table('users', function ($table) {
    $table->renameColumn('name', 'full_name');
});

// Drop column (MySQL/PostgreSQL)
$schema->table('users', function ($table) {
    $table->dropColumn('phone');
});

Dropping Tables

php
$schema->drop('users');
$schema->dropIfExists('users');

Migrations

Migrations track database schema changes over time.

Creating Migrations

bash
php lalaz craft:migration create_users_table

Creates migrations/YYYY_MM_DD_HHMMSS_create_users_table.php:

php
<?php declare(strict_types=1);

use Lalaz\Database\Migrations\Migration;
use Lalaz\Database\Schema\SchemaBuilder;

return new class extends Migration
{
    public function up(SchemaBuilder $schema): void
    {
        $schema->create('users', function ($table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->timestamps();
        });
    }

    public function down(SchemaBuilder $schema): void
    {
        $schema->dropIfExists('users');
    }
};

Running Migrations

bash
# Run all pending migrations
php lalaz migrate

# Rollback last batch
php lalaz migrate:rollback

# Reset all migrations
php lalaz migrate:reset

# Refresh (reset + migrate)
php lalaz migrate:refresh

# Check migration status
php lalaz migrate:status

ORM (Object-Relational Mapping)

Lalaz's ORM provides an ActiveRecord implementation for working with your database.

Defining Models

php
<?php declare(strict_types=1);

namespace App\Models;

use Lalaz\Orm\Model;

class User extends Model
{
    protected ?string $table = 'users';
    protected string $primaryKey = 'id';

    protected array $fillable = [
        'name',
        'email',
        'password',
    ];

    protected array $hidden = [
        'password',
    ];

    protected array $casts = [
        'email_verified_at' => 'datetime',
        'is_admin' => 'boolean',
    ];
}

Retrieving Models

php
// Get all users
$users = User::all($manager);

// Find by primary key
$user = User::find($manager, 1);

// Find or fail (throws exception)
$user = User::findOrFail($manager, 1);

// Query builder
$users = User::queryWith($manager)
    ->where('status', 'active')
    ->orderBy('name')
    ->get();

// First matching record
$user = User::queryWith($manager)
    ->where('email', '[email protected]')
    ->first();

Creating Models

php
// Using create (mass assignment)
$user = User::create($manager, [
    'name' => 'John Doe',
    'email' => '[email protected]',
    'password' => password_hash('secret', PASSWORD_DEFAULT),
]);

// Using build + save
$user = User::build($manager, [
    'name' => 'John Doe',
    'email' => '[email protected]',
]);
$user->password = password_hash('secret', PASSWORD_DEFAULT);
$user->save();

Updating Models

php
$user = User::find($manager, 1);
$user->name = 'Jane Doe';
$user->save();

// Mass update
$user->fill([
    'name' => 'Jane Doe',
    'email' => '[email protected]',
]);
$user->save();

Deleting Models

php
$user = User::find($manager, 1);
$user->delete();

// Soft deletes (if enabled)
$user->delete();        // Sets deleted_at
$user->forceDelete();   // Permanently deletes
$user->restore();       // Restores soft-deleted model

Mass Assignment

Control which attributes can be mass-assigned:

php
class User extends Model
{
    // Only these fields can be mass-assigned
    protected array $fillable = [
        'name',
        'email',
        'password',
    ];

    // Or, guard specific fields
    protected array $guarded = [
        'id',
        'is_admin',
    ];
}

Attribute Casting

php
class User extends Model
{
    protected array $casts = [
        'is_admin' => 'boolean',
        'age' => 'integer',
        'balance' => 'float',
        'settings' => 'array',      // JSON to array
        'metadata' => 'object',     // JSON to object
        'birthday' => 'date',
        'created_at' => 'datetime',
    ];
}

Timestamps

Models automatically manage created_at and updated_at:

php
class User extends Model
{
    // Enable/disable timestamps
    protected bool $timestamps = true;

    // Customize column names
    protected string $createdAtColumn = 'created_at';
    protected string $updatedAtColumn = 'updated_at';
}

Soft Deletes

php
class User extends Model
{
    protected bool $softDeletes = true;
    protected string $deletedAtColumn = 'deleted_at';
}

// Query only non-deleted
$users = User::queryWith($manager)->get();

// Include soft-deleted
$users = (new User($manager))->withTrashed()->get();

// Only soft-deleted
$users = (new User($manager))->onlyTrashed()->get();

Relationships

One-to-One

php
class User extends Model
{
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
        // Or with custom keys:
        // return $this->hasOne(Profile::class, 'user_id', 'id');
    }
}

// Usage
$user = User::find($manager, 1);
$profile = $user->profile;  // Lazy loading

One-to-Many

php
class User extends Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

class Post extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

// Usage
$user = User::find($manager, 1);
$posts = $user->posts;  // Collection of posts

$post = Post::find($manager, 1);
$author = $post->user;  // User model

Many-to-Many

php
class User extends Model
{
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
        // With custom pivot table:
        // return $this->belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id');
    }
}

class Role extends Model
{
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}

// Usage
$user = User::find($manager, 1);
$roles = $user->roles;

Eager Loading

Prevent N+1 query problems:

php
// Eager load relationships
$users = User::queryWith($manager)
    ->with('posts')
    ->get();

// Multiple relationships
$users = User::queryWith($manager)
    ->with(['posts', 'profile'])
    ->get();

// Nested eager loading
$users = User::queryWith($manager)
    ->with('posts.comments')
    ->get();

Read Replicas

Configure read replicas for read/write splitting:

php
return [
    'database' => [
        'driver' => 'mysql',
        'connections' => [
            'mysql' => [
                'host' => env('DB_HOST', '127.0.0.1'),
                // ... write connection config
            ],
        ],
        'read' => [
            'enabled' => true,
            'sticky' => true,  // Use write connection after write operations
            'connections' => [
                ['host' => 'read-replica-1.example.com'],
                ['host' => 'read-replica-2.example.com'],
            ],
        ],
    ],
];

Connection Pooling

Lalaz manages database connections efficiently with connection pooling:

php
return [
    'database' => [
        'pool' => [
            'min' => 0,           // Minimum connections to keep open
            'max' => 10,          // Maximum connections allowed
            'timeout_ms' => 5000, // Timeout when pool is exhausted
        ],
    ],
];

Query Logging

Enable query logging for debugging:

php
// The ConnectionManager automatically logs queries when a logger is provided
$manager = new ConnectionManager($config, $logger);

// Log output:
// [db:mysql][write] 12.34ms select SELECT * FROM users WHERE id = ?

Best Practices

Use Query Builder for Complex Queries

php
// ✅ Good: Query builder with relationships
$users = User::queryWith($manager)
    ->where('status', 'active')
    ->with('posts')
    ->orderBy('name')
    ->get();

// ❌ Avoid: N+1 queries
$users = User::all($manager);
foreach ($users as $user) {
    echo $user->posts->count(); // Query per user!
}

Use Transactions for Data Integrity

php
$connection->transaction(function ($conn) use ($data) {
    $user = User::create($this->manager, $data['user']);
    Profile::create($this->manager, [
        'user_id' => $user->id,
        ...$data['profile'],
    ]);
});

Paginate Large Result Sets

php
$users = User::queryWith($manager)
    ->orderBy('id')
    ->limit(25)
    ->offset(($page - 1) * 25)
    ->get();

Use Model Events for Side Effects

php
class User extends Model
{
    protected function registerObservers(): void
    {
        $this->events?->listen('creating', function ($model) {
            $model->uuid = Str::uuid();
        });

        $this->events?->listen('deleted', function ($model) {
            // Clean up related data
        });
    }
}

Released under the MIT License.