Database
TinyMVC provides a fluent query builder and database abstraction layer supporting multiple database drivers.
Configuration
Configure your database connection in env.php
:
SQLite Configuration
'database' => [
'driver' => 'sqlite',
'file' => __DIR__ . '/database/sqlite.db'
]
MySQL Configuration
'database' => [
'driver' => 'mysql',
'host' => 'localhost',
'port' => 3306,
'name' => 'test_db',
'user' => 'root',
'password' => 'password'
]
Query Builder
The query builder provides a fluent interface for database operations.
Basic Usage
// Get all records
$users = query('users')->result();
// Get single record
$user = query('users')->where('id', 1)->first();
// Count records
$count = query('users')->where('active', 1)->count();
Inserting Data
// Insert single record
query('users')->insert([
'name' => 'John Doe',
'email' => 'john@example.com'
]);
// Insert multiple records
query('users')->insert([
['name' => 'John', 'email' => 'john@example.com'],
['name' => 'Jane', 'email' => 'jane@example.com']
]);
Updating Data
// Update single record
query('users')
->where('id', 1)
->update(['name' => 'Updated Name']);
// Update multiple records
query('users')
->where('status', 'pending')
->update(['status' => 'active']);
Deleting Data
// Delete single record
query('users')->where('id', 1)->delete();
// Delete multiple records
query('users')->where('last_login', '<', '2023-01-01')->delete();
Complex Queries
// Join tables
query('posts')
->as('p')
->select(['p.*', 'u.name as author'])
->join('users as u', 'u.id = p.user_id')
->where('p.published', 1)
->result();
// Conditional where clauses
query('products')
->where('price', '>', 100)
->orWhere('featured', 1)
->result();
Database Helpers
// Get database instance
$db = database();
// Alternative helper
$db = db();
// Create query builder instance
$query = query('table_name');
Transactions
try {
database()->beginTransaction();
query('accounts')
->where('id', 1)
->update(['balance' => 1000]);
query('transactions')
->insert(['account_id' => 1, 'amount' => 1000]);
database()->commit();
} catch (Exception $e) {
database()->rollback();
throw $e;
}
select()
to specify only the columns you need
rather
than selecting all columns with *
.
Full Example
// Blog post query example
$posts = query('posts')
->as('p')
->select([
'p.id',
'p.title',
'p.content',
'u.name as author',
'c.name as category'
])
->join('users as u', 'u.id = p.user_id')
->join('categories as c', 'c.id = p.category_id')
->where('p.published', 1)
->where('p.created_at', '>', '2023-01-01')
->result();
// User registration example
try {
database()->beginTransaction();
$userId = query('users')->insert([
'name' => request('name'),
'email' => request('email'),
'password' => password_hash(request('password'), PASSWORD_DEFAULT)
]);
query('profiles')->insert([
'user_id' => $userId,
'bio' => request('bio')
]);
database()->commit();
} catch (Exception $e) {
database()->rollback();
return response()->with('error', 'Registration failed')->back();
}
Database Models
TinyMVC provides an active record implementation through the Model class, allowing you to interact with your database tables using object-oriented syntax.
Creating Models
Models are stored in the app/Models
folder and extend the base
Spark\Database\Model
class:
<?php
namespace App\Models;
use Spark\Database\Model;
class User extends Model
{
/**
* The database table name
*/
public static string $table = 'users';
/**
* Fields that are not mass assignable
*/
protected array $guarded = ['id', 'created_at'];
}
Basic CRUD Operations
Creating Records
// Create and save in one step
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com'
]);
// Or create then save
$user = new User();
$user->name = 'John Doe';
$user->email = 'john@example.com';
$user->save();
Reading Records
// Find by ID
$user = User::find(1);
// Get all records
$users = User::result();
// Get first record
$user = User::first();
// Get last record
$user = User::last();
Updating Records
// Find then update
$user = User::find(1);
$user->name = 'Updated Name';
$user->save();
// Mass update
User::where('status', 'pending')
->update(['status' => 'active']);
Deleting Records
// Find then delete
$user = User::find(1);
$user->remove();
// Direct delete
User::where('last_login', '<', '2020-01-01')->delete();
Query Builder Integration
Models inherit all QueryBuilder methods:
// Complex queries
$users = User::where('status', 'active')
->where('created_at', '>', '2023-01-01')
->orderAsc('name')
->result();
// Joins
$posts = Post::select(['p.*', 'u.name as author'])
->as('p')
->join('users as u', 'u.id = p.user_id')
->where('p.published', 1)
->result();
Pagination
The paginate method makes it easy to paginate query results:
// Basic pagination (10 items per page)
$users = User::paginate();
// Custom pagination (25 items per page)
$users = User::paginate(25);
// Displaying paginated results
foreach ($users->getData() as $user) {
echo $user->name;
}
// Rendering pagination links
echo $users->getLinks();
Pagination Methods
// Check if data exists
if ($users->hasData()) {
// Process data
}
// Get paginated data
$data = $users->getData();
// Custom pagination links
if ($users->hasLinks()) {
echo $users->getLinks(
links: 3, // Number of links to show
classes: [
'container' => 'pagination',
'item' => 'page-item',
'link' => 'page-link'
],
entity: [
'previous' => '«',
'next' => '»'
]
);
}
Mass Assignment
Control which fields can be mass assigned:
class User extends Model
{
// Only these fields can be mass assigned
protected array $fillable = ['name', 'email'];
// Or specify guarded fields (opposite of fillable)
protected array $guarded = ['id', 'is_admin'];
}
Model Methods
// Convert model to array
$array = $user->toArray();
// Create model from array
$user = User::load(['name' => 'John', 'email' => 'john@example.com']);
// Get query builder instance
$query = User::query();
Full Example
// Blog controller example
public function index()
{
$posts = Post::with('author')
->where('published', 1)
->orderDesc('created_at')
->paginate(15);
return view('posts.index', ['posts' => $posts]);
}
// User registration example
public function store(Hash $hash)
{
$inserted = User::insert([
'name' => request('name'),
'email' => request('email'),
'password' => $hash->hashPassword(request('password'))
]);
return response()
->with('message', 'Registration successful!')
->redirect('/dashboard');
}
ORM Relationships
TinyMVC provides powerful ORM capabilities through the HasOrm
trait, allowing you to
define and work with model relationships.
Defining Relationships
Relationships are defined in the orm()
method of your model:
<?php
namespace App\Models;
use Spark\Database\Model;
use Spark\Database\Traits\HasOrm;
class User extends Model
{
use HasOrm;
protected function orm(): array
{
return [
// One-to-many relationship
'posts' => [
'has' => 'many',
'model' => Post::class,
'foreignKey' => 'user_id'
],
// One-to-one relationship
'profile' => [
'has' => 'one',
'model' => Profile::class,
'foreignKey' => 'user_id',
'localKey' => 'id'
],
// Many-to-many relationship
'roles' => [
'has' => 'many-x',
'model' => Role::class,
'table' => 'user_roles', // Pivot table
'foreignKey' => 'user_id',
'localKey' => 'role_id',
'callback' => function($query) {
// Additional query constraints
return $query->where('active', 1);
}
]
];
}
}
Relationship Types
One-to-One
'profile' => [
'has' => 'one',
'model' => Profile::class,
'foreignKey' => 'user_id'
]
One-to-Many
'posts' => [
'has' => 'many',
'model' => Post::class,
'foreignKey' => 'author_id'
]
Many-to-Many
'roles' => [
'has' => 'many-x',
'model' => Role::class,
'table' => 'user_roles', // Pivot table
'foreignKey' => 'user_id',
'localKey' => 'role_id'
]
Loading Relationships
Eager Loading (with)
// Load single relationship
$users = User::with('posts')->result();
// Load multiple relationships
$users = User::with(['posts', 'profile'])->result();
// Load all relationships
$users = User::with('*')->result();
Lazy Loading
$user = User::find(1);
// Access relationship (loaded when first accessed)
$posts = $user->posts;
$profile = $user->profile;
Advanced Usage
Query Callbacks
'recentPosts' => [
'has' => 'many',
'model' => Post::class,
'foreignKey' => 'user_id',
'callback' => function($query) {
return $query->where('created_at', '>', '2023-01-01')
->orderDesc('created_at');
}
]
Disabling Lazy Loading
'profile' => [
'has' => 'one',
'model' => Profile::class,
'foreignKey' => 'user_id',
'lazy' => false // Must be loaded explicitly with with()
]
Manual Relationship Loading
$users = User::all();
User::runOrm('posts', $users); // Load posts for all users
Full Example
// Blog controller with eager loading
public function show($userId)
{
$user = User::with(['posts', 'profile'])
->where('id', $userId)
->first();
return view('users/show', compact('user'));
}
// Accessing relationships in views
<h1><?= $user->name ?>></h1>
<h2>Profile</h2>
<p><?= $user->profile->bio ?>></p>
<h2>Posts</h2>
<?php foreach ($user->posts as $post): ?>
<article>
<h3><?= $post->title ?>></h3>
<p><?= $post->excerpt ?>></p>
</article>
<?php endforeach ?>
with()
) when you know
you'll need related data to avoid the N+1 query problem.
Error Handling
try {
$users = User::with('undefined_relation')->result();
} catch (UndefinedOrmException $e) {
// Handle undefined relationship
}
try {
$user->posts; // Attempt lazy loading when disabled
} catch (OrmDisabledLazyLoadingException $e) {
// Handle disabled lazy loading
}
Database Migrations & Schema Builder
TinyMVC provides a powerful database migration system and fluent schema builder for managing your database structure.
Creating Migrations
Generate a new migration file using the Spark CLI:
php spark make:migration users
This creates a new file in database/migrations/
with timestamp prefix:
<?php
use Spark\Database\Schema\Blueprint;
use Spark\Database\Schema\Schema;
return new class {
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique()->required();
$table->string('password')->required();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('users');
}
};
Running Migrations
# Run all pending migrations
php spark migrate:run
# Rollback the last migration
php spark migrate:rollback
# Rollback multiple migrations
php spark migrate:rollback 2
# Reset and re-run all migrations
php spark migrate:fresh
Schema Builder
Column Types
$table->id(); // Auto-incrementing ID
$table->string('name', 100); // String with optional length
$table->text('description'); // Long text
$table->integer('votes'); // Integer
$table->boolean('active'); // Boolean
$table->decimal('amount', 8, 2); // Decimal with precision
$table->date('created_at'); // Date only
$table->dateTime('updated_at'); // Date and time
$table->timestamp('published_at'); // Timestamp
$table->enum('status', ['active', 'pending']); // Enumeration
Column Modifiers
$table->string('email')->unique(); // Unique constraint
$table->string('name')->nullable(); // Allows NULL values
$table->integer('count')->default(0); // Default value
$table->string('ip')->required(); // NOT NULL constraint
$table->integer('views')->unsigned(); // Unsigned integer
Indexes
// Single column index
$table->string('email')->unique();
// Composite index
$table->index(['account_id', 'created_at']);
// Primary key
$table->primary(['id', 'parent_id']);
Foreign Keys
// Simple foreign key
$table->foreignId('user_id')->constrained();
// Custom foreign key
$table->foreign('author_id')->references('id')->on('authors');
// With actions
$table->foreign('user_id')
->references('id')
->on('users')
->onDelete('cascade');
Modifying Tables
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('old_column');
$table->dropIndex(['email', 'status']);
$table->dropForeign('user_id', 'users');
});
Migration Structure
Each migration class contains two methods:
up()
- Runs when migrating forwarddown()
- Runs when rolling back
Best Practices
- Keep migrations focused on one schema change
- Always implement the
down()
method - Use descriptive migration names
- Test migrations in development before production
- Consider data migrations for large changes
Full Example
<?php
use Spark\Database\Schema\Blueprint;
use Spark\Database\Schema\Schema;
return new class {
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->boolean('published')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->index(['title', 'published_at']);
});
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained('posts');
$table->foreignId('user_id')->constrained('users');
$table->text('body');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('comments');
Schema::dropIfExists('posts');
}
};