Models & database
The TubePress data layer: model classes, the PDO Database helper, prepared statements, the database schema and versioned SQL migrations explained.
TubePress talks to MySQL/MariaDB through two thin layers: models (one static class per entity) and the Database helper (a small wrapper over PDO). There is no ORM and no query builder to learn — just prepared statements and predictable method names.
The model layer
Each core entity has a model in src/Models/ exposing static methods. The available models are Video, Category, Tag, Performer, Channel, Comment, Page, User, Setting, Favorite, History, Report, ContactMessage, MenuConfig and FooterConfig.
$video = Video::find($id); // one row (or null)
$latest = Video::all(['status' => 'published', 'limit' => 20]);
$id = Video::create($data); // returns new id
Video::update($id, ['title' => 'New title']);
Video::delete($id);Models encapsulate the joins and filters specific to their entity — for example Video::find() returns the video with its categories, tags, performers and channels attached, so controllers and templates stay simple.
The Database helper
For anything a model does not already cover, use the Database helper directly. Every method uses prepared statements — never concatenate user input into SQL.
| Method | Returns | Purpose |
|---|---|---|
Database::fetchOne($sql, $params) | array\|null | First matching row. |
Database::fetchAll($sql, $params) | array | All matching rows. |
Database::insert($table, $data) | int | Insert an associative array; returns the new id. |
Database::update($table, $data, $where, $whereParams) | int | Update; returns affected rows. |
Database::delete($table, $where, $params) | int | Delete; returns affected rows. |
Database::count($table, $where, $params) | int | Row count. |
Database::query($sql, $params) | PDOStatement | Any statement, for full control. |
Database::transaction($callback) | mixed | Run a closure in a transaction (auto commit/rollback). |
$rows = Database::fetchAll(
"SELECT * FROM videos WHERE status = ? ORDER BY id DESC LIMIT ?",
['published', 20]
);
$newId = Database::insert('categories', [
'name' => 'Amateur',
'slug' => 'amateur',
]);utf8mb4, real prepared statements (no emulation) and exception error mode, and it automatically reconnects once on a genuine "server has gone away" error — so long-running workers survive an idle MySQL timeout.Transactions
Wrap multi-step writes so they all succeed or all roll back:
Database::transaction(function () use ($videoId, $catIds) {
Database::delete('video_categories', 'video_id = ?', [$videoId]);
foreach ($catIds as $cid) {
Database::insert('video_categories', [
'video_id' => $videoId, 'category_id' => $cid,
]);
}
});The schema
The database uses a clean, normalised MySQL schema. The core tables include users, settings, categories, tags, performers, channels, videos and the join tables video_categories, video_tags, video_performers — plus comments, user_favorites, user_history, video_likes, pages, plugins, themes and migrations. Optional subsystems add their own tables (import jobs, media storages, transcode jobs/servers, ad zones/spots, reports, feed sources, translation tables and more) as you enable them.
Migrations
Schema changes live in sql/ as numbered files — 001_users.sql, 002_settings.sql, … — applied in order by Migrator::run() during installation and after every update. Each migration is additive and recorded in the migrations table so it never runs twice.
- To extend the schema from a plugin, ship your
CREATE TABLEstatements and run them in the plugin'sinstall()method (see Hooks & plugins). - Never renumber or edit an applied migration — add a new, higher-numbered one.
Writing a model
A model is just a static class that wraps the Database helper for one table:
class Promo
{
public static function current(): ?array
{
return Database::fetchOne(
"SELECT * FROM promos WHERE active = 1
ORDER BY id DESC LIMIT 1"
);
}
}Next steps
Still stuck?
Open a ticket from your dashboard and our team will help you out.