V minulém díle jsme řešili jen lehké úpravy, které naší online hře pomáhají v hratelnosti. V naší hře na běh minovým polem jsme zavedli zobrazení předchozí krok, abychom hráči ukázali úroveň nebezpečí a míru s jakou se mu vyhnul.
V dnešním díle nás čeká poněkud nezáživnější část. A to komunikace s databází. Dříve než se pustíme do samotné práce, dovolím si ještě malou odbočku. Když jsem dělal zkoušku z jazyka SQL, dovolil jsem si pronést větu ve znění „Databáze stejně nebudu nikdy potřebovat.“ Zkoušející se tehdy jen lehce usmála a já zkoušku nakonec dostal. S postupem doby jsem její úsměv pochopil a dnes se vlastně divím, jak silné měla sebeovládání. Dnes, poučen praxí a mnoha a mnoha lety vývoje různých aplikací, můžu s klidem říct, že databáze jsou fenomén dnešní doby. Prakticky odjakživa existovala potřeba shromažďovat data a někde je udržovat. Ve světě výpočetní techniky to tedy nemůže být jinak. Vývoj tedy vedl na SŘBD (děsná zkratka pro Systém Řízení Báze Dat), který ať už je implementován jakkoliv, má za úkol data udržovat v čitelné podobě, tedy alespoň pro program v čitelné. V konečném důsledku to znamená, že už jsem velmi dlouho nenapsal program, který by nepoužíval databázi v nějaké více či méně primitivní podobě. Na internetu a vůbec v oblasti webových aplikací to není jinak. Naopak velmi často jsou webové aplikace na databázi závislejší než klasické desktopové aplikace, které si mohou některá data zapsat do souboru.
Cílem dnešního dílu tedy bude ukázat si jeden z mnoha možných způsobů jak přistupovat k databázi. Vzhledem k tomu, že domovským jazykem naší aplikace je PHP, budeme používat jako databázi řešení tomuto jazyku velmi blízké tedy MySQL.
MySQL je do PHP portováno pomocí rozšíření PHP. Funkce obsažené v tomto rozšíření mají společný prefix názvů (tedy předponu názvů) a tedy MySQL… . Vzato kolem a kolem, je třeba naučit se pouze několik kroků: navázat spojení s databází, položit dotaz, zapsat data, uzavřít spojení s databází. MySQL nám poskytuje výhodu dotazovacího jazyka SQL, který umožňuje data jak číst tak i zapisovat.
Navázání spojení s databází:
MySQL_Connect je kouzelná funkce, která nám pomůže otevřít komunikační kanál s databází, potřebujeme k tomu znát pouze loginové informace do MySQL serveru a jeho adresu. Zbytek obstará funkce samotná. Je třeba si ale uvědomit, že mnoho hostinců s MySQL servery má omezení na současně běžící spojení, proto velmi často jsou spojení do databáze otevírána na dobu nezbytně nutnou. Proti této snaze ale tlačí fakt, že samotné navázání spojení s databází je časově náročný proces, proto jiní autoři řeší tento problém tak, že otevřou spojení a udržují ho pro všechny potřebné operace.
MySQL_Query je funkce, která nám umožní položit databázi dotaz, ať už pro vyzvednutí nebo zapsání dat. Je třeba znát alespoň základní operace dostupné pomocí SQL jazyka. MySQL až na drobnosti implementuje SQL velmi slušně, proto se nemusíme bát nějaké fatální nekompatibility, tedy pokud se nesnažíme vytáhnout z daného sql serveru maximum z jeho možností. Pokud se snažíme data číst, existuje mnoho cest jak je načíst do PHP, jednou z dostupných funkcí je MySQL_Fetch_Array, která přenese data z databázové řádky do proměnné typu pole. Je důležité pamatovat si, že po dokončení práce s čtenými daty je třeba uzavřít práci s datovou sadou pomocí MySQL_free_result, pomocí které jsou data načtená v paměti uvolněna a zdroje serveru jsou mu vráceny k dispozici. Pokud nebudete volat tuto funkci, zpočátku si ničeho nevšimnete, ale váš hoster bude mít hlavu plnou práce zjistit, proč server ztrácí a kam se ta paměť bere. Na IIS je navíc tento fakt zamaskován pod volání PHP samostatně pro každý dotaz uživatele.
MySQL_Close nám umožní spojení do databáze uzavřít a umožnit tak jinému procesu připojení do databáze.
Vyzbrojeni těmito znalostmi přidáme dnes do aplikace právě tuto podporu. Po té, co hráč šlápne na minu, zapíšeme jeho score do tabulky a zároveň umožníme hře ukázat tabulku výsledků a to v podobě dvaceti záznamů s nejvyšším score.
Databázová tabulka tedy musí obsahovat jméno hráče a dosažené score. Jméno hráče je řetězcová informace. Obyčejně se jméno vejde do padesáti znaků, proto nadimenzujeme tuto informaci na řetězec o maximální délce 50 znaků. Dosažené score je čistě numerická informace. Protože je možné, že budeme potřebovat zaměřit konkrétní řádku databázové tabulky a ani obě informace dohromady nejsou jednoznačné, necháme MySQL, aby nám generovalo klíčovou hodnotu samo pomocí takzvané identity, tedy počítadla, které pro každý další řádek má unikátní hodnotu, zpravidla zvětšenou o 1. Příkaz pro založení takové tabulky tedy bude vypadat následovně:
CREATE TABLE `results` (
`id` int(11) NOT NULL auto_increment,
`date` datetime NOT NULL default '0000-00-00 00:00:00',
`username` varchar(100) NOT NULL default '',
`score` bigint(20) NOT NULL default '0',
PRIMARY KEY (`id`),
KEY `IDX_Score` (`score`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
`id` int(11) NOT NULL auto_increment,
`date` datetime NOT NULL default '0000-00-00 00:00:00',
`username` varchar(100) NOT NULL default '',
`score` bigint(20) NOT NULL default '0',
PRIMARY KEY (`id`),
KEY `IDX_Score` (`score`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
Výsledkem, bude tabulka obsahující požadované sloupečky. Kvůli ohledu na rychlost čtení je přidán index, pomocí kterého budeme rychleji vyhledávat zobrazované záznamy o score, a protože ve vyhledávání se budeme řídit právě touto hodnotou, bude stačit index vedoucí právě tímto sloupcem.
V rámci zvýšení bezpečnosti náš programátor při zápisu do tabulky pomocí SQL příkazu INSERT provede také nějaké zpracování vstupních hodnot. Technika, které se tímto snaží zabránit je nazývána SQL-Injection. Postup, který je zvolen je sice účinný ale není stoprocentní.
Dalším velmi častým požadavkem je opětovné čtení dat vložených do databáze, v našem případě se jedná o zobrazení výsledkové tabulky. Zde, stejně jako při zakládání tabulky se programátor opřel o specifické vlastnosti MySQL serveru. V případě čtení z databáze příkazem SELECT se jedná zejména o klauzuli LIMIT na konci příkazu, kterým MySQL umožňuje ořezávat počet záznamů vrácených ke zpracování.
V praxi často bývají skupiny funkcí, v našem případě podpora pro práci s databází, uvedeny v jiném souboru, který je vložen do skriptu, nebo dokonce jsou zařazeny jako metody objektu, který je následně používán. Náš programátor oproti praxi zvolil spíše cestu jediného souboru právě kvůli tomu, aby byl kód na jednom místě.
V příštím díle se podíváme na grafiku aplikace a zareagujeme na připomínky, pro které náš programátor připravil živnou půdu.
Ke stažení Zde
Zdrojový kód:
<?php
// konfigurace
$dbserver = "127.0.0.1";
$dbuser = "mfr";
$dbpwd = "mfr";
$dbname = "mfr";
$tableNameResults = "results";
/*
CREATE DATABASE `mfr`
CREATE TABLE `results` (
`id` int(11) NOT NULL auto_increment,
`date` datetime NOT NULL default '0000-00-00 00:00:00',
`username` varchar(100) NOT NULL default '',
`score` bigint(20) NOT NULL default '0',
PRIMARY KEY (`id`),
KEY `IDX_Score` (`score`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
*/
session_start();
// zpracovani vstupnich parametru
$action = $_POST["action"];
if (!isset($action)){
$action = "G_".$_GET["action"];
}
// *** centralni zpracovani ***
if ( ( GetSession("username","") == "" ) && ($action != "LOGIN" ) )
{
$action = "LOGINFORM";
} else {
$action = StrToUpper( $action );
}
switch( $action ){
case "STEP" : {
OnStep( $_POST["fieldcode"] );
break;
}
case "LOGINFORM" : {
OnLoginForm();
break;
}
case "LOGIN" : {
OnLogin( $_POST["username"] );
break;
}
case "G_SCORE" :
case "SCORE" : {
OnScoreTable();
break;
}
default :{
OnDefault( GetEmptyRow() );
break;
}
}
// *** /centralni zpracovani ***
// *** podpora pro praci s DB ***
function OpenDB()
{
global $dbserver, $dbuser, $dbpwd, $dbname;
$db = mysql_connect($dbserver, $dbuser, $dbpwd );
if (!$db )
{
echo MySQL_Error();
return false;
}
else
{
@mysql_select_db($dbname);
echo MySQL_Error();
return $db;
}
}
function CloseDB( $db )
{
mysql_close($db);
}
function ExecuteSQL( $db, $sql )
{
@MySQL_Query( $sql, $db );
return @mysql_affected_rows($db);
}
function OpenSQL( $db, $sql, $callback )
{
$Q = MySQL_Query( $sql, $db );
if ($Q)
{
while( $row = MySQL_fetch_array( $Q ) )
{
$callback( $row );
}
MySQL_Free_result( $Q );
}
}
function SQLEncode( $str )
{
return mysql_escape_string( $str );
}
function SQLNumber( $str )
{
return 0+$str+0; // :o)
}
// *** /podpora pro praci s DB ***
// *** podpurne funkce ***
function SetSession( $varname, $value ){
$_SESSION[$varname] = $value;
}
function GetSession( $varname, $default ){
if (isset($_SESSION[$varname])) {
return $_SESSION[$varname];
} else {
return $default;
}
}
function WriteHeader(){
echo '<html>
<head>
<title>Mine Field Runner 1.0</title>
<meta name="Copyright" content="genesis, genesis@cardici.com" />
</head>
<body>
<H1>Mine Field Runner</H1>
';
}
function WriteFooter(){
echo ' </body>
</html>';
}
function GetEmptyRow(){
return array( $false, $false, $false, $false, $false, $false, $false, $false, $false, $false );
}
function LogScore( $username, $score )
{
global $dbname, $tableNameResults;
if ( $score < 1 ) return;
$score = SQLNumber( $score );
$username = SQLEncode( $username );
$sql = "INSERT INTO $dbname.$tableNameResults (`date`, `username`, `score`) VALUES ( NOW(), '$username', $score )";
$db = OpenDB();
if ( $db != false ){
ExecuteSQL( $db, $sql );
CloseDB( $db );
}
}
function WriteScoreRow( $row )
{
echo "<tr><td>".$row["username"]."</td><td>".$row["score"]."</td></tr>";
}
function WriteScoreTable()
{
global $dbname, $tableNameResults;
echo "<table>";
$sql = "SELECT username, score FROM $dbname.$tableNameResults ORDER BY `score` DESC limit 10";
$db = OpenDB();
if ( $db != false )
{
OpenSQL( $db, $sql, WriteScoreRow );
CloseDB( $db );
}
else
{
echo "<tr><td>Nebyly nalezeny žádné záznamy</td></tr>";
}
echo "</table>";
}
// *** /podpurne funkce ***
// *** sekce jednotlivych akci ***
function OnLoginForm()
{
WriteHeader();
echo "<H2>Přihlášení</H2>";
echo "<form action=\"?\" method=\"POST\">";
echo "Uživatelské jméno: ";
echo "<input type=\"text\" name=\"username\"/>";
echo "<input type=\"submit\" value=\"Přihlásit\"/>";
echo "<input type=\"hidden\" name=\"action\" value=\"LOGIN\"/>";
echo "</form>";
echo "<a href=\"?action=score\">Výsledková tabulka</a> ";
WriteFooter();
}
function OnLogin( $username )
{
if ( $username != "" )
{
SetSession('username', $username );
SetSession('score', 0);
OnDefault( GetEmptyRow() );
}
else
{
OnLoginForm();
}
}
function OnDefault( $originalRow ){
WriteHeader();
// score
$score = 0+GetSession('score', 0);
echo "<H2>Score $score bodu</H2>";
// herni pole
echo "<form action=\"?\" method=\"POST\">";
// vykresli 10 buttonu
for ( $f = 1; $f < 11; $f++ ){
echo "<input type=\"submit\" name=\"fieldcode\" value=\"$f\" style=\"width:20px;\"/>";
}
// vykresli puvodni radku
if ((!isset( $originalRow ) || (!is_array( $originalRow ) ))){
$originalRow = GetEmptyRow();
}
for ( $f = 1; $f < 11; $f++ ){
$color = "#55FF55";
if( $originalRow[$f]==true) $color = "#FF0000";
echo "<div style=\"float:left;width:20px;height:20px;background-color:$color;\"> </div>";
}
echo "<input type=\"hidden\" name=\"action\" value=\"STEP\"/>";
echo "</form><br/>";
echo "<div>";
echo "<form action=\"?\" method=\"POST\">";
echo "<input type=\"hidden\" name=\"action\" value=\"LOGIN\"/>";
echo "<input type=\"submit\" value=\"Odhlásit\"/>";
echo "</form><br/>";
WriteFooter();
}
function OnStep( $code ){
$score = 0+GetSession('score',0);
$code = 0+$code;
// nasazime miny
$minecount = round($score / 5);
srand( time() );
$row = GetEmptyRow();
if ( $minecount < 10 ) {
for( $f = 0; $f < $minecount; $f++ )
{
$index = rand(1,10);
$row[$index] = true;
}
} else {
for( $f = 1; $f <= 10; $f++ )
{
$row[$f] = true;
}
}
if ( $row[$code] ){
// mina
WriteHeader();
// score
echo "<H2>Score $score bodu</H2>";
// herni pole nahradime hlaskou, ze leti vzduchem
echo "<p>Lituji, ale právě jste šlápl na minu. Snad příště.</p>";
echo "<a href=\"?\">Hrát znovu</a> ";
echo "<a href=\"?action=score\">Výsledková tabulka</a> ";
WriteFooter();
// zapiseme score do db
$username = GetSession('username', $username );
LogScore( $username, $score );
SetSession('score',0);
}
else
{
// volne pole
SetSession('score', $score + 1 );
OnDefault( $row );
}
}
function OnScoreTable()
{
WriteHeader();
echo "<div><a href='?'>Hrát znovu</a></div>";
WriteScoreTable();
WriteFooter();
}
// *** /sekce jednotlivych akci ***
?>
// konfigurace
$dbserver = "127.0.0.1";
$dbuser = "mfr";
$dbpwd = "mfr";
$dbname = "mfr";
$tableNameResults = "results";
/*
CREATE DATABASE `mfr`
CREATE TABLE `results` (
`id` int(11) NOT NULL auto_increment,
`date` datetime NOT NULL default '0000-00-00 00:00:00',
`username` varchar(100) NOT NULL default '',
`score` bigint(20) NOT NULL default '0',
PRIMARY KEY (`id`),
KEY `IDX_Score` (`score`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
*/
session_start();
// zpracovani vstupnich parametru
$action = $_POST["action"];
if (!isset($action)){
$action = "G_".$_GET["action"];
}
// *** centralni zpracovani ***
if ( ( GetSession("username","") == "" ) && ($action != "LOGIN" ) )
{
$action = "LOGINFORM";
} else {
$action = StrToUpper( $action );
}
switch( $action ){
case "STEP" : {
OnStep( $_POST["fieldcode"] );
break;
}
case "LOGINFORM" : {
OnLoginForm();
break;
}
case "LOGIN" : {
OnLogin( $_POST["username"] );
break;
}
case "G_SCORE" :
case "SCORE" : {
OnScoreTable();
break;
}
default :{
OnDefault( GetEmptyRow() );
break;
}
}
// *** /centralni zpracovani ***
// *** podpora pro praci s DB ***
function OpenDB()
{
global $dbserver, $dbuser, $dbpwd, $dbname;
$db = mysql_connect($dbserver, $dbuser, $dbpwd );
if (!$db )
{
echo MySQL_Error();
return false;
}
else
{
@mysql_select_db($dbname);
echo MySQL_Error();
return $db;
}
}
function CloseDB( $db )
{
mysql_close($db);
}
function ExecuteSQL( $db, $sql )
{
@MySQL_Query( $sql, $db );
return @mysql_affected_rows($db);
}
function OpenSQL( $db, $sql, $callback )
{
$Q = MySQL_Query( $sql, $db );
if ($Q)
{
while( $row = MySQL_fetch_array( $Q ) )
{
$callback( $row );
}
MySQL_Free_result( $Q );
}
}
function SQLEncode( $str )
{
return mysql_escape_string( $str );
}
function SQLNumber( $str )
{
return 0+$str+0; // :o)
}
// *** /podpora pro praci s DB ***
// *** podpurne funkce ***
function SetSession( $varname, $value ){
$_SESSION[$varname] = $value;
}
function GetSession( $varname, $default ){
if (isset($_SESSION[$varname])) {
return $_SESSION[$varname];
} else {
return $default;
}
}
function WriteHeader(){
echo '<html>
<head>
<title>Mine Field Runner 1.0</title>
<meta name="Copyright" content="genesis, genesis@cardici.com" />
</head>
<body>
<H1>Mine Field Runner</H1>
';
}
function WriteFooter(){
echo ' </body>
</html>';
}
function GetEmptyRow(){
return array( $false, $false, $false, $false, $false, $false, $false, $false, $false, $false );
}
function LogScore( $username, $score )
{
global $dbname, $tableNameResults;
if ( $score < 1 ) return;
$score = SQLNumber( $score );
$username = SQLEncode( $username );
$sql = "INSERT INTO $dbname.$tableNameResults (`date`, `username`, `score`) VALUES ( NOW(), '$username', $score )";
$db = OpenDB();
if ( $db != false ){
ExecuteSQL( $db, $sql );
CloseDB( $db );
}
}
function WriteScoreRow( $row )
{
echo "<tr><td>".$row["username"]."</td><td>".$row["score"]."</td></tr>";
}
function WriteScoreTable()
{
global $dbname, $tableNameResults;
echo "<table>";
$sql = "SELECT username, score FROM $dbname.$tableNameResults ORDER BY `score` DESC limit 10";
$db = OpenDB();
if ( $db != false )
{
OpenSQL( $db, $sql, WriteScoreRow );
CloseDB( $db );
}
else
{
echo "<tr><td>Nebyly nalezeny žádné záznamy</td></tr>";
}
echo "</table>";
}
// *** /podpurne funkce ***
// *** sekce jednotlivych akci ***
function OnLoginForm()
{
WriteHeader();
echo "<H2>Přihlášení</H2>";
echo "<form action=\"?\" method=\"POST\">";
echo "Uživatelské jméno: ";
echo "<input type=\"text\" name=\"username\"/>";
echo "<input type=\"submit\" value=\"Přihlásit\"/>";
echo "<input type=\"hidden\" name=\"action\" value=\"LOGIN\"/>";
echo "</form>";
echo "<a href=\"?action=score\">Výsledková tabulka</a> ";
WriteFooter();
}
function OnLogin( $username )
{
if ( $username != "" )
{
SetSession('username', $username );
SetSession('score', 0);
OnDefault( GetEmptyRow() );
}
else
{
OnLoginForm();
}
}
function OnDefault( $originalRow ){
WriteHeader();
// score
$score = 0+GetSession('score', 0);
echo "<H2>Score $score bodu</H2>";
// herni pole
echo "<form action=\"?\" method=\"POST\">";
// vykresli 10 buttonu
for ( $f = 1; $f < 11; $f++ ){
echo "<input type=\"submit\" name=\"fieldcode\" value=\"$f\" style=\"width:20px;\"/>";
}
// vykresli puvodni radku
if ((!isset( $originalRow ) || (!is_array( $originalRow ) ))){
$originalRow = GetEmptyRow();
}
for ( $f = 1; $f < 11; $f++ ){
$color = "#55FF55";
if( $originalRow[$f]==true) $color = "#FF0000";
echo "<div style=\"float:left;width:20px;height:20px;background-color:$color;\"> </div>";
}
echo "<input type=\"hidden\" name=\"action\" value=\"STEP\"/>";
echo "</form><br/>";
echo "<div>";
echo "<form action=\"?\" method=\"POST\">";
echo "<input type=\"hidden\" name=\"action\" value=\"LOGIN\"/>";
echo "<input type=\"submit\" value=\"Odhlásit\"/>";
echo "</form><br/>";
WriteFooter();
}
function OnStep( $code ){
$score = 0+GetSession('score',0);
$code = 0+$code;
// nasazime miny
$minecount = round($score / 5);
srand( time() );
$row = GetEmptyRow();
if ( $minecount < 10 ) {
for( $f = 0; $f < $minecount; $f++ )
{
$index = rand(1,10);
$row[$index] = true;
}
} else {
for( $f = 1; $f <= 10; $f++ )
{
$row[$f] = true;
}
}
if ( $row[$code] ){
// mina
WriteHeader();
// score
echo "<H2>Score $score bodu</H2>";
// herni pole nahradime hlaskou, ze leti vzduchem
echo "<p>Lituji, ale právě jste šlápl na minu. Snad příště.</p>";
echo "<a href=\"?\">Hrát znovu</a> ";
echo "<a href=\"?action=score\">Výsledková tabulka</a> ";
WriteFooter();
// zapiseme score do db
$username = GetSession('username', $username );
LogScore( $username, $score );
SetSession('score',0);
}
else
{
// volne pole
SetSession('score', $score + 1 );
OnDefault( $row );
}
}
function OnScoreTable()
{
WriteHeader();
echo "<div><a href='?'>Hrát znovu</a></div>";
WriteScoreTable();
WriteFooter();
}
// *** /sekce jednotlivych akci ***
?>



