<?xml version="1.0" encoding="utf-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
<channel>
	
	<title>RSS 2.0 Článků kategorie "PHP"</title>
	<atom:link href="https://mike.treba.cz/rss/kategorie/php/" rel="self" type="application/rss+xml" />
	<link>https://mike.treba.cz/</link>
	<description>RSS 2.0 Článků kategorie "PHP"</description>
	<lastBuildDate>Sun, 21 Jun 2020 14:41:05 +1100</lastBuildDate>
	<language>cs</language>
	<generator>Abstract CMS</generator>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>

		
		<item>
			<title>Pokročilé stránkování v PHP</title>
			<link>https://mike.treba.cz/pokrocile-strankovani-php/</link>
			<pubDate>Sun, 21 Jun 2020 14:41:05 +1100</pubDate> 
			<comments>https://mike.treba.cz/pokrocile-strankovani-php/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>Za víc než 10 let, co svou stránkovací knihovnu používám, už prošla řadami změn. Od nepatrných, jako jsou bug fixy až po zásadní, jako například změna parametrů v konstruktoru. Některé nové funkce byly přidány, ale základní kostra je víceméně stejná. </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/pokrocile-strankovani-php/</guid>
			<content:encoded><![CDATA[<p>Za víc než 10 let, co svou stránkovací knihovnu používám, už prošla řadami změn. Od nepatrných, jako jsou bug fixy až po zásadní, jako například změna parametrů v konstruktoru. Některé nové funkce byly přidány, ale základní kostra je víceméně stejná. </p> <p>Ano, už jste asi pochopili, že v následujícím článku vás nebudu učit, jak vypsat cyklem stránkování. Raději vám představím hotové řešení: takové, které na pár řádcích zařídí vše potřebné. Vy pak nemusíte řešit malichernosti a soustředit se na důležitější věci. Například optimalizace SQL dotazů. </p>
<p>Tak se jdeme podívat, jak na to. Článek byl poprvé publikován v roce 2010 a nyní, po rozsáhlé revizi stránkovací knihovny, ho znovu posouvám výš. Na PHP7 už to samozřejmě funguje. </p>
<h2>Základní nasazení</h2>
<p>Samotné nasazení jsem se snažil zredukovat na nutné minimum tak, abychom nemuseli volat hromady _set funkcí, a zároveň, aby šlo stále nastavit vše potřebné. </p>
<pre><code class="php">// SQL dotaz samozrejme upravim v souladu se svym frameworkem
$count = $mysqli-&gt;query("SELECT COUNT(*) FROM `tabulka`")-&gt;fetch_row();
$total = $count[0];

$paging_limit = 20;

// celkem | po kolika
$paging = new Paging($total, $paging_limit);
$paging-&gt;set_paging();

$paging_start = $paging-&gt;get_start();
$query = "SELECT * FROM `tabulka` LIMIT {$paging_start}, {$paging_limit}"; // -&gt; assoc_list

// simple vypis
echo $paging-&gt;export_paging();</code></pre>
<p>A to je vše. Jedním SQL dotazem spočítám záznamy, někde z nastavení vytáhnu stránkovací limit. Zbytek si knihovna zjistí sama. Výchozí proměnná pro stránkování je <strong>$_GET['page']</strong>, v případě potřeby jiné proměnné ji lze změnit v konstruktoru. Ukážu na dalším příkladu. Co se odkazů týče, vše je automatické. Ať už oddělovač ? nebo &amp;, tak doplnění celého query stringu, pokud ho chceme předávat dál. Vstup je automaticky ošetřen: _GET parametr je přetypován na int, takže ani na tohle už myslet nemusíme. </p>
<p>Na výše uvedeném příkladu dostaneme defaultní výstup. Jak ho změnit si ukážeme v rozšířeném nastavení.</p>
<h2>Rozšířené nastavení</h2>
<p>Kromě základních parametrů můžeme knihovně předat i spoustu dalších informací. Nejčastěji využijeme přejmenovaní GET proměnné. Nechceme-l GET['page'], stačí nastavit třetí argument. Podporu má i jednoduché pole. Umíme i vícerozměrné, nicméně takové věci už podle mého názoru nemají v URL co dělat. Ale pro jistotu. </p>
<p>Další důležité metody jsou <strong>set_paging_mode()</strong>, <strong>set_output_mode<strong>()</strong></strong> a <strong>set_around<strong>()</strong></strong>. Ty definují, jaký typ stránkování dostaneme na výstupu. Jednotlivé příklady najdete na konci článku. </p>
<p>Generování odkazů je automatické, vezme se REQUEST_URI a zkopíruje se celý GET vyjma definované proměnné. Pokud nastavím <strong>set_base_path()</strong>, automatické generování přestane. Dostanu pouze to, co jsem třídě předal, tedy například www.treba.cz/?page=2. V takovém případě pak další z metod, <strong>set_drop_vars<strong>()</strong></strong>, nemá význam. </p>
<p>Změnu generovaného HTML pomocí metody set_html_pattern() tu více rozvádět nebudu, ať mi jednoduchý návod nenakyne do obřích rozměrů. Na to se můžete podívat ve zdrojáku souboru.</p>
<p>A na závěr opět metoda set_paging, která si navíc umí pohlídat, jestli je volaná, kdy má. </p>
<pre><code class="php">$paging = new Paging($total, $paging_limit, 'filter[page]');
$paging-&gt;set_paging_mode(1)               // 1|0
       -&gt;set_output_mode(1)               // 1|0
       -&gt;set_around(2)                    // pri paging_mode=1
       -&gt;set_valid_links(true)            // replace [] pri slozenych odkazech

       -&gt;set_base_path($category_url)     // nechci generovat nic, predam zaklad odkazu
       -&gt;set_default_page(2)              // kdyz neni GET[page] zacinam na strane 2
       -&gt;set_drop_vars(array('ajax'))     // chci zkopirovat cely get krome ?ajax=
       -&gt;disable_translate_function(true) // mam funkci __() a dela neco jineho, nez ze akceptuje 1 string a vraci 1 string =&gt; nebudu prekladat

       -&gt;set_title_format($dt, $mb, $mn)  // desktop title, mobile back, mobile next
       -&gt;set_html_patterns($pattern)      // @see protected $pattern
       -&gt;set_paging();</code></pre>
<h3>Funkce __ <span class="small-text">(dvě podtržítka)</span></h3>
<p>Jednou z metod, které můžete či nemusíte zavolat, je také "<strong>disable_translate_function()</strong>". Protože počítám i s lokalizací titulků, testuji existenci funkce dvě podtržítka. Můžete ji najít například ve Wordpressu, používá ji i Kohana framework. Pokud ovšem takovou funkci máte k něčemu jinému, přidal jsem i možnost její volání vypnout. Stejně tak pro případ, že si chcete přeložit texty ještě před zavoláním stránkovací knihovny. Proto ani neřeším žádné callbacky. Pokud tuto funkci nemáte vůbec, neřešíte, protože volání je vždy podmíněno function_existem. </p>
<h3>Filosofická vsuvka</h3>
<p>Na chvíli se ještě vrátím zpět k parsování QUERY STRINGu a celkové logice předávání parametrů. Jako správně by to knihovna vůbec neměla dělat. Měla by dostat URL, referenci na jeden klíč a podle toho se zařídit. Jenže... To bychom pak v systému neřešili nic jiného, než které proměnné předat a které ne. Takhle máme možnost buď vše anebo nic. Funkce <em>set_drop_vars</em> je výjimka z tohoto pravidla, ale tady jsem si prostě chtěl ušetřit práci. Raději zvolím řešení, díky kterému nemusím na nic myslet a vím, že to skript vyřeší za mě. </p>
<h2>Výstup do šablony</h2>
<p>Jak je zmíněno výše, máme celkem 4 způsoby, jak stránkování vypsat. A k tomu navíc pátý pro telefon. Pokud se tedy nespokojíme se základním echo $paging; můžeme si na výstupu podmínit i alternativní výpis právě pro mobilní zařízení: namísto hromady odkazů jenom tlačítko "Následující" respektive "Předchozí". Jakoukoli další modifikaci zařídí styly, protože každý z elementů lze snadno a unikátně zacílit. </p>
<pre><code class="php">&lt;div class="paging"&gt;
	&lt;div class="hide-this-on-phone"&gt;
		&lt;?=$paging-&gt;export_paging();?&gt;
	&lt;/div&gt;
	&lt;div class="hide-this-on-desktop"&gt;
		&lt;?=$paging-&gt;export_mobile();?&gt;
	&lt;/div&gt;
&lt;/div&gt;</code></pre>
<p>A pokud jsme ještě náročnější a ani tohle nám nestačí, pak nám pomůže poslední funkce a my si můžeme s výstupem dělat cokoli, co se nám zlíbí. Ale jak říkám, tohle je jedna z věcí, kterou člověk opravdu nechce zaplácat jednoduchou šablonu. Když se sami podíváte na funkci <strong>export_paging()</strong>, fakt není úplně jednoduchá. Ale proti gustu... :-) </p>
<pre><code class="php">&lt;xmp&gt;
&lt;?php print_r($paging-&gt;export_data()); ?&gt;
&lt;/xmp&gt;</code></pre>
<h2>Varianty výstupu</h2>
<pre><code>export_paging();
1 ... 6 | 7 | 8  ... 50       (paging_mode = 1 | output_mode = 1) // vychozi
 &lt;&lt;  &lt;  5  &gt;  &gt;&gt;              (paging_mode = 1 | output_mode = 0 | vhodne dat set_around=1)
1 | 2 | 3 | 4 | 5             (paging_mode = 0 | output_mode = 0)
1-10 | 11-20 | 21-30 | 31-32  (paging_mode = 0 | output_mode = 1)

export_mobile();
&lt; Predchazejici  Nasledujici &gt;</code></pre>
<p>Na zdroják se můžete podívat nebo si ho stáhnout. Člověk by si řekl - že maličkost - ale je to pěkných 600 řádků kódu. </p>
<p class="download-links"><a href="https://mike.treba.cz/doc/2020/paging_lib/" target="_blank">class.Paging.php</a><br /><a href="https://mike.treba.cz/doc/2020/paging_lib/class.Paging2.zip">class.Paging.zip</a></p>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2020/web/php.jpeg</url>
			</image>
		</item>
		
		
		<item>
			<title>Správa redirectů s využitím databáze</title>
			<link>https://mike.treba.cz/redirect-php-mysql/</link>
			<pubDate>Sun, 8 Mar 2020 13:18:00 +1100</pubDate> 
			<comments>https://mike.treba.cz/redirect-php-mysql/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>Je tomu už příliš dlouho, co jsem naposledy psal nějaký článek z oboru, a tak je nejvyšší čas tento nedostatek napravit. Je tomu už podobně dlouho, co jsem naposledy něco skutečně programoval, a tak doufám, že můj kód nebude ostuda. Ale chtěl bych své články na téma zábava zase občas proložit něčím víc technickým. Většina mých článků je směrovaná na vývojáře začínající, dnes to bude ale spíš na ty středně pokročilé. Podíváme se na správu redirectů. </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/redirect-php-mysql/</guid>
			<content:encoded><![CDATA[<p>Je tomu už příliš dlouho, co jsem naposledy psal nějaký článek z oboru, a tak je nejvyšší čas tento nedostatek napravit. Je tomu už podobně dlouho, co jsem naposledy něco skutečně programoval, a tak doufám, že můj kód nebude ostuda. Ale chtěl bych své články na téma zábava zase občas proložit něčím víc technickým. Většina mých článků je směrovaná na vývojáře začínající, dnes to bude ale spíš na ty středně pokročilé. Podíváme se na správu redirectů. </p> <p>Pokud jste úplný začátečník, těžko si nacpete následující kód do vlastního webu. (<em>V takové situaci doporučuji použít čistě <a href="mod_rewrite-a-hezke-url-dil-iii/">.htaccess.</a></em>) Používáte-li komplexní redakční systém, podobný plugin už nejspíš obsahuje. A pokud ne, můžete si vzít následující kód jako inspiraci, co změnit či upravit.</p>
<h2>Proč chceme přesměrovat</h2>
<p>Pokud jste přesměrování jedné adresy na druhou ještě nepotřebovali, pravděpodobně se svému webu málo věnujete :-) V opačném případě jistě víte, že změna URL adresy znamená ztrátu pozice v Googlu, kterou jste složitě budovali. A právě tohle vyřešíte přesměrováním s hlavičkou 301.</p>
<p>Možností jak danou funkcionalitu integrovat do systému je více a já vám dnes ukážu jednu z nich. Redirecty uložené v samotné tabulce mají jednu velkou výhodu, kterou je jejich přehlednost. Pokud bychom tyto informace ukládali automaticky a řešili je například na úrovní ukládání článku (<em>změní se url článku, uložím si automaticky redirect</em>), museli bychom provádět obrovské množství různých kontrol napříč mnoha tabulkami. A co víc, snadno ztratíme přehled, co směruje kam. Nemluvě o automatickém čištění všech předchozích redirectů s každým uložením.</p>
<p>Nevýhoda odděleného řešení je pak samozřejmě ta, že na to člověk musí myslet. Ale pokud se o svůj web staráte, tak URL, která skáče na první stránce v Googlu asi neměníte každý den. A nastavit případný redirect člověk jistě nezapomene.</p>
<h3>Kód #1: SQL</h3>
<p>Začneme tabulkou v databázi. Ukládat chceme výchozí url, cílovou url, checkbox pro přesnou shodu, checkbox pro přesnou náhradu (<em>vysvětlím níže</em>) a poznámku. Ta bude sloužit ke krátkému popisu, proč redirect vlastně zakládám. Na závěr samozřejmě možnost deaktivace a čas vytvoření záznamu. <em>original_url</em> chceme mít jako unikátní klíč, protože více redirectů ze stejné adresy je nesmysl. </p>
<pre><code class="sql">CREATE TABLE IF NOT EXISTS `project_redirects` (
	`id` int(10) unsigned NOT NULL   AUTO_INCREMENT,
	`original_url` varchar(255) NOT NULL,
	`redirect_url` varchar(255) NOT NULL,
	`exact_match` tinyint(1) DEFAULT '1',
	`do_replace` tinyint(1)  DEFAULT '0',
	`note` varchar(255) DEFAULT NULL,
	`active` tinyint(1) DEFAULT '1',
	`timestamp` timestamp  DEFAULT CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`),
	UNIQUE KEY `original_url` (`original_url`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;</code></pre>
<h2>Jak to bude fungovat </h2>
<p>Ze struktury tabulky už nejspíš pochopíte i chování. Uložíme původní a cílovou url a při každém načtení stránky zjistíme, jestli má aktuální URL kam přesměrovat. Porovnáme vždy relativní i absolutní variantu cesty.</p>
<p>Pole <em>exact_match</em> nám řekne, jestli budeme testovat přesnou shodu nebo nasadíme funkci <em>strpos</em>. Pole <em>do_replace</em> následně definuje, jestli provádíme klasický <em>str_replace</em> nebo chceme směrovat vše na novou url. </p>
<p><strong>Jaké případy tedy umíme podchytit?</strong></p>
<ul>
<li>Jedna konkrétní adresa na novou adresu: <em>/kategorie/ =&gt; /nova-kategorie/</em></li>
<li>Zahodíme celou větev a přesměrujeme na novou adresu: <em>/kategorie/vše/</em> =&gt; <em>/nova-kategorie/</em></li>
<li>Celá větev změní jen část své adresy: <em>/kategorie/cokoli/</em> =&gt; <em>/nova-kategorie/cokoli/</em></li>
</ul>
<p>Porovnání přes strpos === 0 zaručí, že se podmínka chytí jenom na adresy, které daným stringem začínají. Takže když bude stejný string zanořený někde hlouběji, nic se nestane. Vše je navíc navrženo tak, abych tu <em>root_url</em> nemusel nikam ukládat, ať u případné změny domény neřeším absolutní cesty bůhví kde v databázi. </p>
<h3>Kód #2: .htaccess</h3>
<p>Abychom mohli provádět efektivnější redirecty, jsou potřeba i nějaké ty "<a href="mod_rewrite-a-hezke-url/">hezké url</a>". Následující zápis nasměruje vše, co není existující soubor nebo složka na index. Pro spuštění v podadresáři je pak potřeba přidat i <strong>RewriteBase</strong>. </p>
<pre>RewriteEngineOn<br />#RewriteBase /redir/
RewriteCond %{REQUEST_FILENAME} !-d 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?path=$1 [L,QSA]</pre>
<h3>Kód #3: základní proměnné, funkce a připojení k databázi</h3>
<p>Zpracujeme primární GET proměnnou, nastavíme ROOT a připojíme se k databázi. Databázi si jistě vyřeší každý po svém, tady jsem uvedl jen minimum kódu tak, aby ukázka fungovala. <em>(Pro sešny platí to samé.)</em> Funkce <strong>get_url</strong> nám pak vrátí celou absolutní URL, na které se zrovna nacházíme. </p>
<pre><code class="php">$_root_url = 'http://localhost/redir/';
$_path = (!empty($_GET['path']) ? (string)$_GET['path'] : '');

$config['db_host'] = 'localhost'; 
$config['db_user'] = 'root';
$config['db_pass'] = ''; 
$config['db_name'] = 'evil';

$sqli = new mysqli($config['db_host'], $config['db_user'], $config['db_pass'], $config['db_name']);
$sqli-&gt;set_charset('utf8');
if(!empty($sqli-&gt;connect_error)){
 	exit($sqli-&gt;connect_error);
}

session_start();

function get_url($header = false){
	$pure_url = null;
	$html_url = null;
	
	if(!$pure_url){
		$url = (isset($_SERVER['HTTPS']) ? 'https://' : 'http://');
		$url .= $_SERVER['SERVER_NAME'];
		$port = explode(':', $_SERVER['HTTP_HOST']);
		if(!empty($port[1])){
			$url .= ':'.$port[1];
		}
		$url .= $_SERVER['REQUEST_URI'];
		$pure_url = $url;
		$html_url = str_replace('&amp;', '&amp;', $pure_url);
	}
	
	return $header ? $pure_url : $html_url;
}</code></pre>
<h3>Kód #4: Samotná funkce</h3>
<p>Hned první blok kódu vznikl po analýze adres, kterou často navštěvují spamboti. Jakože fakt často. Takže mezera (<em>respektive plus</em>) hranatá závorka se automaticky přesměruje na <strong>root</strong>. </p>
<p>Pak už jen vytáhneme všechny existující redirecty z databáze a pustíme se do kontroly. SQL dotaz by šel určitě lépe vydefinovat tak, aby se netahaly úplně všechny řádky, ale na druhou stranu počítám s nějakou lidsky rozumnou správou. Fakt tam nechceme mít stovky řádků. Což bychom ani mít neměli, když je možnost vypnout <em>exact_match</em> a obsáhnout i větší množství záznamů jedním řádkem. </p>
<p>V cyklu podle <em>exact_match</em> zvolíme způsob porovnání. No a následuje samotné ošetření smyčky. <strong>Redirect loop</strong> totiž není příliš fajn věc, protože vám to zpravidla zahlásí až prohlížeč. Žádnou chybovku od serveru nedostaneme, což se opravdu špatně ladí. Třeba Chrome už se v poslední době umoudřil a stránku zabije poměrně rychle, ovšem na to se nedá vždy spolehnout. Raději si tam podstrčím hlášku vlastní, abych hned viděl, kde je problém. Stejné ošetření pak můžeme provádět během jakéhokoli systémového redirectu. </p>
<pre><code class="php">// redirekty z tabulky
// plus nejake natvrdo
function redirect_check($_path, $_root_url, $sqli){
	
	// tohle delaji spamboti
	if(strpos($_path,' [') !== false){
		header('Location: ' . $_root_url);
		return false;
	}
	
	$redir_query = $sqli-&gt;query("SELECT original_url, redirect_url, exact_match, do_replace FROM project_redirects WHERE `active` = 1 ORDER BY id DESC");
	$redirects = array();
	while($row = $redir_query-&gt;fetch_assoc()){
		$redirects[] = $row;
	}
	
	// podminka vzdy pro relativni i absolutni URL
	// vse bud relativni nebo absolutni
	// neni-li exact_match, testujeme jestli url danym retezcem zacina, at to nereplacuje kdekoli
	// neni-li replace (default 0), pak to redirektuje vse co danym stringem zacina na cilovou url
	foreach($redirects as $item){
		if(!empty($item['exact_match'])){
			
			if(($_path == $item['original_url']) || (get_url(true) == $item['original_url'])){
				$redirect_target = $item['redirect_url'];
			}
			
		}else{
			
			if((strpos(get_url(true), $item['original_url']) === 0) || (strpos($_path, $item['original_url']) === 0)){
				if(!empty($item['do_replace'])){
					$redirect_target = str_replace($item['original_url'], $item['redirect_url'], $_path);
				}else{
					$redirect_target = $item['redirect_url'];
				}
			}	
			
		}
	}
</code></pre>
<pre><code class="php">	
	if(!empty($redirect_target)){
	
		// zastupny znak pro redirekt na homepage
		if($redirect_target == '/'){ 
			$redirect_target = $_root_url;
		}
		
		if((strpos($redirect_target, 'http://') === false) &amp;&amp; (strpos($redirect_target, 'https://') === false)){
			$redirect_target = $_root_url . $redirect_target;
		}
		
		// redirect_happened bude vzdy prazdny, pokud nedojde k zacykleni
		if(!empty($_SESSION['redirect_happened']) &amp;&amp; $_SESSION['redirect_target_url'] == get_url(true)){		
			
			// potreba vynulovat
			// aby to zase nechciplo, kdyz to v administraci opravim
			// cela konstrukce musi byt obalena v try catch
			// musi se porovnavat URL, nestaci 1/0, kvuli requestovym spam botum
			$exception_message = 'Redirect linking to another.'; 
			$exception_message.= '&lt;br&gt;Previous URL: ' . $_SESSION['redirect_prev_url'];
			$exception_message.= '&lt;br&gt;Next URL: ' . $_SESSION['redirect_target_url'];

			$_SESSION['redirect_happened']   = false;
			$_SESSION['redirect_target_url'] = null;
			$_SESSION['redirect_prev_url']   = null;
			
			exit($exception_message);
		}
		
		$_SESSION['redirect_happened']   = true;
		$_SESSION['redirect_target_url'] = $redirect_target;
		$_SESSION['redirect_prev_url']   = get_url(true);
		
		header('Location: ' . $redirect_target, true, 301);
		return;
	}
	
	$_SESSION['redirect_happened']   = false;
	$_SESSION['redirect_target_url'] = null;
	$_SESSION['redirect_prev_url']   = null;
</code></pre>
<pre><code class="php">		
	// doplneni lomitka
	// lze vyresit komplexeni : ale pro ukazku staci
	if(!empty($_path) &amp;&amp; substr($_path, -1) != '/'){
		if((strpos($_SERVER['REQUEST_URI'], '?') === false) &amp;&amp; (strpos($_SERVER['REQUEST_URI'], '&amp;') === false)){
			
			// u nekterych url treba lomitko nechceme
			if($_path != 'sitemap.xml'){
				header('Location: ' . get_url() . '/', false);
				return;
			}
		}
	}
	
	return $redirects;
}

$_redirects = redirect_check($_path, $_root_url, $sqli);</code></pre>
<h2>Ošetříme zacyklení</h2>
<blockquote>
<p>Funkce běží a zjistí, jestli má proběhnout redirect. V tu chvíli se zeptá: neproběhl už naposled nějaký?</p>
</blockquote>
<p>Nastavíme do _SESSION potřebné hodnoty a zabijeme skript. </p>
<p>Pokud podmínka splněna není, tedy je vše v pořádku, proměnné v _SESSION zase vynulujeme. Protože kdybychom to neudělali, hodnoty zůstanou uložené a bude se to cyklit i v případě, že chybné přesměrování v administraci opravíme.</p>
<p>A protože vynulování proměnných nastane jen tehdy, je-li vše v naprostém pořádku, máme automaticky ošetřené i víceúrovňové zacyklení: tedy když odkaz 1 směruje na odkaz 2, ten na odkaz 3 a ten zase zpátky na 1. Jednoduše se testuje, jestli jeden redirect nesměřuje na jiný. Je jedno, jestli to po dvou / třech přesměrováních skončí na normální adrese nebo ve smyčce. Nechceme ani jedno. </p>
<p>Na řádek s exitem samozřejmě doporučuji nějaké pokročilé odchytávání výjimek: např. logovací soubor nebo odeslání mailu.</p>
<p>Závěrem ještě doplníme lomítko, protože je s tím méně ladění, než to řešit v .htaccessu. </p>
<h3>Kód #5: Testovací data</h3>
<pre><code class="sql">INSERT INTO `project_redirects` (`id`, `original_url`, `redirect_url`, `exact_match`, `do_replace`, `note`, `active`, `timestamp`) VALUES
(1, 'test-url-1/', 'test-url-2/', 1, 0, 'test', 1, '2020-02-02 13:37:33'),
(2, 'test-url-2/', 'test-url-1/', 1, 0, 'test zacykleni', 1, '2020-02-02 13:37:33'),
(3, 'kategorie/', 'nova-kategorie/', 0, 0, 'vse na jednu jedinou url', 1, '2020-02-02 13:37:33'),
(4, 'kategorie-x/', 'nova-kategorie-x/', 0, 1, 'vcetne deti', 1, '2020-02-02 13:37:33'),
(5, 'nesmysl/', '/', 1, 0, 'test redirectu na root', 1, '2020-02-02 13:37:33');</code></pre>
<p>A na úplný závěr ještě jeden tip: při testování redirectů doporučuji vždy smazat historii prohlížeče, protože si ty redirecty cachují. Což má jít vyřešit zasláním no-cache v headeru a exitem, nicméně to není bohužel stoprocentní. </p>
<p class="download-links"><a href="https://mike.treba.cz/doc/2020/redirects/index.php" target="_blank">index.php</a><br /><a href="https://mike.treba.cz/doc/2020/redirects/htaccess.txt" target="_blank">.htaccess</a><br /><a href="https://mike.treba.cz/doc/2020/redirects/redirects.sql" target="_blank">redirect.sql</a></p>
<p class="download-links"><a href="https://mike.treba.cz/doc/2020/redirects/redirects.zip" target="_blank">ZIP archív ke stažení</a></p>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2020/web/abstract-business-code-coder-270348.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>PHP Třída na detekci zařízení</title>
			<link>https://mike.treba.cz/php-trida-na-detekci-zarizeni/</link>
			<pubDate>Wed, 27 Apr 2016 21:49:25 +1100</pubDate> 
			<comments>https://mike.treba.cz/php-trida-na-detekci-zarizeni/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>V sérii předchozích článků na téma YouTube videa jsem několikrát referoval na správnou detekci zařízení. Nic totiž nelze cílit se stoprocentní přesností jenom pomocí @media query, proto je dobré nasadit na responzivní weby i silnější kalibr. Tím samozřejmě myslím PHP knihovnu, která správně určí vstupní zařízení, na kterém se momentálně uživatel nachází. A jedno takové řešení vám dnes představím.</p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/php-trida-na-detekci-zarizeni/</guid>
			<content:encoded><![CDATA[<p>V sérii předchozích článků na téma YouTube videa jsem několikrát referoval na správnou detekci zařízení. Nic totiž nelze cílit se stoprocentní přesností jenom pomocí @media query, proto je dobré nasadit na responzivní weby i silnější kalibr. Tím samozřejmě myslím PHP knihovnu, která správně určí vstupní zařízení, na kterém se momentálně uživatel nachází. A jedno takové řešení vám dnes představím.</p> <p>Než jsem našel tu správnou knihovnu, strávil jsem nějaký čas na Googlu. Nakonec se ukázalo, že žádná jiná pořádně ani neexistuje. Tedy pokud chceme kvalitní produkt, jehož vývoj stále pokračuje. Autor knihovnu průběžně aktualizuje, což je v případě detekce zařízení nejdůležitější aspekt. Dovolte mi, abych vám představil Mobile Detect Library.</p>
<blockquote>
<p><strong>Mobile Detect Library</strong><br /><a href="http://mobiledetect.net" target="_blank">mobiledetect.net</a></p>
</blockquote>
<p>Nasazení si můžete nastudovat na uvedeném odkazu, pro snazší porozumění uvedu příklad i zde.</p>
<pre><code class="php">include_once('libs/detect/detect.php');
if (Detect::isTablet()) {
	$device = 'tablet';
} elseif (Detect::isMobile()) {
	$device = 'mobile';
} else {
	$device = 'desktop';
}</code></pre>
<pre><code class="html">&lt;body class="&lt;?=$device;?&gt;"&gt;
</code></pre>
<p>Je to opravdu takhle jednoduché. Jenom na jeden malý zádrhel si musíme dávat pozor: metoda Detect::isMobile vrací true i v případě tabletu, proto je nutné dodržet dané pořadí, chceme-li tato dvě zařízení odlišit. Je to vlastně uvedené i na hlavní stránce projektu.</p>
<p>Pak už stačí jen vytisknout proměnnou do třídy &lt;html&gt; nebo &lt;body&gt; a ve stylech můžeme krásně cílit zařízení. Stejně tak v JavaScriptu, kde stačí otestovat element na přítomnost třídy "mobile" respektive "tablet".</p>
<pre><code class="php">if ($('body.mobile').length) {
	// tento blok proběhne pouze na telefonu
}</code></pre>
<p>Třída funguje krásně, nedostatkem může být nejpspíš jen špatná rychlost jednotlivých metod. Pořád je to regulár za regulárem, což chvíli trvá a urychlit to příliš nelze. U mě na webu detekce vezme nějakých 0,02 sekundy, což se nemusí zdát až tak moc, ale pro srovnání - stejně dlouho mi trvá hlavní SQL dotaz na články, ve kterém spojuji víc jak 4 tabulky dohromady...</p>
<p>Každopádně nic lepšího asi neexistuje, a tak si musíme vystačit s tím, co máme zadarmo. A bez práce. Mně detekce slouží dobře: k čemu přesně ji využívám, si řekneme v dalším dílu seriálu "<em>Co jsem dnes udělal pro svůj web</em>".</p>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2023/renewed/devices.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>Návštěvní kniha v PHP s reakcí na příspěvky</title>
			<link>https://mike.treba.cz/navstevni-kniha-v-php-s-reakci-na-prispevky/</link>
			<pubDate>Thu, 14 Jan 2016 13:58:39 +1100</pubDate> 
			<comments>https://mike.treba.cz/navstevni-kniha-v-php-s-reakci-na-prispevky/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>Také v novém roce bych rád pokračoval s články v oboru. Mám tu ještě nějaké resty u starších textů, které potřebují revizi. Návštěvní kniha v PHP a MySQL s reakcí na příspěvky byla velmi navštěvovaným článkem, a tak je více než vhodné ji malinko předělat. Začaly se mi tu množit dotazy, proč to nyní nefunguje... </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/navstevni-kniha-v-php-s-reakci-na-prispevky/</guid>
			<content:encoded><![CDATA[<p>Také v novém roce bych rád pokračoval s články v oboru. Mám tu ještě nějaké resty u starších textů, které potřebují revizi. Návštěvní kniha v PHP a MySQL s reakcí na příspěvky byla velmi navštěvovaným článkem, a tak je více než vhodné ji malinko předělat. Začaly se mi tu množit dotazy, proč to nyní nefunguje... </p> <p>Knížku jsem totiž psal v době, kdy PHP5 rozhodně nebylo samozřejmostí. Bylo potřeba dodržovat nějakou zpětnou kompatibilitu, která už je ale v tuto chvíli bezpředmětná. Celou miniaplikaci jsem tedy aktualizoval a stejně jako naposledy si i dnes ukážeme, jak ji rozběhnout na vlastním webu. Také si projdeme některé nové funkce a vychytávky, které jsem zachoval, přidal či zrušil.</p>
<blockquote>
<p>Prosím berte v potaz, že původní článek je z roku <strong>2007</strong>. Hodně se za tu dobu změnilo, a tak návod nemusí odpovídat dnešním standardům. Ukázka ale funguje na <strong>PHP5</strong> a můžete ji využít jako první krok k lepšímu porozumnění PHP.</p>
</blockquote>
<h2>1. Co všechno návštěvní kniha umí</h2>
<p>Než si ukážeme, jak knížku nainstalovat, projdeme si, co všechno vlastně umí. Aplikace se skládá ze dvou skriptů (<em>frontend a backend</em>), instalačního souboru, configu, souboru s funkcemi a tří knihoven. Části kódu jsem se snažil okomentovat tak, aby bylo jasné, k čemu přesně slouží. Co vás tedy zajímá, jsou souboru "<strong>guestbook.php</strong>", "<strong>guestbook-admin.php</strong>", "<strong>config.php</strong>" a případně i "<strong>functions.php</strong>".</p>
<p><strong>Funkce:</strong></p>
<ul>
<li>jednoúrovňová reakce na příspěvky: žádná složitá strukturovaná diskuse, pouze první úroveň odpovědí</li>
<li>základní formátování písma, smajlíci</li>
<li>několik vrstev antispamu</li>
<li>administrace</li>
<li>notifikace na e-mail</li>
<li>ošetření dlouhých řetězců</li>
</ul>
<h2>2. Přístup do databáze</h2>
<p>Tímto začneme. Po rozbalení zipky, ještě před samotnou instalací musíme nastavit přístupové údaje do databáze. Otevřeme soubor "<strong>config.php</strong>" a hned na začátku upravíme:</p>
<pre><code class="php">$config['db']['host'] = 'localhost';	// db server
$config['db']['user'] = 'root';		// uzivatel
$config['db']['pass'] = '';		// heslo
$config['db']['name'] = 'mysql';	// jmeno databaze</code></pre>
<h2>3. Instalace: vytvoření tabulky v databázi</h2>
<p>Tabulku můžeme vytvořit buď manuálně nebo spuštěním skriptu "<strong>guestbook-install.php</strong>". Na první pohled se možná zarazíte, proč se datum ukládá do varcharu namísto jiného, přijatelnějšího formátu: je to pouze kvůli zpětné kompatibilitě. Tak, aby to stále fungovalo, když si vezmete čistě skripty a přepíšete ty původní.</p>
<pre><code class="sql">CREATE TABLE `knizka_2` (
	`id` int(10) unsigned NOT NULL   auto_increment,
	`jmeno` varchar(255),
	`text` text,
	`datum` varchar(255),
	`email` varchar(255),
	`web` varchar(255),
	`addr` varchar(255),
	`r` int(11) DEFAULT '0',
	PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;</code></pre>
<h2>4. Nastavení</h2>
<p>Tabulka byla úspěšně vytvořena, nyní se můžeme pustit do rozšířeného nastavení guestbooku a podívat se, co všechno umí. Vrátíme se do souboru "<strong>config.php</strong>", kde je uvedeno další nastavení.</p>
<pre><code class="php">// vase IP adresa, pro odliseni komentaru (jmeno bude jinou barvou)
$config['guestbook']['myip'] = '127.0.0.1'; 

// po kolika budeme strankovat (reakce nejsou zapocitany)
$config['guestbook']['per_page'] = 10; 

// po kolika budeme strankovat v administraci
$config['guestbook']['per_page_admin'] = 20; 

// heslo do administrace - NEZAPOMENTE ZMENIT
$config['guestbook']['admin_password'] = '123'; 

// IP adresy, z nichz nebude povoleno zanechavat vzkazy
$config['guestbook']['blacklist'] = array(
    '127.0.0.2',
    '127.0.0.3',
);

// dalsi ochrana proti spamu - pokud bude v poli zprava nektere ze zadanych slov, 
// prispevek se neodesle. (piste malymi pismeny)
$config['guestbook']['spamwords'] = 'anatrim website [url] [/url]';</code></pre>
<p>Takto jsou jednotlivé direktivy okomentovány přímo v souboru. Pokud máte pevnou IP adresu, můžete odlišit vlastní příspěvky jinou barvou. Následuje limit stránkování pro frontend i administraci spolu s heslem do administrace. Poslední 2 proměnné jsou pro vlastní ochranu knížky: můžeme zabanovat otravné spammery a definovat seznam slov, které zamezí uložení příspěvku.</p>
<p><strong>Notifikace na e-mail</strong></p>
<pre><code class="php">// budou chodit notifikace na email
$config['notif']['enable'] = true;

// kam budou chodit notifikace, mozno vice oddeleno strednikem
$config['notif']['to'] = 'vas@email.cz';

// odesilatel
$config['notif']['from_name'] = 'Guestbook - nový komentář';

// vzor predmetu
$config['notif']['subject'] = 'Nový komentář od %s';

// vzor pro telo emailu
$config['notif']['body'] = "
	Od: %s
	
	Zpráva: %s 
	
	Odkaz: %s
";</code></pre>
<p>Nová verze knížky vám nyní pošle e-mail s každou novou zprávou. Nastavení je poměrně intuitivní, jediné, co stačí nastavit, je vaše e-mailová adresa. Pokud notifikace nechcete, stačí řádky zakomentovat. Nebo nastavit první proměnnou na <strong>false</strong>. Nebo prázdný řetězec do adresy. Testování probíhá přes konstrukt !<strong>empty</strong>, takže blok kódu se pak prostě přeskočí. Pro posílání e-mailu používám vlastní jednoduchou knihovnu, její náhradu za PHPMailera by měl zvládnout každý začátečník.</p>
<h3>5. Změny oproti předchozí verzi</h3>
<p>Celou knížku jsem dost překopal. Tabulka ale zůstala nezměněna, takže když starý guestbook smažete a nahradíte novým, o uloženou diskusi nepřijdete. Jak už jsem zmiňoval na začátku, hlavní důvod k nové verzi byla zpětná kompatibilita, která v nových verzích PHP už vyhazuje chyby. Co se tedy změnilo dále?</p>
<ul>
<li>Třídy ve stylech jsou trochu rozumněji pojmenované, aby to případně nic nepřebíjelo.</li>
<li>Nasazena <a href="objekty-v-php5-navrh-databazove-tridy-dil-i-uvod-cile/">databázová třída</a> a <a href="pokrocile-strankovani-php/">stránkovací knihovna</a> - přesně ty, o kterých už jsem psal. Odkazy na články najdete znovu níže jako "<strong>Související</strong>". Až totiž soudruzi z NDR jednoho dne zakážou mysql_ funkce, změna bude pohodlnější.</li>
<li>Změnilo se ukládání data, nyní už ve formátu datetime: 2016-01-12. Kvůli zpětné kompatibilitě se ale stále ukládá do sloupce varchar. Každopádně není problém sloupec přetypovat.</li>
<li>Diskuse už nepřevádí odkazy, protože toto chování považuji za nevyžádané. Stejně tak už se nezobrazuje email - ten uvidíme pouze v administraci.</li>
<li>Chybové hlášky jsou méně "nasí*ací"</li>
<li>Systém reakcí je trochu upraven: už nesměruje na novou url, při reakci na reakci se akorát do příspěvku vloží jméno.</li>
<li>JavaScriptové funkce jsou psány na frameworku jQuery.</li>
<li>Formátovací značky už se vkládají na pozici vybraného textu (není-li vybraný, pak nakonec).</li>
</ul>
<h3>6. Ukázka a link ke stažení</h3>
<p>Ukázka v tuto chvíli není k dispozici. Skript je psaný pro PHP5 a na webu mám nasazené PHP7. Ale ZIPku si stáhnout můžete. </p>
<p class="download-links">ZIP archív ke <strong>stažení</strong>: <a href="https://mike.treba.cz/doc/2016/gb/guestbook.zip" target="_blank">guestbook.zip</a></p>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2016/www/guestbook_new.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>Formuláře v PHP - ošetření odesílaných dat: díl II</title>
			<link>https://mike.treba.cz/formulare-v-php-osetreni-odesilanych-dat/</link>
			<pubDate>Wed, 4 Feb 2015 21:14:30 +1100</pubDate> 
			<comments>https://mike.treba.cz/formulare-v-php-osetreni-odesilanych-dat/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>Další z článků, který po letech potřebuje oprášit jsou Formuláře v PHP - ošetření odesílaných dat. Byť samotné kontroly zůstávají pořád stejné, způsob jejich realizace se po letech vyvinul. Dříve jsme zobrazovali chybová hlášení všechna po kupě někde nad formulářem, pomocí JavaScriptového alertu nebo v případě začátečnické realizace samostatně na externí stránce. </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/formulare-v-php-osetreni-odesilanych-dat/</guid>
			<content:encoded><![CDATA[<p>Další z článků, který po letech potřebuje oprášit jsou Formuláře v PHP - ošetření odesílaných dat. Byť samotné kontroly zůstávají pořád stejné, způsob jejich realizace se po letech vyvinul. Dříve jsme zobrazovali chybová hlášení všechna po kupě někde nad formulářem, pomocí JavaScriptového alertu nebo v případě začátečnické realizace samostatně na externí stránce. </p> <p>Dnes uživatelé vyžadují větší pohodlí a intuitivnější chování webových aplikací, a tak zobrazujeme chybová hlášení většinou hned vedle daného políčka. Jak jsem už ale zmínil, princip kontrol není třeba měnit. Článek "<a href="je-cas-udelat-wordpressu-papa/">Je čas udělat Wordpressu pápá</a>" se datuje na 1. 10. 2007, tehdy jsem přešel na vlastní redakční systém, kde používám úplně stejné kontroly v komentářích. A od té doby jsem nezaznamenal jediný spam - tedy spam od neživého návštěvníka. Základní prvky ochrany tedy fungují stále dobře a je na čase si je připomenout.</p>
<blockquote>
<p>Prosím berte v potaz, že původní článek je z roku <strong>2007</strong>. Hodně se za tu dobu změnilo, a tak návod nemusí odpovídat dnešním standardům. Ukázka ale funguje <span>na </span><strong>PHP5</strong> a můžete ji využít jako první krok k lepšímu porozumnění PHP.</p>
</blockquote>
<h2>Ukázkový formulář</h2>
<pre><code class="html">&lt;? if(!empty($_GET['sent'])){ ?&gt;
	&lt;p class="success"&gt;Formulář byl úspěšně odeslán&lt;/p&gt;
&lt;? } ?&gt;

&lt;form action="&lt;?=$_SERVER['REQUEST_URI'];?&gt;" method="post"&gt;
	&lt;fieldset&gt;
		&lt;legend&gt;Přidej komentář&lt;/legend&gt;
		
		&lt;label&gt;Jméno &lt;b&gt;*&lt;/b&gt;&lt;/label&gt;&lt;br /&gt;
		&lt;input type="text" name="jmeno" value="&lt;?=(isset($_jmeno) ? $_jmeno : '');?&gt;" /&gt;&lt;br /&gt;
		&lt;?=(!empty($error_messages['jmeno']) ? '&lt;div class="error"&gt;'.$error_messages['jmeno'].'&lt;/div&gt;' : ''); ?&gt;
		
		&lt;label&gt;E-mail &lt;b&gt;*&lt;/b&gt;&lt;/label&gt;&lt;br /&gt;
		&lt;input type="text" name="web" value="&lt;?=(isset($_email) ? $_email : '');?&gt;" /&gt;&lt;br /&gt;
		&lt;?=(!empty($error_messages['email']) ? '&lt;div class="error"&gt;'.$error_messages['email'].'&lt;/div&gt;' : ''); ?&gt;
		
		&lt;label&gt;Web&lt;/label&gt;&lt;br /&gt;
		&lt;input type="text" name="email" value="&lt;?=(isset($_web) ? $_web : '');?&gt;" /&gt;&lt;br /&gt;
		&lt;?=(!empty($error_messages['web']) ? '&lt;div class="error"&gt;'.$error_messages['web'].'&lt;/div&gt;' : ''); ?&gt;
		
		&lt;label&gt;Vaše zpráva &lt;b&gt;*&lt;/b&gt;&lt;/label&gt;&lt;br /&gt;
		&lt;textarea name="text" rows="4" cols="10"&gt;&lt;?=(isset($_text) ? $_text : '');?&gt;&lt;/textarea&gt;&lt;br /&gt;
		&lt;?=(!empty($error_messages['text']) ? '&lt;div class="error"&gt;'.$error_messages['text'].'&lt;/div&gt;' : ''); ?&gt;
		
		&lt;span class="schovany"&gt;
			&lt;label&gt;Tohle políčko ponechte prázdné&lt;/label&gt;&lt;br /&gt;
			&lt;input type="text" name="city" /&gt;&lt;br /&gt;
			&lt;?=(!empty($error_messages['check1']) ? '&lt;div class="error"&gt;'.$error_messages['check1'].'&lt;/div&gt;' : ''); ?&gt;
		&lt;/span&gt;
		
		&lt;label&gt;Kontrola: Opište číslici pět &lt;b&gt;*&lt;/b&gt;&lt;/label&gt;&lt;br /&gt;
		&lt;input type="text" name="skype" /&gt;&lt;br /&gt;
		&lt;?=(!empty($error_messages['check2']) ? '&lt;div class="error"&gt;'.$error_messages['check2'].'&lt;/div&gt;' : ''); ?&gt;
		
		&lt;button name="submit" type="submit"&gt;Odeslat&lt;/button&gt;&lt;br /&gt;
	&lt;/fieldset&gt;
&lt;/form&gt;</code></pre>
<p>Takhle nějak by mohla vypadat základní kostra formuláře. Doporučuji samozřejmě i atributy FOR a ID pro labely a inputy (<em>popřípadě obalit input labelem</em>). Třídu "<em>schovany</em>" nastylujte na <em>display:none</em>. Pokud se na kód podíváte blíže, můžete si všimnout hned <strong>tří úrovní</strong> ochrany proti spamu, které za chvíli rozeberu. Další dvě úrovně ochrany pak budou schované už v obsluze odesílacího skriptu.</p>
<h2>Zpracování dat po odeslání, krok 1: Základní ošetření</h2>
<p>Formulář budeme posílat na stejnou url, na které je, takže zpracování odeslaných dat musí proběhnout ještě před zobrazením jakéhokoli HTML kódu. V případě jednoduchého formuláře, na kterém kontrolu předvádím můžeme začít například takto:</p>
<pre><code class="php">&lt;?php
if(isset($_POST['submit'])){
	$_jmeno = htmlspecialchars(trim($_POST['jmeno']));
	$_email = htmlspecialchars(trim($_POST['web']));
	$_web   = htmlspecialchars(trim($_POST['email']));
	$_text  = htmlspecialchars(trim($_POST['text']));
	
	$_check1 = $_POST['city'];
	$_check2 = htmlspecialchars(trim($_POST['skype']));</code></pre>
<p><strong>htmlspecialchars()</strong> a <strong>trim() </strong>je vše, co k ošetření vstupu potřebujete. Při ukládání do databáze je samozřejmě potřeba myslet na escape uvozovek, ale o databázi se v dnešním článku bavit nebudeme. U větších formulářů pak tato kontrola bude napsaná úplně jinak, ale pro začátek je tato ukázka víc než dostatečná. Ukončující závorku záměrně neuvádím, protože v bloku kódu budeme pokračovat.</p>
<h2>Zpracování dat po odeslání, krok 2: Antispam</h2>
<p>Na pořadí kroku 2 a 3 samozřejmě nezáleží, já zvolil jako první antispam. Jak nahoře ve formuláři, tak zde v prvních pár řádcích PHP jste si mohli všimnout, že jména pole <strong>e-mail</strong> a <strong>web </strong>jsou prohozená. To je první past na roboty. Pokud v poli email bude zavináč, vypíšeme chybu a formulář nezpracujeme. Dále zkontrolujeme odpověď na kontrolní otázku a hodnotu skrytého pole: pokud je vyplněno, opět se jedná o robota. Roboti totiž zpravidla vyplní každé pole formuláře nějakými bláboly - a my si proto řekneme, že právě jedno z polí musí být prázdné. Robot ho vyplní a skončil.</p>
<p>Skrytému poli je také dobré dát nějaký výstižný název, na který se roboti nachytají. V mém případě "city", můžete ale uvést i názvy "email_here" nebo cokoli jiného. I kontrolní pole samo o sobě má název, který láká na vyplnění všeho jiného jen ne jednomístného čísla.</p>
<p>Prohození názvů polí samozřejmě můžeme vynechat. V případě, kdy generujeme celý formulář z databáze a atribut name má hodnoty ve tvaru "input_25", "input_26" a podobně, budou nám bohatě stačit dvě antispamová pole pojmenovaná "email", "skype" či v podobném duchu.</p>
<p>Dále provedeme kontrolu zakázaných slov a počet hypertextových odkazů v těle zprávy. Zakázaná slova můžeme mít uložená v databázi nebo jenom staticky v proměnné, na tom nezáleží. Na čem ale záleží je uvedení těch správných frází, které se často vyskytují ve spamu. Pokud takové slovo vloží sám uživatel, dostane chybové hlášení, slovo smaže a příspěvek může odeslat. Robot ale bude ztracen.</p>
<pre><code class="php">	// antispam
	$evil_words = array('[url', 'viagra', 'website');
	foreach($evil_words as $word){
		if(strpos($_text, $word) !== false){
			$error_messages['text'] = 'Zpráva obsahuje některé ze zakázaných slov: '.implode(', ', $evil_words);
		}
	}
	
	if(substr_count($_text, 'http://') &gt; 5 ){
  		$error_messages['text'] = 'Více než pět hypertextových odkazů není povoleno.';
  	}
  	
  	if(strpos($_web, '@') !== false){
		$error_messages['web'] = 'Jste spam, prosím nebuďte spam.';
	}
	
	if(!empty($_check1)){
		$error_messages['check1'] = 'První kontrolní pole ponechte prázdné.';
	}
	
	if($_check2 != 5){
		$error_messages['check2'] = 'Chybně opsaný kontrolní kód.';
	}</code></pre>
<h2>Zpracování dat po odeslání, krok 2: Kontrola uživatelských dat</h2>
<p>Po prvním bloku kontrol následuje druhý, kde už jenom zkontrolujeme, zda-li jsou vyplněna všechna pole a jestli mají správný formát. Abychom ošetřili samotné tělo zprávy proti dlouhým řetězcům bez mezer, přidáme poslední blok kódu, které rozdělí slova delší než 50 znaků. Pokud je vše v pořádku (<em>proměnná $error_messages nebyla ani jednou naplněna</em>), můžeme provést uložení formuláře a <strong>redirect</strong>.</p>
<pre><code class="php">	// kontrola uzivatelskych dat
	if(empty($_jmeno)){
		$error_messages['jmeno'] = 'Pole Jméno je povinné.';
	}
	
	if(!preg_match('/^[a-zA-Z0-9_.&plus;-]&plus;@[a-zA-Z0-9-]&plus;\.[a-zA-Z0-9-.]&plus;$/', $_email)){
		$error_messages['email'] = 'Pole E-mail má chybný formát.';
	}

	if(empty($_email)){
		$error_messages['email'] = 'Pole E-mail je povinné.';
	}
	
	if(empty($_text)){
		$error_messages['text'] = 'Pole Vaše zpráva je povinné.';
	}else{
		$text_array  = explode(' ', $_text);
		$text_count  = count($text_array);
		$text_return = NULL;
		
		for($i = 0; $i &lt;= $text_count-1; $i&plus;&plus;){
		    $text_array[$i] = wordwrap($text_array[$i], 50, ' ', 1); 
		    $text_return .= $text_array[$i].' '; 
		}
		
		$_text = $text_return;
	}
	
	// uložení dat do DB, odeslání na email &plus; přesměrování
	if(empty($error_messages)){
		$current_url = $_SERVER['REQUEST_URI'];
		$current_url.= (strpos($current_url, '?') === false ? '?sent=1' : '&amp;sent=1');
		
		header('Location: '.$current_url);
		echo '&lt;meta http-equiv="refresh" content="1;url='.$current_url.'" /&gt;';
		exit;
	}
} // konec if(isset($_POST['submit']))</code></pre>
<h2>Vypsání chyb zpět do formuláře</h2>
<p>Jak jste si mohli všimnout v ukázce výchozího HTML, formulář už je na chystaný na zpětný výpis chyb. Pokud bude naplněn některý z klíčů proměnné k tomu určené, na daném místě se vypíše. Pokud bude u některého pole zaznamenáno více chyb, vypíše se vždy jen poslední zaznamenaná: není nutné vypisovat všechny, uživatel formulář odešle a dostane nové chybová hlášení. Když například zapomenu vyplnit e-mail, stačí napsat, že e-mail není vyplněn. Nemusím ještě číst další řádek o tom, že pole neodpovídá regulárnímu výrazu.</p>
<h2>Bonus: GEO IP ochrana</h2>
<p>Ochrana před přístupy z některých vyjmenovaných zemí je sice věc, která by měla být řešená na úrovni PHP (<em>abychom mohli návštěvníkovi vypsat alespoň hlášení, že je ve špatné lokalitě a celé to mohlo být administrovatelné</em>), ovšem pro malé weby cílené na menší region je tohle vcelku zbytečné. IP adresy některé exotických zemí bývají často využíváné spammery, takže zákazem přístupu z dané země můžeme velice jednoduše přidat další úroveň ochrany. Stačí do souboru <strong>.htaccess</strong> vložit následující řádky, popřípadě je rozšířit o další domény.</p>
<pre><code># soubor .htaccess
Deny from .ru # rusko
Deny from .ua # ukrajina
Deny from .cn # cina
Deny from .in # indie
Deny from .sa # saudska arabie</code></pre>
<h2>Ukázka</h2>
<p class="download-links"><a href="https://mike.treba.cz/doc/2015/form/form_example.php" target="_blank">Formulář</a><br /><a href="https://mike.treba.cz/doc/2015/form/form_example.html" target="_blank">Zdrojový kód</a></p>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2023/renewed/design-art.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>Objekty v PHP5 - Skládání objektů, abstrakce a dědičnost</title>
			<link>https://mike.treba.cz/objekty-v-php5-skladani-objektu-abstrakce-dedicnost/</link>
			<pubDate>Wed, 16 Jul 2014 22:11:16 +1100</pubDate> 
			<comments>https://mike.treba.cz/objekty-v-php5-skladani-objektu-abstrakce-dedicnost/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>V dnešním článku si povíme něco víc o objektech, a to konkrétně o<strong> skládání objektů</strong>, <strong>dědičnosti</strong>, <strong>abstrakci </strong>a samozřejmě i <strong>viditelnosti</strong>, jejíž znalost je k výše uvedeným technikám nezbytná. </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/objekty-v-php5-skladani-objektu-abstrakce-dedicnost/</guid>
			<content:encoded><![CDATA[<p>V dnešním článku si povíme něco víc o objektech, a to konkrétně o<strong> skládání objektů</strong>, <strong>dědičnosti</strong>, <strong>abstrakci </strong>a samozřejmě i <strong>viditelnosti</strong>, jejíž znalost je k výše uvedeným technikám nezbytná. </p> <p>Na první pohled se může zdát o složité termíny, ale jejich samotné použití nijak náročné není. Mnohem složitější je <em>správné použití</em>. Nepřidávat dědičnost jen proto, abychom ukázali, že ji umíme, nenaplácat všechna klíčová slova do jedné třídy jenom aby tam byly. To je také hlavní důvod, proč píšu samostatný text, důvod, proč nemohu vše demonstrovat na své databázové třídě.</p>
<h2>Skládání objektů</h2>
<p>Objekty skládáme ve chvíli, kdy třída A uloží do členské proměnné instanci třídy B a do jejího konstruktoru<strong> předá sama sebe</strong>. Tímto způsobem to může jít pořád dál, takže v každé třídě aplikace budeme mít přístup k veřejným metodám všech ostatních tříd. Předání objektu neprobíhá přes kopii jako u proměnných, nýbrž přes <strong>referenci</strong>. Nevzniká X kopií databázové třídy, ale pouze jedna, a jakákoli změna nastavení se projeví ve všech referencích na daný objekt.</p>
<p>Tato definice ale zní strašně krkolomně, takže si ukažme příklad ze života.</p>
<p>Mějme třídu <strong>Environment</strong>, která vytváří základní prostředí aplikace. V členských proměnných má uložené instance některých tříd, které budou volány napříč celou aplikací. V našem příkladu to bude třída Db (<em>databáze</em>) a třída Debug (<em>s nějakým uživatelským výstupem pro ladění</em>). Třída Env pak vytvoří instanci samotného modulu, který už se bude starat o obsluhu konkrétní části webu. V ukázce je kontroler pro administraci. Ten vytvoří instanci další třídy, která už bude zpracovávat jednotlivé části administračního prostředí: kategorie, články, produkty, formuláře... Třída Db je prázdná, aby nám ukázka neházela fatal error, <strong>Debug </strong>obsahuje akorát metodu s funkcí <em>print_r()</em> pro ukázku rekurzivního předávání objektů.</p>
<blockquote>
<p>Takže pokaždé, kdy postoupíme hlouběji do aplikace, daná třída předá sama sebe dál.</p>
</blockquote>
<p>Je samozřejmě důležité, aby všechny parametry obsahující referenci na jiný objekt měly viditelnost <strong>public </strong>- pokud tedy nechceme přístup explicitně zakázat. Kostra by mohla vypadat například takto:</p>
<pre><code class="php">class Db { }

class Debug {

	public $env;
	
	public function __construct(Environment $env){
		$this-&gt;env = $env;
	}
	
	public function printDebug(){
		print_r($this);
	}
}

class Environment {

	public $db;
	public $debug;
	public $module;
	
	public function __construct(){
		$this-&gt;db = new Db();
		$this-&gt;debug = new Debug($this);
		$this-&gt;module = new MasterAdminController($this);
	}
}

class MasterAdminController {

	public $env;
	public $localModule;
	
	public function __construct(Environment $env){
		$this-&gt;env = $env;
		
		$this-&gt;localModule = new ArticleAdminController($this);
	}
}

class ArticleAdminController {

	public $parentModule;
	
	public function __construct($parent_controller){
		$this-&gt;parentModule = $parent_controller;
		
		// pristup k tride DB
		$db = $this-&gt;parentModule-&gt;env-&gt;db; // -&gt;metoda();
	}
}</code></pre>
<p>Nyní si ukážeme, co se stane, když zavoláme následující kód:</p>
<pre><code class="php">$env = new Environment;

echo "&lt;pre&gt;";
print_r($env);
echo "&lt;/pre&gt;";

echo "&lt;pre&gt;";
print_r($env-&gt;debug-&gt;printDebug());
echo "&lt;/pre&gt;";</code></pre>
<p>Oba bloky vypíšou skoro to stejné. Klíčové slovíčko *RECURSION* nám potvrdí, že objekty jsou vázány opravdu přes referenci.</p>
<pre><code>Environment Object
(
    [db] =&gt; Db Object
        (
        )

    [debug] =&gt; Debug Object
        (
            [env] =&gt; Environment Object
 *RECURSION*
        )

    [module] =&gt; MasterAdminController Object
        (
            [env] =&gt; Environment Object
 *RECURSION*
            [localModule] =&gt; ArticleAdminController Object
                (
                    [parentModule] =&gt; MasterAdminController Object
 *RECURSION*
                )
        )
)

Debug Object
(
    [env] =&gt; Environment Object
        (
            [db] =&gt; Db Object
                (
                )

            [debug] =&gt; Debug Object
 *RECURSION*
            [module] =&gt; MasterAdminController Object
                (
                    [env] =&gt; Environment Object
 *RECURSION*
                    [localModule] =&gt; ArticleAdminController Object
                        (
                            [parentModule] =&gt; MasterAdminController Object
 *RECURSION*
                        )
                )
        )
)</code></pre>
<h2>Dědičnost</h2>
<p>Abychom pochopili, jak správně využít dědičnost, budeme si muset vysvětlit některé pojmy a stejně tak si povědět o základním smyslu dědění tříd. Příklad bych opět demonstroval na kostře projektu, na které jsme si popsali skládání. Přeskočíme třídu prostředí a začneme rovnou moduly. Mějme tedy instanci modulu pro administraci, která pak načítá už konkrétní obsluhy jednotlivých funkčních celků.</p>
<p>Chceme-li spravovat například již zmíněné <strong>články</strong>, <strong>kategorie </strong>a <strong>produkty</strong>, jistá část skriptů bude vždy podobná až stejná. Jiné části se budou lišit, a to konkrétními bloky kódu, které v jiných modulech obsaženy nebudou. A tak si navrhneme jakousi centrální třídu, kterou dané moduly zdědí.</p>
<p>Tím vlastně docílíme stejné logiky každého skriptu. Rodičovská třída bude obsahovat metody a parametry společné pro každý modul, její potomci ji pak vždy rozšíří o metody sobě specifické. Tím nejen ušetříme spoustu řádků kódu, ale také donutíme sami sebe psát ten kód tak říkajíc "slušně". Budeme muset dodržovat předem stanovenou logiku.</p>
<p><strong>Druhý případ</strong> využití dědičnosti je už mnohem jednodušší. Máme knihovnu, do které potřebujeme přidat funkčnost. A jelikož knihovny nejsou určeny k tomu, aby se do nich hrabalo, založíme třídu novou, která knihovnu rozšíří. Můžeme vzít <a href="objekty-v-php5-navrh-databazove-tridy-dil-i-uvod-cile/">databázovou třídu</a> a přidáme funkce pro zasílání chyb na e-mail. Tento způsob se často využívá v situacích, kdy jsou knihovny umístěné v nějakém centrálním úložišti, my je známe, můžeme je <strong>includovat </strong>ale nemůžeme tyto soubory měnit.</p>
<p>Na následujícím příkladě si ukážeme stav první. Ale zpět k důležitým pojmům. Než se dostanu ke kódu, je potřeba si tyto pojmy vysvětlit.</p>
<h2>Viditelnost</h2>
<p>PHP nabízí tři úrovně viditelnosti: <strong>public</strong>, <strong>protected </strong>a <strong>private</strong>. Public (<em>veřejný</em>) znamená, že k metodám či atributům třídy můžeme přistupovat z venčí. Protected (<em>chráněný</em>) toto omezí pouze na potomky: k funkci nepůjde přistupovat z venčí, ale pokud třídu zdědíme, ke všemu se dostaneme. Často se říká, že pokud chceme metody respektive atributy využít jen interně, dáme všechno protected. Nejvyšší stupeň utajení, private (<em>soukromý</em>), pak omezí přístup pouze na danou třídu. I když ji zdědíme, k metodám se nedostaneme.</p>
<h2>Abstrakce</h2>
<p>Klíčové slovo <strong>abstract </strong>definuje, že třída musí být zděděná. Nelze vytvořit její instance, jakýkoli pokus selže chybou. Pakliže je třída definovaná jako abstraktní, může obsahovat i abstrakní metody. Abstraktní metoda má pouze definici, nemá vlastní tělo funkce. Tím řekneme, že potomek musí danou funkci obsahovat. Jaké bude její tělo už ale neurčujeme. Asi by bylo dobré v daném kontextu i vysvětlit rozdíl mezi <strong>rozhraním </strong>(interface) a <strong>abstraktní třídou.</strong></p>
<blockquote>
<p><strong>Rozhraní </strong>je šablona vzoru, osnova. <strong>Abstraktní třída</strong> už může být knihovna nebo modul, musí se ale "dokončit" jejím zděděním a dopsáním prázdných metod.</p>
</blockquote>
<h2>Klíčové slovo "final"</h2>
<p>Naopak klíčové slovíčko <strong>final </strong>jakoukoli dědičnost zakazuje. Třída už je kompletní a nechce být žádným způsobem rozšiřována.</p>
<pre><code class="php">abstract class AdminController {

	public $parentModule;
	
	protected $path = array();
	
	private $someParam;
	
	final public function __construct($parent_module = null){
		$this-&gt;parentModule = $parent_module;
	}
	
	public function loadData($param){
		echo 'load some data';
	}
		
	abstract public function __init();
}

class CategoryAdminController extends AdminController {
	public function __init(){
		// samotne spusteni
	}
	
	public function loadData($param = null){
		parent::loadData($param);
		echo ' and do something';
	}
	
	public function loadAnotherData($param){
		echo 'load another data';
	}
}

$cont = new CategoryAdminController();
$cont-&gt;loadData();</code></pre>
<p>Na výše uvedeném příkladu je ukázaná možnost redefinovat metody rodiče v potomkovi, což je přesně ten typ rozšíření, kvůli kterému vůbec chceme něco dědit. Ukázka vypíše přesně toto:</p>
<pre><code>load some data and do something</code></pre>
<p>Na závěr přikládám použitý kód ke stažení, i když si myslím, že zde to ani tolik nutné není.</p>
<p><strong>Odkaz</strong><span>:</span></p>
<p class="download-links"><a href="doc/2014/dedeni_skladani_sample.zip">dedeni_skladani_sample.zip</a></p>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2023/renewed/code-funny.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>Návrh databázové třídy - díl VI - Závěr</title>
			<link>https://mike.treba.cz/navrh-databazove-tridy-dil-vi-zaver/</link>
			<pubDate>Mon, 14 Jul 2014 20:31:52 +1100</pubDate> 
			<comments>https://mike.treba.cz/navrh-databazove-tridy-dil-vi-zaver/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>Šestým dílem bych rád uzavřel svůj seriál o databázové třídě a úvod do <strong>objektového programování v PHP5</strong>. Ukázali jsme si středně složitou třídu s obsluhou většiny typů SQL dotazů, které využijeme pro projekt průměrné náročnosti. </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/navrh-databazove-tridy-dil-vi-zaver/</guid>
			<content:encoded><![CDATA[<p>Šestým dílem bych rád uzavřel svůj seriál o databázové třídě a úvod do <strong>objektového programování v PHP5</strong>. Ukázali jsme si středně složitou třídu s obsluhou většiny typů SQL dotazů, které využijeme pro projekt průměrné náročnosti. </p> <p>Kromě naprosto základních funkcí jako ošetření SQL dotazů proti <strong>SQL injection</strong> umí i jednoduché logování, export databáze buď do souboru nebo přímo do force downloadu i vlastní obsluhu chyb. Posílání chyb na e-mail zde ale obsažené není: na notifikační e-maily mám třídu další, o které si můžeme povědět v jiném článku. Nesmím také vynechat možnost <strong>připojení k více databázím</strong> v rámci jednoho projektu: i to <strong>class Db</strong> zvládne. Tak hurá na rekapitulaci.</p>
<h2>Vytvoření instance</h2>
<p>Přihlašovací údaje k databázi nastavíme v globálních proměnných. Klíč proměnné <strong>config</strong>, ve kterém bude třída hledat potřebná data můžeme změnit přímo v parametru konstruktoru; tímto pak můžeme vytvořit připojení k více databázím.</p>
<pre><code class="php">$config['db']['host'] // db server
$config['db']['user'] // uzivatel
$config['db']['pass'] // heslo
$config['db']['name'] // jmeno databaze

$db = new Db;</code></pre>
<p>Nastavení ještě umožňuje zadání tří dalších nepovinných hodnot, které ale najdete až v komentářích ve zdrojové kódu.</p>
<h2>Základní metody pro SQL dotazy</h2>
<pre><code class="php">// asociativni pole z tabulky, zakladni (pokrocile nastaveni @227)
$query = $db-&gt;query("SELECT * FROM ?_tabulka WHERE id = '?' AND category_id = '?'", array($id, $category_id))-&gt;assocList();

// insert, vraci mysql_insert_id()
$insert_id = $db-&gt;insert('?_categories', array( 
    'name' =&gt; $_POST['name'], 
    'text' =&gt; $_POST['text'], 
));

// update, zakladni (pokrocile nastaveni @499)
$affected_rows = $db-&gt;update('?_categories', $_GET['id'], array( 
    'name' =&gt; $_POST['name'], 
    'text' =&gt; $_POST['text'], 
)); 
 
// delete, vraci opet mysql_affected_rows();
$deleted_rows = $db-&gt;delete('?_articles', array( 
    'parent_id' =&gt; 1, 
));</code></pre>
<h2>Debug</h2>
<p>Jak jsem zmínil v úvodu, knihovna má obsažený i pár debugovacích funkcí pro případné <strong>ladění rychlosti</strong> SQL dotazů. Umí vypsat počet provedených dotazů &plus; pole s jejich kompletním zněním i časem, jak dlouho který dotaz trval.</p>
<pre><code class="php">// debug
echo "&lt;pre&gt;";
echo "pocet provedenych dotazu: ".$db-&gt;numQueries();
print_r($db-&gt;getQueries());
echo "&lt;/pre&gt;";</code></pre>
<h2>Export databáze</h2>
<p>V neposlední řadě můžeme provést i export databáze. Buď celé nebo jen konkrétních tabulek či konkrétních částí daných tabulek. Tuto funkci nedoporučuji přidávat dynamicky do aplikace: sám ji používám vždy v podobě samostatného souboru kam kód i podmínky napíšu ručně, takže i vám doporučuji stejnou logiku použití.</p>
<pre><code class="php">// export databaze
$db = new Db; 
//$db-&gt;addExportCondition('shop_categories', ''); 
//$db-&gt;addExportCondition('shop_products', 'WHERE category_id IN (1,2,3)'); 
$db-&gt;dumpAndSave();
//$db-&gt;dumpAndDownload();</code></pre>
<p><strong>Odkazy</strong>:</p>
<p class="download-links">Zdrojové kód: <a href="doc/2014/class.Db2.html" target="_blank">class.Db2.phps</a><br />ZIP ke stažení: <a href="doc/2014/class.Db2.zip">class.Db2.zip</a></p>
<blockquote>
<p><em><em>Edit 4. 5. 2020: </em><br />Text se týká PHP 5. Pod PHP 7 už třída fungovat nebude, protože všechny mysql_ funkce skončí chybou.</em></p>
</blockquote>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2023/renewed/code.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>Návrh databázové třídy - díl V - Logování změn</title>
			<link>https://mike.treba.cz/php-oop-databazova-trida-dil-5-logovani/</link>
			<pubDate>Wed, 9 Jul 2014 10:01:53 +1100</pubDate> 
			<comments>https://mike.treba.cz/php-oop-databazova-trida-dil-5-logovani/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>V dalším pokračování seriálu o objektovém programování v PHP si ukážeme, jak přidat do ukázkové třídy DB logování konkrétních SQL dotazů. Rozebereme si tedy znovu metody na <strong>insert</strong>, <strong>update </strong>a <strong>delete</strong>, které rozšíříme o další zápis do nové tabulky, kam budeme ukládat informace o provedených změnách. </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/php-oop-databazova-trida-dil-5-logovani/</guid>
			<content:encoded><![CDATA[<p>V dalším pokračování seriálu o objektovém programování v PHP si ukážeme, jak přidat do ukázkové třídy DB logování konkrétních SQL dotazů. Rozebereme si tedy znovu metody na <strong>insert</strong>, <strong>update </strong>a <strong>delete</strong>, které rozšíříme o další zápis do nové tabulky, kam budeme ukládat informace o provedených změnách. </p> <p>Nejdřív ale krátká rekapitulace. Soubor článků o objektech v PHP jsem začal psát už poměrně dávno, ještě před pauzou v blogování. Jelikož jsem měl ale na články vcelku pozitivní ohlasy, byla by asi škoda seriál nedokončit, byť opožděně.</p>
<p><a href="objekty-v-php5-navrh-databazove-tridy-dil-i-uvod-cile/">V prvním dílu</a> jsme si stanovili cíle a řekli si něco o rozhraní nad třídou. <a href="navrh-databazove-tridy-dil-ii-zakladni-metody/">V dílu druhém</a> jsme si ukázali základní metody na dotazy typu <strong>select</strong>, <strong>insert</strong>, <strong>update </strong>a <strong>delete</strong>, které si dnes připomeneme a upravíme. <a href="navrh-databazove-tridy-dil-iii-class-query/">Třetí díl</a> byl vlastně o skládání objektů, a to ukázka třídy Query, jejíž instanci vracely metody mateřské třídy Db. <a href="navrh-databazove-tridy-dil-iv-export-databaze/">Poslední, čtvrtý díl</a> byl spíš takový návod jak jednoduše exportovat databázi bez přístupu do PHPMyAdmina; hodně kódu a méně povídání.</p>
<p>Dnešní článek bude podobně stavěný jako ten předchozí. Ukážeme si, jak logovat SQL dotazy, povíme si proč spolu s vysvětlením, u kterých metod má rozšíření o logování smysl a u kterých ne. V seriálu budou následovat <strong>ještě 2 další články</strong>: slovo závěrem s odkazem na celý <strong>zdrojový kód ke stažení</strong> a navíc ještě jeden krátký text o dědičnosti. Té jsem se zatím nevěnoval a jelikož se jedná o poměrně důležitou kapitolu v objektově orientovaném programování, zaslouží si článek navíc.</p>
<h2>Princip logování a kdy má smysl</h2>
<p>Základní princip je poměrně triviální: u každého provedeného dotazu zkopírujeme celé jeho znění do další tabulky. Metoda insert poskládá <strong>SQL dotaz</strong> dle zadaných parametrů a provede ho vlastně na 2 tabulkách. Metody update a delete pak upraví respektive smažou záznam z databáze spolu se zavoláním dalšího insertu na pozadí do logovací tabulky. U žádných dalších metod pak logování samozřejmě nemá význam.</p>
<p>Samotné logování má smysl v jednom jediném případě, a to v <strong>administraci </strong>či na jiných místech aplikace, kde mají <strong>uživatelé </strong>přístup k úpravě dat. Ve výsledku totiž dostaneme takovou jednoduchou historii provedených změn. Chtěl bych zdůraznit, že v žádném případě nepůjde o nějaké revize článků či kategorií, to je samo o sobě neskutečně komplexní funkcionalita, kterou rozhodně neobslouží pár metod v databázové třídě.</p>
<h2>Využití v praxi</h2>
<p>Pokud si spravujeme vlastní malé stránky jako například nyní já a stane se nám nehoda, kdy si například omylem umažeme kus napsaného textu, v databázi pak snadno dohledáme předchozí provedený <strong>update</strong> ainformace zachráníme. Pakliže má do administrace přístup více uživatelů, můžeme velice snadno zjistit, kdo kde co rozbil :-) Tento případ se může na první pohled zdát poněkud úsměvný, ale z praxe vím, že k takovýmto situacím dochází dnes a denně... <strong>Jako jeden z příkladů</strong> mohu uvést editaci uživatelského profilu na e-shopu. Může se stát, že zákazník odešle objednávku, další den si v profilu upraví adresu a pak se bude hádat, že mu objednávka byla odeslána na adresu špatnou. A člověk je pak zbytečně za blbce. Bohužel, i tak to chodí. Takže je fajn mít tyto stavy podchycené.</p>
<h2>Struktura tabulek</h2>
<p>Dost bylo teorie, přejdeme k samotnému kódu. V databázi bude potřeba založit 2 nové tabulky.</p>
<pre><code class="sql">CREATE TABLE `project_log` (
  `id` bigint(20) unsigned  auto_increment,
  `user_id` int(10) default '0',
  `table_name` varchar(64) NOT NULL,
  `method` varchar(16) NOT NULL,
  `insert_id` bigint(20) DEFAULT NULL,
  `whole_query` longtext,
  `timestamp` timestamp  default CURRENT_TIMESTAMP,
  PRIMARY KEY  (`id`),
  KEY (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;

CREATE TABLE `project_log_items` (
  `id` bigint(20) unsigned  auto_increment,
  `parent_id` bigint(20) unsigned NOT NULL,
  `col_name` text,
  `col_value` text,
  PRIMARY KEY  (`id`),
  KEY (`parent_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; </code></pre>
<p>První bude obsahovat základní informace o provedeném dotazu: ID uživatele referující na interní tabulku v daném systému, jméno tabulky, kde úprava proběhla, název fukce, která logování zavolala, ID záznamu pro dotazy typu insert, celé znění SQL dotazu a informaci o čase. Do druhé tabulky s referencí na první pak budeme ukládat veškeré údaje sloupec po sloupci. Díky tomu pak budeme mít zpětný přístup k naprosto všem změnám <strong>včetně smazaných řádků</strong>. Opět bych ale zdůraznil, že se rozhodně <strong>nejdená </strong>o nějaký <strong>archív smazaných položek</strong> nebo <strong>koš</strong>; logovací tabulky jsou primárně určeny pro vkládání a nic jiného. Předpokládáme zde obrovský objem dat, takže jakýkoli výpis záznamů s případným filtrováním by nám docela zavařil dráty.</p>
<h3>Rozšíření třídy Db</h3>
<pre><code class="php">private $log = false;
private $user = 0;
private $logTable;
private $tableItems;</code></pre>
<p>Ve čtyřech nových proměnných s viditelností private bude uloženo vše potřebné pro logování: ID uživatele, jména tabulek a stav zapnuto / vypnuto.</p>
<pre><code class="php">public function enableLog($user = 0){
	$this-&gt;log = true;
	if($user){
		$this-&gt;user = $user;
	}
}

public function setLogTable($logTable = null){
	if($logTable){
		$this-&gt;logTable = $logTable;
	}
}

public function setLogItemsTable($tableItems = null){
	if($tableItems){
		$this-&gt;tableItems = $tableItems;
	}
}

public function disableLog(){
	$this-&gt;log = false;
}</code></pre>
<p>Další 4 nové metody typu <strong>set </strong>budou tyto proměnné nastavovat. Metoda <em>enableLog()</em> bude volána přímo s parametrem ID uživatele, jelikož předpokládáme její zavolání někde uvnitř administračního modulu po <a href="prihlasovani-a-registrace-uzivatelu-pomoci-sessions/">přihlášení</a>. Další dvě metody umožní přenastavení výchozích jmen tabulek a nakonec zbývá už jen poslední funkce pro vypnutí. Vypínač bude mít i velice důležitou interní funkci, k tomu ale až za chvíli.</p>
<h2>Metoda insert</h2>
<p>Základní znění metody insert jsme si ukázali hned v druhém dílu, nyní si ji rozšíříme.</p>
<pre><code class="php">public function insert($table, array $items){
	try {
		if(empty($items)){
			throw new DbException('No values to insert.', var_export($items, true));
		}
		
		$keys = array();
		$vals = array();
		
		foreach ($items as $key =&gt; $t){
			if($t === 0 || $t === '0'){
				$vals[$key] = "'0'";
				$keys[]     = "`{$key}`";
			} elseif(!empty($t)){				
				$vals[$key] = "'".$this-&gt;escape($t)."'";
				$keys[]     = "`{$key}`";
			}
		}

		$qtable  = $this-&gt;replacePrefix($table);
		$final_query     = "INSERT INTO {$qtable} ( ".implode("\n,", $keys)." ) VALUES ( ".implode(",\n ", $vals)." )";
		$final_query_log = "INSERT INTO {$qtable} ( ".implode(",",   $keys)." ) VALUES ( ".implode(", ",   $vals)." ) ;";

		$insertId = $this-&gt;query($final_query, array(), false)-&gt;insertId();
		
		if($this-&gt;log){
			$this-&gt;log = false;
			$this-&gt;insert($this-&gt;logTable, array(
				'user_id'     =&gt; $this-&gt;user, 
				'table_name'  =&gt; $this-&gt;replacePrefix($table), 
				'method'      =&gt; __FUNCTION__, 
				'insert_id'   =&gt; $insertId,
				'whole_query' =&gt; $final_query_log,
			));
			$this-&gt;log = true;
		}
		
		return $insertId;
	}catch(Exception $e){
		$e-&gt;show();
	}
}</code></pre>
<p>Úprava spočívá v bloku začínajícím podmínkou<em> if($this-&gt;log)</em>. Pokud je tedy logovaní aktivní, nejdříve ho vypneme, uložíme data a pak ho znovu zapneme. Proč to? Funkce totiž volá sama sebe, takže kdybychom tak neučinili, celé by se to zacyklilo a metoda by volala sama sebe pořád dokola do nekonečna. A logovat logování fakt nechceme. Ve funkci <strong>insert </strong>logujeme až na závěr, abychom už znali <em>mysql_insert_id </em>a mohli uložit referenci na právě vytvořený záznam.</p>
<h2>Metoda update</h2>
<p>Update bude o jednu úroveň náročnější, protože zde už využijeme i druhé tabulky. Tady naopak zálohujeme záznam v jeho podobě před úpravou. Nejdříve tedy proběhne select na řádek / řádky, jenž cílíme v podmínce updatu, získané údaje uložíme a nakonec až přepíšeme.</p>
<pre><code class="php">public function update($table, $id, array $items, $col = 'id'){
	try {
		if(empty($items)){
			throw new DbException('No values to update.', var_export($items, true));
		}
		if(empty($id)){
			throw new DbException('No id.', var_export($id, true));
		}
		
		$update = array();
		foreach ($items as $key =&gt; $item){
			$update[$key] = "`{$key}` = '".$this-&gt;escape($item)."'";
		}
		
		$qtable = $this-&gt;replacePrefix($table);
		$final_query     = "UPDATE {$qtable} SET ".implode(",\n ", $update)." WHERE `{$col}` = '{$id}'";
		$final_query_log = "UPDATE {$qtable} SET ".implode(" ",    $update)." WHERE `{$col}` = '{$id}' ;";
		
		if($this-&gt;log){
			$this-&gt;log = false;	
			$_entry = $this-&gt;query("SELECT * FROM {$table} WHERE `{$col}` = '{$id}'")-&gt;assocList();
			foreach ($_entry as $_row){
				$_log_id = $this-&gt;insert($this-&gt;logTable, array(
					'user_id'     =&gt; $this-&gt;user, 
					'table_name'  =&gt; $this-&gt;replacePrefix($table), 
					'method'      =&gt; __FUNCTION__,
					'whole_query' =&gt; $final_query_log,
				));
				foreach ($_row as $_key =&gt; $_col){
					$this-&gt;insert($this-&gt;tableItems, array(
						'parent_id' =&gt; $_log_id, 
						'col_name'  =&gt; $_key, 
						'col_value' =&gt; $_col
					));
				}
			}
			$this-&gt;log = true;
		}
		
		return $this-&gt;query($final_query, array(), false)-&gt;affectedRows();
	}catch(Exception $e){
		$e-&gt;show();
	}
}</code></pre>
<p>Logování opět nejdříve vypneme před zavoláním patřičných insertů.</p>
<h2>Metoda delete</h2>
<p>Jak jsem naznačil výše, i při mazání řádků z databáze vlastně provedeme nejdřív jejich zálohu. Chování bude úplně stejné jako u metody insert.</p>
<pre><code class="php">public function delete($table, array $cond){
	try {
		if(empty($cond)){
			throw new DbException('No condition.', var_export($cond, true));
		}
		
		$c = array();
		foreach ($cond as $key =&gt; $t){
			$c[] = "`{$key}` = '".$this-&gt;escape($t)."'";
		}
		
		$qtable = $this-&gt;replacePrefix($table);
		$string = implode(' AND ', $c);
		$final_query     = "DELETE FROM {$qtable} WHERE {$string}";
		$final_query_log = "DELETE FROM {$qtable} WHERE {$string} ;";
		
		if($this-&gt;log){
			$this-&gt;log = false;
			$_entry = $this-&gt;query("SELECT * FROM {$table} WHERE {$string}")-&gt;assocList();
			foreach ($_entry as $_row){
				$_log_id = $this-&gt;insert($this-&gt;logTable, array(
					'user_id'    =&gt; $this-&gt;user, 
					'table_name' =&gt; $this-&gt;replacePrefix($table), 
					'method'     =&gt; __FUNCTION__,
					'whole_query' =&gt; $final_query,
				));
				foreach ($_row as $_key =&gt; $_col){
					$this-&gt;insert($this-&gt;tableItems, array(
						'parent_id' =&gt; $_log_id, 
						'col_name'  =&gt; $_key, 
						'col_value' =&gt; $_col
					));
				}
			}
			$this-&gt;log = true;
		}

		return $this-&gt;query($final_query, array(), false)-&gt;affectedRows();
	}catch(Exception $e){
		$e-&gt;show();
	}
}</code></pre>
<h2>Závěrem</h2>
<p>To by bylo v kostce asi vše. V příštím dílu bych seriál o databázové třídě uzavřel, abych mohl navázat slíbeným článkem o dědičnosti.</p>
<blockquote>
<p><em><em>Edit 4. 5. 2020: </em></em><br />Text se týká PHP 5. Pod PHP 7 už třída fungovat nebude, protože všechny mysql_ funkce skončí chybou.</p>
</blockquote>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2023/renewed/girl-programmer.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>Návrh databázové třídy - díl IV: Export databáze</title>
			<link>https://mike.treba.cz/navrh-databazove-tridy-dil-iv-export-databaze/</link>
			<pubDate>Sat, 3 Jul 2010 21:50:19 +1100</pubDate> 
			<comments>https://mike.treba.cz/navrh-databazove-tridy-dil-iv-export-databaze/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>Dnešní pokračování pohádky o databázové třídě si vezme pod lupu pár konkrétních metod. Půjde o funkce na export celé databáze s možností <strong>force download </strong>nebo postupného ukládání do souboru. O možnostech exportu databáze moc článků napsáno nebylo, navíc se nejedná o úplně triviální algoritmus s pár vnořenými cykly. V následujícím článku vám předvedu, jak problém vyřešit s elegancí.</p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/navrh-databazove-tridy-dil-iv-export-databaze/</guid>
			<content:encoded><![CDATA[<p>Dnešní pokračování pohádky o databázové třídě si vezme pod lupu pár konkrétních metod. Půjde o funkce na export celé databáze s možností <strong>force download </strong>nebo postupného ukládání do souboru. O možnostech exportu databáze moc článků napsáno nebylo, navíc se nejedná o úplně triviální algoritmus s pár vnořenými cykly. V následujícím článku vám předvedu, jak problém vyřešit s elegancí.</p> <p>Kostra řešení bude poměrně jednoduchá:</p>
<pre><code>SHOW TABLES

foreach ($tables) {
	SHOW CREATE TABLE
	SELECT * FROM $table
	while (FETCH_ROW) {
		foreach ($row) {
			mysql_real_escape_string($row);					
		}
		$export .= "INSERT INTO $table VALUES implode(, $row)"
	}
}
</code></pre>
<p>Nejdříve zjistíme, jaké tabulky exportujeme, následně od každé z nich získáme strukturu přes <strong>SHOW CREATE TABLE</strong>, vybereme hvězdičku, provedeme escapování a uložíme znění <strong>SQL</strong> dotazu na insert.</p>
<p>Metody na export databáze budou 2. Budou si podobné, protože v rámci zachování čitelnosti kódu není dobry nápad cpát do už tak velké funkce hromadu podmínek. První metoda uloží celý export do proměnné a nabídne soubor k uložení, druhá bude data postupně ukládat do souboru, který si pak můžeme stáhnout přes FTP klienta.</p>
<p>Před uvedením výsledné funkce si ale nejprve ukážeme pár podpůrných metod, jenž se nám budou hodit. O prováděném exportu chceme samozřejmě mít nějaké zběžné informace, které nám poskytnou následující funkce.</p>
<pre><code class="php">private function prepareBackupInfo(){
	$this-&gt;backupInfo['date_pattern'] =  @date('Y-m-d_H.i.s');
	$this-&gt;backupInfo['timestamp'] = strtr($this-&gt;backupInfo['date_pattern'], array('_' =&gt; ' ', '.' =&gt; ':'));
	$this-&gt;backupInfo['start_time'] = $this-&gt;startTime();
	$this-&gt;backupInfo['queries'] = 0;
	$this-&gt;backupInfo['written'] = 0;
}

private function appendBackupInfo(){
	$output = "";
	$output .= "\n";
	$output .= "--\n";
	$output .= "-- Queries contained: ".$this-&gt;backupInfo['queries']."\n";
	$output .= "-- Shots to file: ".$this-&gt;backupInfo['written']."\n";
	$output .= "-- Total time: ".$this-&gt;stopTime($this-&gt;backupInfo['start_time'])."\n";
	$output .= "-- Timestamp start: ".$this-&gt;backupInfo['timestamp']."\n";
	$output .= "--\n";
	return $output;
}</code></pre>
<p>Metoda <em>DB::downloadHeaders()</em> zašle potřebné hlavičky pro <strong>force download</strong>, naproti tomu <em>Db::writeToFile()</em> bude obsluhovat průběžný zápis do souboru. Nakonec uvedu ještě funkci <em>Db::addExportCondition()</em>, kterou využijeme v případě, když budeme chtít exportovat pouze některé tabulky nebo jejich části.</p>
<pre><code class="php">private function downloadHeaders($file_size, $file_type, $file_name){
	header("Pragma: public");
	header("Expires: 0");
	header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
	header("Cache-Control: private", false);
	header("Content-Transfer-Encoding: binary");
	header("Content-Type: ".$file_type);
	header("Content-Length: ".$file_size);
	header("Content-Disposition: attachment; filename=".$file_name);
}

// pouzivano na nekolika mistech v zaloze
// zapis do souboru
// @param string $file_name
// @param string $content
// @return null abych si automaticky vynulloval promennou
public function writeToFile($file_name, $content){
	$file = fopen($file_name, 'a');
	fwrite($file, $content);
	fclose($file);
	return null;
}

public function addExportCondition($table, $where = null){
	$this-&gt;exportConditions[$table] = $where; 
}</code></pre>
<p>Nyní už se můžeme pustit do exportu. Funkci uvedu pouze jednu, metodu <em>Db::dumpAndSave()</em> - tu složitější. Tělo pouze kopíruje úvodní vzor, přidaná je podmínka na kontrolu nastavení exportu (<em>konkrétní tabulky a vnitřní podmínky</em>).</p>
<pre><code class="php">public function dumpAndSave(){
	$limit = 5000000;
	
	$this-&gt;prepareBackupInfo();
	$file_name = $this-&gt;backupFolder.$this-&gt;database.'_'.$this-&gt;backupInfo['date_pattern'].'.sql';
	
	if($this-&gt;backupFolder &amp;&amp; !is_dir($this-&gt;backupFolder)){
		mkdir($this-&gt;backupFolder, '0777');
	}
	
	$_tables = array();
	$_select = $this-&gt;query('SHOW TABLES');
	
	while($t = $_select-&gt;fetch(FETCH_ROW)){
		$_tables[] = $t[0];
	}
	
	unset($_select);

	$_output = null;
	$_output = "--\n-- Database: `".$this-&gt;database."`\n--\n\n-- --------------------------------------------------------\n\n";
	$_output = $this-&gt;writeToFile($file_name, $_output);
	
	// zaloha po jedne tabulce
	foreach ($_tables as $value){
		if(!count($this-&gt;exportConditions) || (count($this-&gt;exportConditions) &amp;&amp; isset($this-&gt;exportConditions[$value]))){
		
			// show create table
			$_select_ct = $this-&gt;query("SHOW CREATE TABLE `{$value}`");
			$_row = $_select_ct-&gt;fetch(FETCH_ROW);
			
			// ukladame do souboru show create table
			$_output = null;
			$_output .= "--\n";
			$_output .= "-- `".$_row[0]."`\n";
			$_output .= "--\n\n";
			$_output .= $_row[1]." ;\n\n";
			$_output = $this-&gt;writeToFile($file_name, $_output);
			
			unset($_select_ct);
			
			&plus;&plus;$this-&gt;backupInfo['queries'];

			if(isset($this-&gt;exportConditions[$value])){
				$_select = $this-&gt;query("SELECT// FROM `{$value}` {$this-&gt;exportConditions[$value]}");
			}else{
				$_select = $this-&gt;query("SELECT// FROM `{$value}`");
			}
			
			while($t = $_select-&gt;fetch(FETCH_ROW)){
				foreach ($t as $k =&gt; $i){
					$t[$k] = "'".$this-&gt;escape($i)."'";
				}
				
				$_output .= "INSERT INTO `{$value}` VALUES (".implode(',', $t).");\n";
				&plus;&plus;$this-&gt;backupInfo['queries'];
				
				// co 5 mega, to zapis do souboru a vynulovani promenne. abych neshazoval server
				if(strlen($_output) &gt; $limit){
					$_output = $this-&gt;writeToFile($file_name, $_output);
					&plus;&plus;$this-&gt;backupInfo['written'];
				}
			}
			
			unset($_select);
			
			// pokud tu jsou nejake resty. zbavime se jich. tady uz tech 5 mega fakt testovat nebudu a rovnou to zapisu
			if($_output){
				$_output = $this-&gt;writeToFile($file_name, $_output);
				&plus;&plus;$this-&gt;backupInfo['written'];
			}
		}
	}
	
	$_output .= $this-&gt;appendBackupInfo();
	$_output = $this-&gt;writeToFile($file_name, $_output);
}</code></pre>
<p>Aplikace metod bude opět naprosto triviální. Zakomentované řádky nastaví, že se exportuje celá tabulka s kategoriemi a tabulka s produkty zařazenými do kategorie 1, 2 nebo 3.</p>
<pre><code class="php">$db = new Db;
// $db-&gt;addExportCondition('shop_categories', '');
// $db-&gt;addExportCondition('shop_products', 'WHERE category_id IN (1,2,3)');
$db-&gt;dumpAndSave();</code></pre>
<p>Znění druhé funkce si většina z vás jistě domyslí. Ostatní si budou muset počkat na zveřejnění zdrojového kódu, které je naplánováno na šestý díl. <strong>Příště</strong> si povíme něco o <strong>logování změn</strong>.</p>
<blockquote>
<p><em><em>Edit 4. 5. 2020: </em></em><br />Text se týká PHP 5. Pod PHP 7 už třída fungovat nebude, protože všechny mysql_ funkce skončí chybou.</p>
</blockquote>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2023/renewed/oop.jpg</url>
			</image>
		</item>
		
		
		<item>
			<title>Návrh databázové třídy - díl III: class Query</title>
			<link>https://mike.treba.cz/navrh-databazove-tridy-dil-iii-class-query/</link>
			<pubDate>Mon, 3 May 2010 18:56:33 +1100</pubDate> 
			<comments>https://mike.treba.cz/navrh-databazove-tridy-dil-iii-class-query/#comments</comments>
			<dc:creator></dc:creator>
			<description><![CDATA[<p>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 <strong>instance třídy Query</strong>, respektive zavoláním některé její metody. Jak to všechno funguje uvnitř třídy Query bude tématem tohoto článku. </p>]]></description>   
			<guid isPermaLink="false">https://mike.treba.cz/navrh-databazove-tridy-dil-iii-class-query/</guid>
			<content:encoded><![CDATA[<p>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 <strong>instance třídy Query</strong>, respektive zavoláním některé její metody. Jak to všechno funguje uvnitř třídy Query bude tématem tohoto článku. </p> <p>Krátce ještě zopakuji, co vše už máme: jsou to základní metody třídy Db pro volání SQL dotazů: <em>SELECT</em>, <em>UPDATE</em>, <em>INSERT</em>, <em>DELETE</em>. 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.</p>
<h2>Cíle</h2>
<p>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í <strong>výjimku</strong> a dále už se o nic nestará: to bude v pravomoci třídy <strong>DbException</strong>, které se budu věnovat v pokračování. Seznam členských proměnných napoví základní vlastnosti:</p>
<pre><code class="php">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;
}</code></pre>
<p>V konstruktoru pak proběhne funkce mysql_query, která při nezdaru skončí <strong>výjimkou</strong>. 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.</p>
<p>Konstruktor je opravdu triviální, tam snad nic popisovat nemusím.</p>
<pre><code class="php">public function __construct(Db $db, $query){
	$this-&gt;db = $db;
	try {
		$conn = $this-&gt;db-&gt;getConnection();
		$this-&gt;query = @mysql_query($query, $conn);
		$this-&gt;stringQuery = $query;
		if(!$this-&gt;query){
			throw new DbException(null, $query);
		}
	}catch(Exception $e){
		$e-&gt;show();	
	}
}</code></pre>
<h2>mysql_num_rows, affected_rows, insert_id</h2>
<p>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ě.</p>
<pre><code class="php">public function numRows(){
	if(isset($this-&gt;numRows)){
		return $this-&gt;numRows;
	}else{
		return $this-&gt;countNumRows();
	}
}

public function insertId(){
	if(!isset($this-&gt;insertId)){
		$this-&gt;insertId = mysql_insert_id();
	}
	return $this-&gt;insertId;
}

public function affectedRows(){
	if(!isset($this-&gt;affectedRows)){
		$this-&gt;affectedRows = mysql_affected_rows();
	}
	return $this-&gt;affectedRows;
}</code></pre>
<h2>Získání řádku, celé tabulky a jedné položky</h2>
<p>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.</p>
<h3>mysql_result</h3>
<p>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:</p>
<pre><code class="php">public function result($col = 0, $field = null) {
	if ($this-&gt;numRows()) {
		return mysql_result($this-&gt;query, $col, $field);
	} else {
		return false;
	}
}</code></pre>
<h3>Jediný řádek</h3>
<p>Č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.</p>
<pre><code class="php">public function result($col = 0, $field = null){
	if($this-&gt;numRows()){
		return mysql_result($this-&gt;query, $col, $field);
	}else{
		return false;
	}
}

public function row($type = FETCH_ASSOC){
	$return = $this-&gt;fetch($type);
	return !empty($return) ? $return : false;
}</code></pre>
<h3>Asociativní pole z SQL dotazu</h3>
<p>Přichází na řadu nejsilnější a nejvyužívanější metoda: <strong>selectAssocList</strong>. 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.</p>
<p>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 <em>name</em> tak sloupec <em>title</em> (a je zapnuté nahrazování), doplní se do každé prázdné hodnoty <em>title</em> obsah sloupečku <em>name</em>.</p>
<p>V prvním parametru předáme jméno sloupce, jenž bude sloužit jako klíč. V dalších dvou pak parametry pro <strong>generování odkazů</strong>. Pokud zavoláme i s posledním parametrem, můžeme slepit více polí dohromady.</p>
<pre><code class="php">// 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-&gt;assocList)){
		return $this-&gt;assocList;
	}else{
		$this-&gt;assocList = (array)$array;
	}

	while($t = $this-&gt;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 &amp;&amp; array_key_exists('name', $t) &amp;&amp; array_key_exists('title', $t)){
			$t['title'] = $t['title'] ? $t['title'] : $t['name'];
		}
		
		if(isset($key, $t[$key])){
			$this-&gt;assocList[$t[$key]] = $t;
		}else{
			$this-&gt;assocList[] = $t;
		}
	}

	return $this-&gt;assocList;
}</code></pre>
<h2>Automatizace mysql_free_resultu</h2>
<p>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 <strong>destruktoru</strong>, takže kdykoli zanikne proměnná s objektem aktuálního SQL dotazu, provede se automaticky i mysql_free_result.</p>
<pre><code class="php">public function __unset($value = null){
	if($this-&gt;query !== false &amp;&amp; is_resource($this-&gt;query)){
		mysql_free_result($this-&gt;query);
	}
	$this-&gt;query = false;
}
	 
public function __destruct(){
	$this-&gt;__unset();
}</code></pre>
<h2>Použití</h2>
<p>Nyní už k použití. Jak jsem sliboval, zápis jednotlivých metod bude jednodušší, intuitivnější:</p>
<pre><code class="php">// vygenerování asociativního pole včetně odkazů na kategorie
$query = "SELECT * FROM ?_tabulka WHERE parent_id = '?'";
$array = $db-&gt;query($query, array($_GET['id']))-&gt;assocList('id', 'seo_url', './kategorie/%s/');

// vygenerování asociativního pole, klíče klasicky od nuly
$array = $db-&gt;query("SELECT * FROM ?_tabulka")-&gt;assocList();

// mysql_result
$result = $db-&gt;query("SELECT id FROM ?_tabulka LIMIT 1")-&gt;result();

// vytvoření zdroje &plus; projetí cyklem
$q = $db-&gt;query("SELECT * FROM ?_tabulka");
while ($item = $q-&gt;fetch()) {
	
}

// získání jediného řádku
$query = "SELECT * FROM ?_categories WHERE seo_url = '?'";
$category = $db-&gt;query($query, array($_GET['path']))-&gt;row();
</code></pre>
<div id="_mcePaste" class="mcePaste" style="position: absolute; left: -10000px; top: 0px; width: 1px; height: 1px; overflow-x: hidden; overflow-y: hidden;">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.<br /><br />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.<br />Cíle<br /><br />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:<br />final class Query implements QueryModel { <br /> <br /> /** <br /> * reference na db objekt <br /> * @var object <br /> **/ <br /> private $db; <br /> <br /> /** <br /> * obecné nastavení všech instancí <br /> * náhrada titulku <br /> **/ <br /> public static $titleReplace = false; <br /> <br /> /** <br /> * nyní už info o daném SQL dotazu <br /> * query : string | resource | assocList | num_rows | affected_rows | insert_id <br /> **/ <br /> private $stringQuery; <br /> <br /> private $query; <br /> <br /> private $assocList; <br /> <br /> private $numRows; <br /> <br /> private $affectedRows; <br /> <br /> private $insertId; <br />} <br /><br />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.<br /><br />Konstruktor je opravdu triviální, tam snad nic popisovat nemusím.<br />/** <br /> * konstruktor <br /> * @param Db object $db <br /> * @param string $query <br /> **/ <br />public function __construct(Db $db, $query) { <br /> $this-&gt;db = $db; <br /> <br /> try { <br /> $conn = $this-&gt;db-&gt;getConnection(); <br /> $this-&gt;query = @mysql_query($query, $conn); <br /> $this-&gt;stringQuery = $query; <br /> <br /> if (!$this-&gt;query) { <br /> throw new DbException(null, $query); <br /> } <br /> } catch (Exception $e) { <br /> $e-&gt;show(); <br /> } <br />}<br />mysql_num_rows, <br />affected_rows, <br />insert_id<br /><br />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ě.<br />public function numRows() { <br /> if (isset($this-&gt;numRows)) { <br /> return $this-&gt;numRows; <br /> } else { <br /> return $this-&gt;countNumRows(); <br /> } <br />} <br /> <br />public function insertId() { <br /> if (!isset($this-&gt;insertId)) { <br /> $this-&gt;insertId = mysql_insert_id(); <br /> } <br /> return $this-&gt;insertId; <br />} <br /> <br />public function affectedRows() { <br /> if (!isset($this-&gt;affectedRows)) { <br /> $this-&gt;affectedRows = mysql_affected_rows(); <br /> } <br /> return $this-&gt;affectedRows; <br />}<br />Získání <br />řádku, <br />celé <br />tabulky <br />a <br />jedné <br />položky<br /><br />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.<br />mysql_result<br /><br />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:<br />public function result($col = 0, $field = null) { <br /> if ($this-&gt;numRows()) { <br /> return mysql_result($this-&gt;query, $col, $field); <br /> } else { <br /> return false; <br /> } <br />}<br />Jediný <br />řádek<br /><br />Č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.<br />public function fetch($type = FETCH_ASSOC) { <br /> $function = 'mysql_fetch_'.$type; <br /> return $function($this-&gt;query); <br />} <br /> <br />public function row() { <br /> $return = $this-&gt;fetch(); <br /> return !empty($return) ? $return : false; <br />} <br />Asociativní <br />pole <br />z <br />SQL <br />dotazu<br /><br />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.<br /><br />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.<br /><br />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.<br />/** <br /> * návrat asociativního pole <br /> * @param string [$key] - sloupec, kt. bude klíč * @param string [$col] - sloupec, kt. bude odkaz <br /> * @param string [$pattern] - pro generování odkazu, ve tvaru http://www.treba.cz/kategorie/%s/ <br /> * @return array <br /> **/ <br />public function assocList($key = null, $col = null, $pattern = null, $array = array()) { <br /> static $checked = null; <br /> <br /> if (isset($this-&gt;assocList)) { <br /> return $this-&gt;assocList; <br /> } else { <br /> $this-&gt;assocList = (array)$array; <br /> } <br /> <br /> while ($t = $this-&gt;fetch()) { <br /> if (isset($col, $pattern, $t[$col])) { <br /> <br /> // pouze pro to, aby mb_substr_count proběhl jen jednou <br /> if (!isset($checked)) { <br /> if (mb_substr_count($pattern, '%s', 'utf-8') == 1) { <br /> $checked = true; <br /> } else { <br /> $checked = false; <br /> } <br /> } <br /> <br /> if ($checked) { <br /> $t['href'] = sprintf($pattern, $t[$col]); <br /> } <br /> } <br /> <br /> // je nastaveno automatické doplňování titulku ze jména <br /> if (self::$titleReplace &amp;&amp; array_key_exists('name', $t) &amp;&amp; array_key_exists('title', $t)) { <br /> $t['title'] = $t['title'] ? $t['title'] : $t['name']; <br /> } <br /> <br /> if (isset($key, $t[$key])) { <br /> $this-&gt;assocList[$t[$key]] = $t; <br /> } else { <br /> $this-&gt;assocList[] = $t; <br /> } <br /> } <br /> <br /> return $this-&gt;assocList; <br />} <br />Automatizace <br />mysql_free_resultu<br /><br />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.<br />public function __unset($value = null) { <br /> if ($this-&gt;query !== false &amp;&amp; is_resource($this-&gt;query)) { <br /> mysql_free_result($this-&gt;query); <br /> } <br /> $this-&gt;query = false; <br />} <br /> <br />public function __destruct() { <br /> $this-&gt;__unset(); <br />} <br />Použití<br /><br />Nyní už k použití. Jak jsem sliboval, zápis jednotlivých metod bude jednodušší, intuitivnější:<br />// vygenerování asociativního pole včetně odkazů na kategorie <br />$query = "SELECT * FROM ?_tabulka WHERE parent_id = '?'"; <br />$array = $db-&gt;query($query, array($_GET['id']))-&gt;assocList('id', 'seo_url', './kategorie/%s/'); <br /> <br />// vygenerování asociativního pole, klíče klasicky od nuly <br />$array = $db-&gt;query("SELECT * FROM ?_tabulka")-&gt;assocList(); <br /> <br />// mysql_result <br />$result = $db-&gt;query("SELECT id FROM ?_tabulka LIMIT 1")-&gt;result(); <br /> <br />// tytvoření zdroje &plus; projetí cyklem <br />$q = $db-&gt;query("SELECT * FROM ?_tabulka"); <br />while ($item = $q-&gt;fetch()) { <br /> <br />} <br /> <br />// získlání jediného řádku <br />$query = "SELECT * FROM ?_categories WHERE seo_url = '?'"; <br />$category = $db-&gt;query($query, array($_GET['path']))-&gt;row();</div>
<blockquote>
<p><em>Edit 4. 5. 2020: </em><br />Text se týká PHP 5. Pod PHP 7 už třída fungovat nebude, protože všechny mysql_ funkce skončí chybou.</p>
</blockquote>]]></content:encoded>
			<image>
    				<url>https://mike.treba.cz/img/2023/renewed/code.jpg</url>
			</image>
		</item>
		
	
</channel>
</rss>