Skip to content
TubePress — free, self-hosted & actively maintained
Developer reference

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.

MethodReturnsPurpose
Database::fetchOne($sql, $params)array\|nullFirst matching row.
Database::fetchAll($sql, $params)arrayAll matching rows.
Database::insert($table, $data)intInsert an associative array; returns the new id.
Database::update($table, $data, $where, $whereParams)intUpdate; returns affected rows.
Database::delete($table, $where, $params)intDelete; returns affected rows.
Database::count($table, $where, $params)intRow count.
Database::query($sql, $params)PDOStatementAny statement, for full control.
Database::transaction($callback)mixedRun 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',
]);
Resilient connections. The helper uses 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 TABLE statements and run them in the plugin's install() 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.

Try the live demo → Download TubePress →