Návrh databázové třídy - díl III: class Query

Mon, 3 May 2010 18:56:33 +1100

V minulém dílu seriálu o objektech v PHP5 a živém příkladu v podobě databázové třídy jsme si ukázali základní metody na typové SQL dotazů. Každá z těchto metod skončila vytvořením instance třídy Query, respektive zavoláním některé její metody. Jak to všechno funguje uvnitř třídy Query bude tématem tohoto článku.

Krátce ještě zopakuji, co vše už máme: jsou to základní metody třídy Db pro volání SQL dotazů: SELECT, UPDATE, INSERT, DELETE. Hodnoty, které nás zajímají při změně či odstranění řádků tabulky už máme přímo v návratových hodnotách jednotlivých metod. Co ale ještě hotové nemáme, je zpracování klasického výběru dat z databáze.

Cíle

Cílem třídy Query bude kompletní obsluha SQL dotazů. Ty, jak už jsem se zmiňoval, se provedou přímo v konstruktoru. Jednotlivé metody pak už budou vracet zpracovaná data: celé řádky i sloupce, jednotlivé řádky či jediný sloupec v jediném řádku. Pokud se jí něco nebude líbit, vyhodí výjimku a dále už se o nic nestará: to bude v pravomoci třídy DbException, které se budu věnovat v pokračování. Seznam členských proměnných napoví základní vlastnosti:

final class Query implements QueryModel {

	// reference na db objekt
	// @var object
	private $db;

	// obecne nastaveni vsech instanci
	// nahrada titulku
	public static $titleReplace = false;

	// nyni uz info o danem SQL dotazu
	// query : string | resource | assocList | num_rows | affected_rows | insert_id
	private $stringQuery;
	private $query;
	private $assocList;
	private $numRows;
	private $affectedRows;
	private $insertId;
}

V konstruktoru pak proběhne funkce mysql_query, která při nezdaru skončí výjimkou. Mějte ovšem na paměti, že návrh nepočítá s vytvářením instancí z jiného zdroje než jsou metody třídy Db. PHP bohužel neumí nadefinovat třídu ve třídě a tím zúžit její viditelnost, čímž pádem je potřeba tato pravidla uvést v dokumentaci.

Konstruktor je opravdu triviální, tam snad nic popisovat nemusím.

public function __construct(Db $db, $query){
	$this->db = $db;
	try {
		$conn = $this->db->getConnection();
		$this->query = @mysql_query($query, $conn);
		$this->stringQuery = $query;
		if(!$this->query){
			throw new DbException(null, $query);
		}
	}catch(Exception $e){
		$e->show();
	}
}

mysql_num_rows, affected_rows, insert_id

Metody, jenž jsem referoval v předchozím článku zajistí základní operace nad výsledkem dotazu. mysql_affected_rows a insert_id je automaticky vráceno patřičnými metodami třídy Db, pro num_rows ale musíme šáhnout na jednu z metod. Pro úplnost je zde uvedu všechny 3. Každá z nich je ošetřená proti vícenásobnému volání, abychom zbytečně neprováděli již provedené operace. Na testování používám isset ku nullovým parametrům. Míchat sem například boolean hodnoty by bylo špatně.

public function numRows(){
	if(isset($this->numRows)){
		return $this->numRows;
	}else{
		return $this->countNumRows();
	}
}

public function insertId(){
	if(!isset($this->insertId)){
		$this->insertId = mysql_insert_id();
	}
	return $this->insertId;
}

public function affectedRows(){
	if(!isset($this->affectedRows)){
		$this->affectedRows = mysql_affected_rows();
	}
	return $this->affectedRows;
}

Získání řádku, celé tabulky a jedné položky

Abych i zde splnil cíl, který jsem si na začátku nadefinoval, přidám knihovně metody, které mi budou vracet data v přesně takovém formátu, v jakém je požaduji.

mysql_result

mysql_result jakožto výchozí funkce není moc šikovná, protože vrací errory, pokud SQL dotaz nevrátí žádný řádek. To ošetříme:

public function result($col = 0, $field = null) {
	if ($this->numRows()) {
		return mysql_result($this->query, $col, $field);
	} else {
		return false;
	}
}

Jediný řádek

