MinistryHub uses MySQL as its relational database with a multi-database architecture to separate concerns and optimize for different data access patterns.
# Main DatabaseDB_HOST=localhostDB_USER=ministryhub_userDB_PASS=secure_passwordDB_NAME=ministryhub_mainDB_PORT=3306# Music DatabaseMUSIC_DB_HOST=localhostMUSIC_DB_USER=ministryhub_userMUSIC_DB_PASS=secure_passwordMUSIC_DB_NAME=ministryhub_music# SecurityJWT_SECRET=your-256-bit-secretRECAPTCHA_SECRET_KEY=your-recaptcha-key
<?phpnamespace App\Repositories;use App\Database;use PDO;class SongRepo{ /** * Get all songs for a church (includes global songs with church_id=0) */ public static function getAll($churchId = null) { $db = Database::getInstance('music'); if ($churchId === null) { // Superadmin: get ALL songs $sql = "SELECT * FROM songs ORDER BY title ASC"; $stmt = $db->prepare($sql); } else { // Regular user: get church-specific + global songs $sql = "SELECT * FROM songs WHERE church_id = :church_id OR church_id = 0 ORDER BY title ASC"; $stmt = $db->prepare($sql); $stmt->bindValue(':church_id', $churchId, PDO::PARAM_INT); } $stmt->execute(); return $stmt->fetchAll(); } /** * Get a single song by ID */ public static function findById($id) { $db = Database::getInstance('music'); $sql = "SELECT * FROM songs WHERE id = :id LIMIT 1"; $stmt = $db->prepare($sql); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch(); } /** * Create a new song */ public static function create($data) { $db = Database::getInstance('music'); $sql = "INSERT INTO songs (title, artist, lyrics, original_key, church_id, created_by) VALUES (:title, :artist, :lyrics, :key, :church_id, :created_by)"; $stmt = $db->prepare($sql); $stmt->bindValue(':title', $data['title']); $stmt->bindValue(':artist', $data['artist']); $stmt->bindValue(':lyrics', $data['lyrics']); $stmt->bindValue(':key', $data['key']); $stmt->bindValue(':church_id', $data['church_id'], PDO::PARAM_INT); $stmt->bindValue(':created_by', $data['created_by'], PDO::PARAM_INT); $stmt->execute(); return $db->lastInsertId(); } /** * Update an existing song */ public static function update($id, $data) { $db = Database::getInstance('music'); $sql = "UPDATE songs SET title = :title, artist = :artist, lyrics = :lyrics, original_key = :key WHERE id = :id"; $stmt = $db->prepare($sql); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->bindValue(':title', $data['title']); $stmt->bindValue(':artist', $data['artist']); $stmt->bindValue(':lyrics', $data['lyrics']); $stmt->bindValue(':key', $data['key']); return $stmt->execute(); } /** * Delete a song */ public static function delete($id) { $db = Database::getInstance('music'); $sql = "DELETE FROM songs WHERE id = :id"; $stmt = $db->prepare($sql); $stmt->bindValue(':id', $id, PDO::PARAM_INT); return $stmt->execute(); }}
MinistryHub uses named parameters (:name) instead of positional (?) for clarity:
$sql = "SELECT * FROM members WHERE email = :email AND church_id = :church_id";$stmt = $db->prepare($sql);$stmt->bindValue(':email', $email);$stmt->bindValue(':church_id', $churchId, PDO::PARAM_INT);$stmt->execute();
CREATE TABLE members ( id INT AUTO_INCREMENT PRIMARY KEY, church_id INT NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, phone VARCHAR(50), role_id INT, is_active TINYINT(1) DEFAULT 1, invite_token VARCHAR(255), invite_expires_at DATETIME, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_church (church_id), INDEX idx_email (email), INDEX idx_invite (invite_token), FOREIGN KEY (church_id) REFERENCES churches(id) ON DELETE CASCADE);
CREATE TABLE churches ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, city VARCHAR(100), country VARCHAR(100), timezone VARCHAR(50) DEFAULT 'UTC', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE playlists ( id INT AUTO_INCREMENT PRIMARY KEY, church_id INT NOT NULL, name VARCHAR(255) NOT NULL, date DATE, songs JSON, -- Array of song IDs with order created_by INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_church_date (church_id, date), FOREIGN KEY (church_id) REFERENCES ministryhub_main.churches(id));
public static function getAll($churchId = null){ if ($churchId === null) { // No filter: return ALL data $sql = "SELECT * FROM songs"; } else { // Filter by church $sql = "SELECT * FROM songs WHERE church_id = :church_id"; }}
// Fetch only needed columns$sql = "SELECT id, title, artist FROM songs"; // Not SELECT *// Use LIMIT for pagination$sql = "SELECT * FROM songs LIMIT :offset, :limit";// Count efficiently$sql = "SELECT COUNT(*) as total FROM members WHERE church_id = :church_id";