Minnow

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