Často prováděná operace, proto ji zakonzervujeme do další metody. Bude vracet buď pole nebo false. Metoda fetch zastoupí všechny čtyři mysql_fetch funkce, s výchozím nastavením na assoc.

public function result($col = 0, $field = null){
	if($this->numRows()){
		return mysql_result($this->query, $col, $field);
	}else{
		return false;
	}
}

public function row($type = FETCH_ASSOC){
	$return = $this->fetch($type);
	return !empty($return) ? $return : false;
}

Asociativní pole z SQL dotazu

Přichází na řadu nejsilnější a nejvyužívanější metoda: selectAssocList. Jejím účelem je jak jinak než převod výsledku dotazu do nějaké slušné podoby. Jednotlivé argumenty pak nahrazují další často prováděné operace, které bychom museli dělat mimo třídu během projíždění pole dalším cyklem.

Mám zde dvě ukázková chování: generování odkazu a přiřazení výchozí hodnoty do prázdného sloupce. V případě, že výsledek obsahuje jak sloupec name tak sloupec title (a je zapnuté nahrazování), doplní se do každé prázdné hodnoty title obsah sloupečku name.

V prvním parametru předáme jméno sloupce, jenž bude sloužit jako klíč. V dalších dvou pak parametry pro generování odkazů. Pokud zavoláme i s posledním parametrem, můžeme slepit více polí dohromady.

// navrat asociativniho pole
// @param string [$key] - sloupec, kt. bude klic
// @param string [$col] - sloupec, kt. bude odkaz
// @param string [$pattern] - pro generovani odkazu, ve tvaru http://www.treba.cz/kategorie/%s/
// @return array
public function assocList($key = null, $col = null, $pattern = null, $array = array()){
	static $checked = null;

	if(isset($this->assocList)){
		return $this->assocList;
	}else{
		$this->assocList = (array)$array;
	}

	while($t = $this->fetch()){
		if(isset($col, $pattern, $t[$col])){
			if(!isset($checked)){
				$checked = (mb_substr_count($pattern, '%s', 'utf-8') == 1);
			}
			if($checked){
				$t['href'] = sprintf($pattern, $t[$col]);
			}
		}

		// je nastaveno automaticke doplnovani titulku ze jmena
		if(self::$titleReplace && array_key_exists('name', $t) && array_key_exists('title', $t)){
			$t['title'] = $t['title'] ? $t['title'] : $t['name'];
		}

		if(isset($key, $t[$key])){
			$this->assocList[$t[$key]] = $t;
		}else{
			$this->assocList[] = $t;
		}
	}

	return $this->assocList;
}

Automatizace mysql_free_resultu

V úvodním dílu jsem psal něco o tom, že volání funkce mysql_free_result patří k těm otravným činnostem, kterých bychom se rádi zbavili. Celou věc vyřešíme efektivně: funkci zavoláme prostě z destruktoru, takže kdykoli zanikne proměnná s objektem aktuálního SQL dotazu, provede se automaticky i mysql_free_result.

public function __unset($value = null){
	if($this->query !== false && is_resource($this->query)){
		mysql_free_result($this->query);
	}
	$this->query = false;
}

public function __destruct(){
	$this->__unset();
}

Použití

Nyní už k použití. Jak jsem sliboval, zápis jednotlivých metod bude jednodušší, intuitivnější:

// vygenerování asociativního pole včetně odkazů na kategorie
$query = "SELECT * FROM ?_tabulka WHERE parent_id = '?'";
$array = $db->query($query, array($_GET['id']))->assocList('id', 'seo_url', './kategorie/%s/');

// vygenerování asociativního pole, klíče klasicky od nuly
$array = $db->query("SELECT * FROM ?_tabulka")->assocList();

// mysql_result
$result = $db->query("SELECT id FROM ?_tabulka LIMIT 1")->result();

// vytvoření zdroje + projetí cyklem
$q = $db->query("SELECT * FROM ?_tabulka");
while ($item = $q->fetch()) {

}

// získání jediného řádku
$query = "SELECT * FROM ?_categories WHERE seo_url = '?'";
$category = $db->query($query, array($_GET['path']))->row();

