Příklady použití databázového rozhraní
Tato sekce ukazuje běžné scénáře použití třídy DB v rámci systému.
Inicializace databáze
Automatická inicializace (lazy loading)
Třída DB se inicializuje automaticky při prvním použití jakékoliv metody. Nic se nemusí dělat - DB se postará sám.
Není potřeba:
requireneboincludesouboruapp/database.php- Ruční volání
DB::init() - Žádná inicializace v kódu
Příklad:
<?php
// Všechno funguje bez jakéhokoli setup!
$users = DB::results("SELECT * FROM users WHERE active = ?", [1]);
echo "Načítám " . count($users) . " uživatelů...";
Připojení se vytvoří při prvním dotazu a pak se používá pořád.
Jak to funguje interně?
- Zavoláš
DB::results()nebo jinou metodu - DB kontroluje: "Je připojení inicializováno?" (
DB::$DB === null) - Pokud ne → automaticky zavolá
require_once app/database.php app/database.phpzavoláDB::init(...)s konfiguracií z.env- Teď je DB připravená a dotaz se provede
Interní inicializace v app/database.php
Inicializace a nastavení DB probíhá automaticky v rámci CMS v souboru app/database.php. Není třeba (ani vhodné) volat DB::init() ani DB::setting() ručně v běžných částech kódu.
Ukázka z app/database.php:
$dbc = parse_url(env('DATABASE_URL'));
DB::init(
$dbc['user'],
rawurldecode($dbc['pass']),
trim($dbc['path'], '/'),
$dbc['host'],
'utf8',
$dbc['port'] ?? 3306
);
$setting = [
'cacheUse' => CONFIG['dbUseCache'],
'cacheSeconds' => CONFIG['dbCacheSeconds'],
'errorLogUse' => true,
'errorLogDir' => DIR . '/var/log',
'debugAll' => false,
];
DB::setting($setting);
Dále se nastavuje SQL režim a kódování:
DB::query("SET sql_mode='STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'");
DB::query("SET NAMES utf8mb4");
Použití v aplikaci
Třídu DB lze používat kdekoli globálně bez manuální inicializace - připojení se vytvoří automaticky při prvním dotazu. To platí pro modely, kontrolery i CLI skripty.
Příklady běžných dotazů následují níže.
Druhá databáze (DB2)
Kdy se používá DB2?
DB2 se používá velmi zřídka – pouze pokud potřebuješ připojení k externí databázi:
- Integrace se starším systémem (legacy databáze)
- Datový sklad s jinými přihlašovacími údaji
- Třetí strana API s vlastní databází
- Speciální analytické dotazy na oddělenou DB
V 99% projektů na Petrovo CMS se DB2 vůbec nepoužívá.
Rozdíl od DB
| Vlastnost | DB | DB2 |
|---|---|---|
| Automatická inicializace | ✅ Ano | ❌ Ne |
Vyžaduje DB::init() |
❌ Ne | ✅ Ano |
Vyžaduje require |
❌ Ne | ❌ Ne |
| Lazy loading | ✅ Ano | ❌ Ne |
| API | Stejné | Stejné |
Inicializace DB2 (POVINNÉ!)
Na rozdíl od DB, kterou se inicializuje automaticky, musíš DB2 explicitně inicializovat předtím, než ji začneš používat:
use Petrovo\Database\DB2;
use function Petrovo\Dotenv\env;
// KROK 1: Inicializuj DB2 PŘED POUŽITÍM
$dbc2 = parse_url(env('DATABASE2_URL'));
$port2 = (int)!empty($dbc2['port']) ? $dbc2['port'] : 3306;
DB2::init(
$dbc2['user'],
$dbc2['pass'],
trim($dbc2['path'], '/'),
$dbc2['host'],
'utf8',
$port2
);
Použití DB2
Po inicializaci se DB2 chová identicky jako DB:
// SELECT dotazy
$product = DB2::row("SELECT * FROM products WHERE id = ?", [$id]);
$products = DB2::results("SELECT * FROM products WHERE price > ?", [1000]);
$count = DB2::var("SELECT COUNT(*) FROM orders");
// INSERT/UPDATE/DELETE
$sql = "INSERT INTO log (action, created) VALUES (?, NOW())";
DB2::query($sql, ['product_imported']);
// Transakce
DB2::beginTransaction();
DB2::query("UPDATE products SET imported = 1 WHERE id = ?", [$id]);
DB2::commit();
Komplexní příklad
use Petrovo\Database\DB2;
// Inicializace externí databáze (jednou na začátku, např. v bootstrap)
DB2::init('legacy_user', 'legacy_pass', 'legacy_db', 'old-server.internal');
// Teď můžeš používat kdekoliv
function importLegacyProducts(): void {
// Čti z legacy DB
$legacyProducts = DB2::results("SELECT id, name, price FROM products");
foreach ($legacyProducts as $legacy) {
// Zapisuj do primární DB
$sql = "INSERT INTO products (legacy_id, name, price) VALUES (?, ?, ?)";
DB::query($sql, [$legacy->id, $legacy->name, $legacy->price]);
}
}
Důležitě: DB2 se inicializuje jednou na začátku aplikace a pak se používá globálně. Nemusíš ji inicializovat v každém souboru.
Debug helper - DB::test()
Pro debugging a testování SQL dotazů bez jejich spuštění slouží metoda DB::test(). Vypíše finální SQL s dosazením všech parametrů a zastaví skript (die).
Podobně jako v Dibi se používá k přepisu chybného dotazu a ověření správného SQL výstupu.
Příklady
// Chybný dotaz - běžíš skript a vidíš chybu
$users = DB::results("SELECT * FROM users WHERE id = ?", [42]);
// SQLException...
// Zkusíš debug - zavoláš test()
DB::test("SELECT * FROM users WHERE id = ?", [42]);
// Výstup: SELECT * FROM users WHERE id = '42'
// Skript se zastaví (die)
Funguje s pozičními i pojmenovanými parametry:
// Pozicional parametry (?)
DB::test("SELECT * FROM slug WHERE web_id = ? AND lang = ? AND slug = ?", ["0", "cs", "gdpr"]);
// Výstup: SELECT * FROM slug WHERE web_id = '0' AND lang = 'cs' AND slug = 'gdpr'
// Pojmenované parametry (:name)
DB::test("SELECT * FROM users WHERE id = :id AND active = :active", ['id' => 42, 'active' => 1]);
// Výstup: SELECT * FROM users WHERE id = '42' AND active = '1'
Automaticky se escapují speciální znaky:
DB::test("SELECT * FROM users WHERE name = ?", ["O'Brien"]);
// Výstup: SELECT * FROM users WHERE name = 'O''Brien'
S DB2
DB2::test() funguje stejně jako DB::test(), ale pamatuj, že DB2 musí být nejdřív inicializován:
DB2::init('user', 'password', 'legacy_db', 'old-server.com');
DB2::test("SELECT * FROM products WHERE id = ?", [123]);
// Výstup: SELECT * FROM products WHERE id = '123'
Výběry dat SELECT
Práce s daty probíhá výhradně přes statickou třídu DB a využívá připravené SQL dotazy (prepare + execute), které se volají automaticky metodou DB::query().
Pravidla a upozornění
- Nemíchej náhrady
?a:namev jednom dotazu – způsobuje chyby v PDO. Používej buď jeden, nebo druhý způsob. - Parametry se neescapují ručně – escapování provádí MariaDB až na úrovni vykonání dotazu.
- Nepoužívej vlastní
addslashes()nebo ručníescape()– pouze výjimečně můžeš použítDB::escape()pro případy, kdy musíš dynamicky sestavit části dotazu, ale ne hodnoty. - Dotaz se připravuje a spouští automaticky pomocí
DB::query($sql, $params). - Pokud provádíš opakovaný dotaz ve smyčce, je výhodnější připravit si dotaz ručně pomocí
DB::prepare()a opakovaně volatDB::execute($stmp, $params).
Výběry dat SELECT
Pro práci s daty používej metody DB::results(), DB::row(), DB::col(), DB::var(). Všechny tyto metody volají interně prepare + execute a vracejí výsledek z databáze podle zvoleného typu výstupu.
Návratové hodnoty
Výstupem je vždy datová struktura podle typu požadovaného „row typu“:
'OBJECT'(výchozí) – každý řádek jako objekt'ARRAY_A'– asociativní pole (['column' => value])'ARRAY_N'– číselné pole ([0 => value])'JSON'– JSON string (méně často, např. do API)
Tyto typy platí pro results() a row().
Ukázky použití
// Více řádků – výchozí výstup (OBJECT), bez parametrů
$rows = DB::results("SELECT id, name FROM tags ORDER BY name");
foreach ($rows as $tag) {
echo $tag->name;
}
// Více řádků – výstup jako pole (ARRAY_A), bez parametrů
$rows = DB::results("SELECT id, name FROM tags ORDER BY name", 'ARRAY_A');
foreach ($rows as $tag) {
echo $tag['name'];
}
// Jeden řádek – pojmenované parametry
$user = DB::row("SELECT * FROM users WHERE id = :id", ['id' => 42], 'ARRAY_A');
echo $user['email'];
// Jeden sloupec – bez parametrů
$emails = DB::col("SELECT email FROM users ORDER BY id DESC");
// Jedna hodnota – s parametrem
$login = DB::var("SELECT last_login FROM users WHERE id = :id", ['id' => 42]);
Dotazy, které nevrací data
Pro dotazy typu INSERT, UPDATE, DELETE, SET nebo CALL se používá:
DB::query("UPDATE users SET last_login = NOW() WHERE active = 1");
Tyto dotazy nemají návratovou hodnotu, ale ovlivňují $affectedRows, $lastError, $lastQuery atd.
Sestavení SET části dotazu – DB::set()
Metoda DB::set() slouží k automatickému vytvoření části SQL dotazu typu SET column = :column. Výhodou je, že stačí předat asociativní pole, a metoda vygeneruje správný SQL fragment.
⚠️ Upozornění
- Nepoužívej
?jako placeholdery při použitíDB::set()– vygenerované výrazy používají pojmenované parametry (:column), a ty musí být použity i ve zbytku dotazu. - Nesmí se míchat
?a:name– jinak dojde k chybě při provádění dotazu. - Pokud používáš
DB::set(), musíš následně předat pole s klíči odpovídajícími názvům sloupců.
Základní INSERT s DB::set()
$set = [
'news_id' => $newsNewId,
'lang' => $r->lang,
'active' => $r->active,
'homepage' => $r->homepage,
'data' => $r->data,
];
$sql = "INSERT INTO news_lang SET " . DB::set($set);
DB::query($sql, $set);
UPDATE s podmínkou pomocí :id
$set = [];
$set['name'] = html_entity_decode($_post['name']);
$set['alias'] = $_post['alias'];
$set['group'] = array_get_int($_post, 'group', 0);
$set['active'] = array_get_int($_post, 'active', 0);
$set['perex'] = html_entity_decode($_post['perex']);
$set['description'] = $_post['description'];
$sql = "UPDATE cube_type SET " . DB::set($set) . " WHERE id = :id";
DB::query($sql, [...$set, 'id' => $_post['id']]);
Pokročilé použití – ON DUPLICATE KEY UPDATE
$set = [
'cube_id' => $_POST['cId'],
'lang' => $lang,
'data' => Json\encode($this->data($lang, $onlyFields)),
];
$sql = "INSERT INTO cube_lang SET " . DB::set($set) .
" ON DUPLICATE KEY UPDATE " . DB::set($set, ['cube_id', 'lang'], true);
DB::query($sql, $set);
Třetí parametr metody
DB::set($fields, $exclude, $useValues)umožňuje použítfield = VALUES(field)proON DUPLICATE KEY UPDATE, bez nutnosti přepisovat klíče.
Generování SET výrazů – DB::set()
Metoda DB::set() vygeneruje část SQL dotazu typu SET column = :column, nebo alternativní formáty podle hodnot a nastavení.
Signatura
DB::set(array $fields, array $exclude = [], bool $useValues = false): string
Parametry
$fields– pole['column' => value], ze kterého se generují výrazycolumn = :column$exclude– seznam klíčů, které se mají z$fieldsvyloučit (např. primární klíče)$useValues– pokud jetrue, místo:columnse použijeVALUES(column), např. proON DUPLICATE KEY UPDATE
Automatické úpravy hodnot
Metoda se snaží automaticky přizpůsobit výraz podle typu hodnoty:
| Hodnota ve vstupu | Výstup v SQL |
|---|---|
'NOW()' (string) |
column = NOW() |
true (bool) |
column = 1 |
false (bool) |
column = 0 |
| jiné hodnoty | column = :column |
při useValues = true |
column = VALUES(column) |
Příklad: NOW(), true, false
$set = [
'published' => true,
'archived' => false,
'created' => 'NOW()',
'title' => 'Nový článek',
];
$sql = "INSERT INTO article SET " . DB::set($set);
DB::query($sql, [$set['title']]);
Upozornění: Zde se musí dát pozor. DB::set() první tři hodnoty doplní automaticky do SQL dotazu dle předchozí tabulky!
Lépe použít DB::params(), který přímo vkládané vypustí.
$sql = "INSERT INTO article SET " . DB::set($set);
DB::query($sql, DB::params($set));
Výsledný SQL fragment:
`published` = 1, `archived` = 0, `created` = NOW(), `title` = :title
Příklad: Vyloučení polí (exclude)
$set = [
'id' => 42,
'title' => 'Nový název',
'slug' => 'novy-nazev',
];
$sql = "UPDATE article SET " . DB::set($set, ['id']) . " WHERE id = :id";
DB::query($sql, [...$set]);
Příklad: useValues = true pro ON DUPLICATE KEY UPDATE
$set = [
'user_id' => 7,
'points' => 120,
];
$sql = "INSERT INTO leaderboard SET " . DB::set($set) .
" ON DUPLICATE KEY UPDATE " . DB::set($set, ['user_id'], true);
DB::query($sql, $set);
Výsledný fragment:
`points` = VALUES(`points`)
Práce s JSON daty a podmínkami přes JSON_EXTRACT
V některých případech potřebujeme pracovat s JSON daty přímo v SQL dotazu (např. s polem translations). To platí zejména u polí, která nejsou indexovaná a není třeba na ně aplikovat vyhledávání nebo řazení.
Typické scénáře:
- test na prázdný překlad (
[],[""],null) - práce s
JSON_EXTRACT(...) - nutnost vložit název JSON klíče přímo do SQL — nelze použít
?placeholder
Příklad: kontrola prázdné hodnoty v JSON
$_post = $_post ?? $_POST ?? [];
$_target = DB::escape($_post['target']); // bude použit přímo do SQL
$_section = $_post['section'];
$sql = "SELECT id FROM i18n
WHERE section = ? AND (
JSON_LENGTH(JSON_EXTRACT(translations, '$.$_target')) = ''
OR JSON_LENGTH(JSON_EXTRACT(translations, '$.$_target')) = 0
OR JSON_EXTRACT(translations, '$.$_target') IS NULL
OR JSON_EXTRACT(translations, '$.$_target') = '\[\"\"\]'
OR JSON_EXTRACT(translations, '$.$_target') = 'null')";
$ids = DB::results($sql, [$_section]);
Vysvětlení
$_targetje klíč v JSON objektu (translations) – musí být vložen do SQL dotazu přímo jako součást stringu.- Proto je nutné jej ručně escapovat přes
DB::escape()– kvůli bezpečnosti. - Parametr
sectionje stále předán běžně přes?. - Dotaz testuje různé formy prázdné hodnoty:
NULL, prázdné pole,[""],0.
Vkládání, úpravy a mazání dat
Pro všechny dotazy, které nevracejí výsledek (INSERT, UPDATE, DELETE), se používá DB::query().
Výsledky těchto operací je možné kontrolovat pomocí:
DB::$insertId– ID posledního vloženého záznamuDB::$lastError– text poslední chybyDB::$affectedRows– počet ovlivněných řádků
INSERT s insertId
$sql = "INSERT INTO log_ecomail (email, status, action, created_on)
VALUES (?, 0, ?, NOW())";
DB::query($sql, [$r->email, $action]);
if ($id = DB::$insertId) {
// Záznam byl vložen, máme ID
}
INSERT s DB::set() a kontrola chyby
$_post = $_post ?? $_POST ?? [];
$password = array_get_str($_post, 'password', '');
$set['password'] = hash('sha512', DB::escape($password));
$set['username'] = array_get_str($_post, 'username', '');
$set['name'] = array_get_str($_post, 'name', '');
$set['email'] = array_get_str($_post, 'email', '');
$set['group_id'] = array_get_int($_post, 'group_id', 1);
$set['status'] = array_get_str($_post, 'status', 'Blocked');
$sql = "INSERT INTO admin SET id = UUID(), " . DB::set($set);
DB::query($sql, $set);
$output = empty(DB::$lastError);
UPDATE s ověřením změn
$id = (int)($_get['id'] ?? $_GET['id'] ?? 0);
$forLang = $_get['forLang'] ?? $_GET['forLang'] ?? '';
$sql = "UPDATE news_lang SET homepage = NOT homepage WHERE news_id = ? AND lang = ?";
DB::query($sql, [$id, $forLang]);
$output = DB::$affectedRows;
DELETE s návratovou hodnotou
$_id = base64_decode(array_get_str($_GET, 'id', ''));
$sql = "DELETE IGNORE FROM redirection WHERE old = ?";
DB::query($sql, [$_id]);
$output = (bool)DB::$affectedRows;
Práce s transakcemi
Pokud je třeba provést více SQL operací jako celek (např. INSERT + UPDATE + DELETE), použij DB::beginTransaction() spolu s DB::commit() nebo DB::rollback().
Doporučený postup
DB::beginTransaction();- provedení všech dotazů (
DB::query(),DB::var(), …) - pokud OK →
DB::commit(); - při chybě →
DB::rollback();
Jednoduchý INSERT s transakcí
DB::beginTransaction();
$sql = "INSERT IGNORE INTO i18n (section, identifier, translations)
VALUES (?, ?, ?)";
DB::query($sql, [$_section, $_identifier, $translationsJson]);
if ($output = DB::$insertId) {
DB::commit();
} else {
DB::rollback();
}
Složený mazací dotaz s transakcí
$_id = (int)($_get['id'] ?? $_GET['id'] ?? 0);
$_web = (int)($_get['web'] ?? $_GET['web'] ?? 0);
DB::beginTransaction();
// Získání content_id podle ID stránky
$sql = "SELECT content_id FROM page WHERE id = ?";
$contentId = DB::var($sql, [$_id]);
// Smazání z content
$sql = "DELETE FROM content WHERE id = ?";
DB::query($sql, [$contentId]);
// Smazání z page
$sql = "DELETE IGNORE FROM page WHERE id = ?";
DB::query($sql, [$_id]);
$output = (bool)DB::$affectedRows;
// Smazání z tabulky slug
$sql = "DELETE FROM slug WHERE web_id = ? AND type='page' AND join_id = ?";
DB::query($sql, [$_web, $_id]);
if (DB::$lastError) {
DB::rollback();
} else {
DB::commit();
}
return outputJson($output);