Database & Entities
Database & Entity API
Minnow provides a database layer with raw queries, a query builder, a schema builder, and an Entity ORM — all built on PDO with prepared statements.
Connection
Get the database connection:
use Minnow\Core\Database\Connection;
$db = Connection::getInstance();
Or use the helper function:
use function Minnow\Core\Database\db;
$db = db();
Raw Queries
// SELECT — returns array of rows (each row is an associative array)
$rows = $db->fetchAll("SELECT * FROM {$db->table('posts')} WHERE post_status = ?", ['publish']);
// Single row — returns ?array
$row = $db->fetchOne("SELECT * FROM {$db->table('posts')} WHERE ID = ?", [42]);
// INSERT/UPDATE/DELETE — returns PDOStatement
$db->raw("UPDATE {$db->table('posts')} SET post_title = ? WHERE ID = ?", ['New Title', 42]);
// Last insert ID
$db->raw("INSERT INTO {$db->table('posts')} (post_title) VALUES (?)", ['Hello']);
$id = $db->lastInsertId();
Results are arrays, not objects. Access columns with $row['column'], not $row->column.
Table Names
Use $db->table('name') to get the prefixed table name:
$db->table('posts') // → "minnow_posts"
$db->table('users') // → "minnow_users"
$db->table('postmeta') // → "minnow_postmeta"
Transactions
use function Minnow\Core\Database\transaction;
$result = transaction(function ($db) {
$db->raw("INSERT INTO ...", [...]);
$db->raw("UPDATE ...", [...]);
return $db->lastInsertId();
});
Automatically rolls back on exception.
Connection Methods
| Method | Returns | Description |
|---|---|---|
getInstance() |
Connection |
Get singleton instance |
fetchAll($sql, $params) |
array |
Execute query, return all rows |
fetchOne($sql, $params) |
?array |
Execute query, return first row |
raw($sql, $params) |
PDOStatement |
Execute raw SQL |
table($name) |
string |
Get prefixed table name |
lastInsertId() |
string |
Last auto-increment ID |
beginTransaction() |
bool |
Start transaction |
commit() |
bool |
Commit transaction |
rollBack() |
bool |
Roll back transaction |
pdo() |
PDO |
Get underlying PDO instance |
getPrefix() |
string |
Get table prefix |
Query Builder
A fluent interface for building SQL queries:
use function Minnow\Core\Database\query;
// Basic select
$posts = query('posts')
->where('post_status', 'publish')
->orderBy('post_date', 'DESC')
->limit(10)
->get();
// With multiple conditions
$pages = query('posts')
->where('post_type', 'page')
->where('post_status', 'publish')
->whereNotNull('post_parent')
->orderBy('menu_order')
->get();
// Single record
$post = query('posts')->where('ID', 42)->first();
// Single value
$count = query('posts')->where('post_status', 'publish')->count();
$title = query('posts')->where('ID', 42)->value('post_title');
// Column values
$titles = query('posts')->where('post_status', 'publish')->pluck('post_title');
WHERE Clauses
->where('column', 'value') // column = value
->where('column', '>', 10) // column > 10
->where('column', 'LIKE', '%search%') // LIKE
->where('column', '!=', 'draft') // Not equal
->whereNull('column') // IS NULL
->whereNotNull('column') // IS NOT NULL
->whereIn('column', [1, 2, 3]) // IN (1, 2, 3)
->whereNotIn('column', [4, 5]) // NOT IN
->whereBetween('column', [1, 100]) // BETWEEN 1 AND 100
->whereRaw('YEAR(post_date) = ?', [2025]) // Raw expression
Joins
query('posts')
->join('postmeta', 'posts.ID', '=', 'postmeta.post_id')
->where('postmeta.meta_key', '_thumbnail_id')
->select(['posts.*', 'postmeta.meta_value as thumbnail_id'])
->get();
// Left join
query('posts')
->leftJoin('postmeta', 'posts.ID', '=', 'postmeta.post_id')
->get();
Aggregates & Grouping
->count() // COUNT(*)
->groupBy('post_type') // GROUP BY
->having('count', '>', 5) // HAVING
->select(['post_type', 'COUNT(*) as count'])
Insert, Update, Delete
// Insert — returns last insert ID
$id = query('posts')->insert([
'post_title' => 'Hello World',
'post_status' => 'publish',
'post_type' => 'post',
]);
// Update — returns affected rows
$affected = query('posts')
->where('ID', 42)
->update(['post_title' => 'Updated Title']);
// Delete — returns affected rows
$deleted = query('posts')
->where('post_status', 'trash')
->delete();
Debugging
$builder = query('posts')->where('post_status', 'publish');
echo $builder->toSql(); // "SELECT * FROM minnow_posts WHERE post_status = ?"
print_r($builder->getBindings()); // ['publish']
Full Method Reference
| Method | Description |
|---|---|
select($columns) |
Set columns to select |
distinct() |
SELECT DISTINCT |
where($col, $op, $val) |
WHERE condition (2 or 3 args) |
whereNull($col) |
WHERE col IS NULL |
whereNotNull($col) |
WHERE col IS NOT NULL |
whereIn($col, $values) |
WHERE col IN (...) |
whereNotIn($col, $values) |
WHERE col NOT IN (...) |
whereBetween($col, [$min, $max]) |
WHERE col BETWEEN |
whereRaw($sql, $bindings) |
Raw WHERE expression |
orWhere($col, $op, $val) |
OR WHERE condition |
join($table, $col1, $op, $col2) |
INNER JOIN |
leftJoin($table, $col1, $op, $col2) |
LEFT JOIN |
orderBy($col, $dir) |
ORDER BY |
groupBy($col) |
GROUP BY |
having($col, $op, $val) |
HAVING |
limit($n) |
LIMIT |
offset($n) |
OFFSET |
get() |
Execute and return all rows |
first() |
Execute and return first row |
value($col) |
Get single column value |
pluck($col) |
Get array of column values |
count() |
Get row count |
exists() |
Check if rows exist |
insert($data) |
Insert row, return ID |
update($data) |
Update rows, return count |
delete() |
Delete rows, return count |
toSql() |
Get SQL string |
getBindings() |
Get parameter bindings |
Schema Builder
Create and modify database tables programmatically:
use function Minnow\Core\Database\schema;
$schema = schema();
Creating Tables
$schema->create('my_table', function ($table) {
$table->id();
$table->string('name', 255);
$table->text('description')->nullable();
$table->integer('count')->default(0);
$table->boolean('active')->default(true);
$table->timestamps();
$table->index('name');
$table->unique('name', 'idx_unique_name');
});
Column Types
| Method | MySQL Type |
|---|---|
id($name) |
BIGINT AUTO_INCREMENT PRIMARY KEY |
bigInteger($name) |
BIGINT |
unsignedBigInteger($name) |
BIGINT UNSIGNED |
integer($name) |
INT |
unsignedInteger($name) |
INT UNSIGNED |
smallInteger($name) |
SMALLINT |
tinyInteger($name) |
TINYINT |
boolean($name) |
TINYINT(1) DEFAULT 0 |
string($name, $len) |
VARCHAR(255) |
char($name, $len) |
CHAR(255) |
text($name) |
TEXT |
mediumText($name) |
MEDIUMTEXT |
longText($name) |
LONGTEXT |
decimal($name, $p, $s) |
DECIMAL(8,2) |
float($name) |
FLOAT |
double($name) |
DOUBLE |
datetime($name) |
DATETIME |
date($name) |
DATE |
time($name) |
TIME |
timestamp($name) |
TIMESTAMP |
timestamps() |
created_at + updated_at DATETIME |
json($name) |
JSON |
blob($name) |
BLOB |
longBlob($name) |
LONGBLOB |
Column Modifiers
$table->string('email')->nullable();
$table->integer('views')->default(0);
$table->bigInteger('amount')->unsigned();
Indexes
$table->primary('id'); // Primary key
$table->index('user_id'); // Regular index
$table->unique('email'); // Unique index
$table->unique(['user_id', 'post_id']); // Composite unique
$table->fulltext('content'); // Fulltext index
Altering Tables
$schema->alter('my_table', function ($table) {
$table->string('new_column')->nullable();
$table->dropColumn('old_column');
$table->dropIndex('idx_name');
});
Other Schema Methods
$schema->hasTable('my_table'); // Check if table exists
$schema->hasColumn('my_table', 'name'); // Check if column exists
$schema->getColumns('my_table'); // Get column info
$schema->dropIfExists('my_table'); // Drop table
$schema->rename('old_name', 'new_name'); // Rename table
Entity Base Class
Entities are PHP classes that map to database tables. They provide an ActiveRecord-style ORM.
Defining an Entity
use Minnow\Core\Entity\Entity;
class Bookmark extends Entity
{
protected static string $table = 'minnow_bookmarks';
protected static string $primaryKey = 'id';
protected static bool $usePrefix = true;
protected static array $fillable = ['user_id', 'url', 'title', 'is_public'];
protected static array $hidden = ['user_id'];
protected static array $casts = [
'is_public' => 'bool',
'created_at' => 'datetime',
];
protected static bool $timestamps = true;
}
Finding Records
$bookmark = Bookmark::find(42); // By primary key (or null)
$bookmark = Bookmark::findOrFail(42); // Throws EntityNotFoundException
$all = Bookmark::all(); // Collection of all records
$public = Bookmark::where('is_public', true); // Collection matching condition
$first = Bookmark::firstWhere('url', 'https://example.com'); // Single match
Creating & Saving
// Create and save in one step
$bookmark = Bookmark::create([
'user_id' => 1,
'url' => 'https://example.com',
'title' => 'Example',
]);
// Or build and save manually
$bookmark = new Bookmark(['title' => 'Draft']);
$bookmark->url = 'https://example.com';
$bookmark->save();
Updating
$bookmark = Bookmark::find(42);
$bookmark->title = 'New Title';
$bookmark->save();
Deleting
$bookmark = Bookmark::find(42);
$bookmark->delete();
Querying with the Query Builder
$results = Bookmark::query()
->where('is_public', true)
->orderBy('created_at', 'DESC')
->limit(10)
->get();
// Hydrate raw results into entity objects
$bookmarks = Bookmark::hydrate($results);
Dirty Checking
$bookmark = Bookmark::find(42);
$bookmark->isDirty(); // false
$bookmark->title = 'Changed';
$bookmark->isDirty(); // true
$bookmark->getDirty(); // ['title' => 'Changed']
Conversion
$bookmark->toArray(); // Excludes $hidden fields
$bookmark->toJson(); // JSON string
Meta Data
Entities with a $metaTable and $metaForeignKey support WordPress-style metadata:
$post->getMeta('custom_field'); // Get meta value
$post->setMeta('custom_field', 'value'); // Set meta value
$post->deleteMeta('custom_field'); // Delete meta
$post->getAllMeta(); // All meta as key => value
$post->hasMeta('custom_field'); // Check existence
Built-in Entities
Post
use Minnow\Core\Entity\Post;
| Method | Returns | Description |
|---|---|---|
Post::find($id) |
?Post |
Find by ID |
Post::ofType('page') |
Collection |
Posts by type |
Post::published() |
Collection |
Published posts (newest first) |
Post::withStatus('draft') |
Collection |
Posts by status |
Post::bySlug('hello', 'post') |
?Post |
Find by slug |
Post::byPath('parent/child') |
?Post |
Find page by path |
Post::paginate($options) |
array |
Paginated results with posts, total, per_page, current_page, total_pages |
$post->author() |
?User |
Post author |
$post->parent() |
?Post |
Parent post |
$post->children() |
Collection |
Child posts |
$post->categories() |
Collection |
Post categories |
$post->tags() |
Collection |
Post tags |
$post->terms('taxonomy') |
Collection |
Terms by taxonomy |
$post->isPublished() |
bool |
Status check |
$post->publish() |
bool |
Publish the post |
$post->schedule($date) |
bool |
Schedule for future |
$post->permalink() |
string |
Post URL |
$post->date('F j, Y') |
string |
Formatted date |
$post->previous() |
?Post |
Previous post |
$post->next() |
?Post |
Next post |
User
use Minnow\Core\Entity\User;
| Method | Returns | Description |
|---|---|---|
User::find($id) |
?User |
Find by ID |
User::findByEmail($email) |
?User |
Find by email |
User::findByLogin($login) |
?User |
Find by username |
User::findByCredential($str) |
?User |
Find by email or username |
$user->posts() |
Collection |
User's posts |
$user->getDisplayName() |
string |
Display name |
$user->getEmail() |
string |
Email address |
$user->getRoles() |
string[] |
Role slugs |
$user->hasRole('editor') |
bool |
Check role |
$user->assignRole('editor') |
void |
Add role |
$user->removeRole('editor') |
void |
Remove role |
$user->can('edit_posts') |
bool |
Check capability |
$user->getAllCapabilities() |
array |
All capabilities |
Term
use Minnow\Core\Entity\Term;
| Method | Returns | Description |
|---|---|---|
Term::forTaxonomy('category') |
Collection |
Terms by taxonomy |
Term::categories() |
Collection |
All categories |
Term::tags() |
Collection |
All tags |
Term::findBySlug('news', 'category') |
?Term |
Find by slug |
Term::forPost($postId, 'category') |
Collection |
Terms for a post |
Term::setForPost($postId, $ids, 'tag') |
void |
Set post terms |
Term::createWithTaxonomy($attrs, 'cat') |
Term |
Create with taxonomy |
$term->getTaxonomy() |
?string |
Taxonomy name |
$term->getDescription() |
string |
Description |
$term->parent() |
?Term |
Parent term |
$term->children() |
Collection |
Child terms |
$term->getLink() |
string |
Term URL |
Comment
use Minnow\Core\Entity\Comment;
| Method | Returns | Description |
|---|---|---|
Comment::forPost($postId) |
Collection |
Approved comments for post |
Comment::approved() |
Collection |
All approved comments |
Comment::pending() |
Collection |
Pending comments |
$comment->post() |
?Post |
Parent post |
$comment->parent() |
?Comment |
Parent comment |
$comment->replies() |
Collection |
Reply comments |
$comment->approve() |
bool |
Approve comment |
$comment->isApproved() |
bool |
Status check |
Attachment
use Minnow\Core\Entity\Attachment;
Extends Post with media-specific functionality.
| Method | Returns | Description |
|---|---|---|
Attachment::find($id) |
?Attachment |
Find by ID |
Attachment::all() |
Collection |
All attachments |
Attachment::images() |
Collection |
Image attachments |
Attachment::ofMimeType('image/png') |
Collection |
By MIME type |
Attachment::paginate($options) |
array |
Paginated media |
Attachment::createFromUpload($file, $dir, $url) |
?Attachment |
Upload file |
$att->getUrl() |
string |
File URL |
$att->getAltText() |
string |
Alt text |
$att->setAltText($text) |
bool |
Set alt text |
$att->getDimensions() |
array |
[width, height] |
$att->getSizes() |
array |
Available sizes |
$att->getSizeUrl('thumbnail') |
string |
Size-specific URL |
$att->isImage() |
bool |
Type check |
$att->getMediaType() |
string |
image, video, audio, file |
$att->getMetadata() |
array |
File metadata |
Helper Functions
use function Minnow\Core\Database\db;
use function Minnow\Core\Database\query;
use function Minnow\Core\Database\raw;
use function Minnow\Core\Database\schema;
use function Minnow\Core\Database\transaction;
| Function | Returns | Description |
|---|---|---|
db() |
Connection |
Get database connection |
query('table') |
QueryBuilder |
Start a query builder |
raw($sql, $params) |
PDOStatement |
Execute raw SQL |
schema() |
Schema |
Get schema builder |
transaction($callback) |
mixed |
Run in transaction |