V minulém díle seriálu o objektech v PHP5 a živém příkladu v podobě databázové třídy jsme si ukázali základni metody na typové SQL dotazů. Každá z těchto metod skončila vytvořením instance třídy Query, respektive zavoláním některé její metody. Jak to všechno funguje uvnitř třídy Query bude tématem tohoto článku.

Krátce ještě zopakuji, co vše už máme: jsou to základní metody třídy Db pro volání SQL dotazů: SELECT, UPDATE, INSERT, DELETE. Hodnoty, které nás zajímají při změně či odstranění řádků tabulky už máme přímo v návratových hodnotách jednotlivých metod. Co ale ještě hotové nemáme, je zpracování klasického výběru dat z databáze.
Cíle

Cílem třídy Query bude kompletní obsluha SQL dotazů. Ty, jak už jsem se zmiňoval, se provedou přímo v konstruktoru. Jednotlivé metody pak už budou vracet zpracovaná data: celé řádky i sloupce, jednotlivé řádky či jediný sloupec v jediném řádku. Pokud se jí něco nebude líbít, vyhodí vyjímku a dále už se o nic nestará: to bude v pravomoci třídy DbException, které se budu věnovat v pokračování. Seznam členských proměnných napoví základní vlastnosti:
final class Query implements QueryModel {

/**
* reference na db objekt
* @var object
**/
private $db;

/**
* obecné nastavení všech instancí
* náhrada titulku
**/
public static $titleReplace = false;

/**
* nyní už info o daném SQL dotazu
* query : string | resource | assocList | num_rows | affected_rows | insert_id
**/
private $stringQuery;

private $query;

private $assocList;

private $numRows;

private $affectedRows;

private $insertId;
}

V konstruktoru pak proběhne funkce mysql_query, která při nezdaru skončí vyjímkou. Mějte ovšem na paměti, že návrh nepočítá s vytvářením instancí z jiného zdroje než jsou metody třídy Db. PHP bohužel neumí nadefinovat třídu ve třídě a tím zúžit její vyditelnost, čímž pádem je potřeba tato pravidla uvést v dokumentaci.

Konstruktor je opravdu triviální, tam snad nic popisovat nemusím.
/**
* konstruktor
* @param Db object $db
* @param string $query
**/
public function __construct(Db $db, $query) {
$this->db = $db;

try {
$conn = $this->db->getConnection();
$this->query = @mysql_query($query, $conn);
$this->stringQuery = $query;

if (!$this->query) {
throw new DbException(null, $query);
}
} catch (Exception $e) {
$e->show();
}
}
mysql_num_rows,
affected_rows,
insert_id

Metody, jenž jsem referoval v předchozím článku zajistí základní operace nad výsledkem dotazu. mysql_affected_rows a insert_id je automaticky vráceno patřičnými metodami třídy Db, pro num_rows ale musíme šáhnout na jednu z metod. Pro úplnost je zde uvedu všechny 3. Každá z nich je ošetřená proti vícenásobnému volání, abychom zbytečně neprováděli již provedené operace. Na testování používám isset ku nullovým parametrům. Míchat sem například boolen hodnoty by bylo špatně.
public function numRows() {
if (isset($this->numRows)) {
return $this->numRows;
} else {
return $this->countNumRows();
}
}

public function insertId() {
if (!isset($this->insertId)) {
$this->insertId = mysql_insert_id();
}
return $this->insertId;
}

public function affectedRows() {
if (!isset($this->affectedRows)) {
$this->affectedRows = mysql_affected_rows();
}
return $this->affectedRows;
}
Získání
řádku,
celé
tabulky
a
jedné
položky

Abych i zde splnil cíl, který jsem si na začátku nadefinoval, přidám knihovně metody, které mi budou vracet data přesně v takovém formátu, v jakém je požaduji.
mysql_result

Mysql_result jakožto výchozí funkce není moc šikovná, protože vrací errory, pokud SQL dotaz nevrátí žádný řádek. To ošetříme:
public function result($col = 0, $field = null) {
if ($this->numRows()) {
return mysql_result($this->query, $col, $field);
} else {
return false;
}
}
Jediný
řádek

Často prováděná operace, proto ji zakonzervujeme do další metody. Bude vracet buď pole nebo false. Metoda fetch zastoupí všechny čtyři mysql_fetch funkce, s výchozím nastavením na assoc.
public function fetch($type = FETCH_ASSOC) {
$function = 'mysql_fetch_'.$type;
return $function($this->query);
}

public function row() {
$return = $this->fetch();
return !empty($return) ? $return : false;
}
Asociativní
pole
z
SQL
dotazu

Přichází na řadu nejsilnější a nejvyužívanější metoda: selectAssocList. Jejím účelem je jak jinak, než převod výsledku dotazu do nějaké slušné podoby. Jednotlivé argumenty pak nahrazují další často prováděné oprace, které bychom museli dělat mimo třídu během projíždění pole dalším cyklem.

Mám zde dvě ukázková chování: generování odkazu a přiřazení výchozí hodnoty do prázdného sloupce. V případě, že výsledek obsahuje jak sloupec name tak sloupec title (a je zapnuté nahrazování), doplní se do každé každé prázdné hodnoty title obsah sloupečku name.

V prvním parametru předáme jméno sloupce, jenž bude sloužit jako klíč. V dalších dvou pak parametry pro generování odkazů. Pokud zavoláme i s posledním parametrem, můžeme slepit více polí dohromady.
/**
* návrat asociativního pole
* @param string [$key] - sloupec, kt. bude klíč * @param string [$col] - sloupec, kt. bude odkaz
* @param string [$pattern] - pro generování odkazu, ve tvaru http://www.treba.cz/kategorie/%s/
* @return array
**/
public function assocList($key = null, $col = null, $pattern = null, $array = array()) {
static $checked = null;

if (isset($this->assocList)) {
return $this->assocList;
} else {
$this->assocList = (array)$array;
}

while ($t = $this->fetch()) {
if (isset($col, $pattern, $t[$col])) {

// pouze pro to, aby mb_substr_count proběhl jen jednou
if (!isset($checked)) {
if (mb_substr_count($pattern, '%s', 'utf-8') == 1) {
$checked = true;
} else {
$checked = false;
}
}

if ($checked) {
$t['href'] = sprintf($pattern, $t[$col]);
}
}

// je nastaveno automatické doplňování titulku ze jména
if (self::$titleReplace && array_key_exists('name', $t) && array_key_exists('title', $t)) {
$t['title'] = $t['title'] ? $t['title'] : $t['name'];
}

if (isset($key, $t[$key])) {
$this->assocList[$t[$key]] = $t;
} else {
$this->assocList[] = $t;
}
}

return $this->assocList;
}
Automatizace
mysql_free_resultu

V úvodním díle jsem psal něco o tom, že volání funkce mysql_free_result patří k těm otravným činostem, kterých bychom se rádi zbavili. Celou věc vyřešíme efektivně: funkci zavoláme prostě z destruktoru, takže kdykoli zanikne proměnná s objektem aktuálního SQL dotazu, provede se automaticky i mysql_free_result.
public function __unset($value = null) {
if ($this->query !== false && is_resource($this->query)) {
mysql_free_result($this->query);
}
$this->query = false;
}

public function __destruct() {
$this->__unset();
}
Použití

Nyní už k použití. Jak jsem sliboval, zápis jednotlivých metod bude jednodušší, intuitivnější:
// vygenerování asociativního pole včetně odkazů na kategorie
$query = "SELECT * FROM ?_tabulka WHERE parent_id = '?'";
$array = $db->query($query, array($_GET['id']))->assocList('id', 'seo_url', './kategorie/%s/');

// vygenerování asociativního pole, klíče klasicky od nuly
$array = $db->query("SELECT * FROM ?_tabulka")->assocList();

// mysql_result
$result = $db->query("SELECT id FROM ?_tabulka LIMIT 1")->result();

// tytvoření zdroje + projetí cyklem
$q = $db->query("SELECT * FROM ?_tabulka");
while ($item = $q->fetch()) {

}

// získlání jediného řádku
$query = "SELECT * FROM ?_categories WHERE seo_url = '?'";
$category = $db->query($query, array($_GET['path']))->row();

Edit 4. 5. 2020:
Text se týká PHP 5. Pod PHP 7 už třída fungovat nebude, protože všechny mysql_ funkce skončí chybou.