MySQL database wrapper voor PHP

Eindelijk bijgewerkt: 6 maart 2009: nieuwe versie beschikbaar, niet backwards-compatible met eerdere versie

Eén van de uitdagingen bij het bouwen van een website is om in de gaten te houden wanneer het mis gaat. How weet je wanneer een database query mislukt? Of wanneer de MySQL server onbereikbaar is, maar de webserver het wel gewoon doet?

De functionaliteit die ik nodig had was dat de website mij automatisch een e-mail stuurt als er iets mis gaat met de database. De complete code van hier gedownload worden (ZIP file). De code zal ook in dit artikel komen, maar dan in stukjes.

Om te beginnen heeft de klass een configuratiebestand nodig in dezelfde map als de klasse zelf, genaamd settings.database.php. Dit bestand bevat de gegevens om verbinding met de database te maken:

1
2
3
4
5
6
<?php
$sDbServerName = "localhost";
$sDbDatabase   = "test";
$sDbUsername   = "root";
$sDbPassword   = "";
?>

Je zal deze vier variabelen moeten aanpassen om met jouw database verbinding te kunnen maken. De namen zijn duidelijk genoeg, en zijn voor de server om mee te verbinden, de naam van de te gebruiken database, en de gebruikersnaam en het wachtwoord om te gebruiken bij het inloggen op de MySQL-server.

Dan nu de klasse-definitie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Database
{
	private $sErrorMail = "webmaster@example.com";
	private $rDb;
	private $rResult;
	private $sServerUrl;
	private $sDatabase;
	private $sUsername;
	private $sPassword;
	public $sQuery = "";
	public $iRows = 0;
	public $sError = "";
	public $iErrNo = 0;
	public $iRowId = 0;
	public $aResult = array();

Hier worden alle variabelen voor gebruikt:

Foutmeldingen:

$sErrorMail
Als een query mislukt, of als het niet lukt te verbinden met de database, wordt automatisch een foutmelding met debug-informatie verstuurd naar het hier opgegeven e-mail adres.

Interne verwijzingen:

$rDb
Deze variabele bevat een verwijzing naar de databaseverbinding.
$rResult
Deze variabele bevat een verwijzing naar de ruwe data van de laatst uitgevoerde query.

Private database-verbinding details (niet beschikbaar van buiten de klasse):

$sServerUrl
MySQL-server hostnaam om mee te verbinden.
$sDatabase
Naam van de te gebruiken database.
$sUsername
Gebruikersnaam om mee in te loggen bij het verbinden met de server.
$sPassword
Wachtwoord om mee in te loggen bij het verbinden met de server.

Public variabelen (beschikbaar van buiten de klasse):

$sQuery
De laatst uitgevoerde query.
$iRows
Het aantal resultaten (of aangepaste rijden) van de laatst uitgevoerde query.
$sError
Foutmelding (indien van toepassing) teruggekregen van de laatst uitgevoerde query.
$iErrNo
Foutnummer (indien van toepassing) teruggekregen van de laatst uitgevoerde query.
$iRowId
He primary key ID van de rij die bij de laatst uitgevoerde query is ingevoegd (als dat een INSERT query was).
$aResult
Een associative array met alle rijen die van de laatste query zijn teruggekomen.

De constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
	public function __construct(
		$bPersist   = false,
		$sServerUrl = false,
		$sDatabase  = false,
		$sUsername  = false,
		$sPassword  = false
	)
	{
		if ($sServerUrl) $this->sServerUrl = $sServerUrl;
		if ($sDatabase)  $this->sDatabase  = $sDatabase;
		if ($sUsername)  $this->sUsername  = $sUsername;
		if ($sPassword)  $this->sPassword  = $sPassword;
 
		if (empty($this->sServerUrl))
		{
			include(dirname(__FILE__)."/settings.database.php");
			$this->sServerUrl = $sDbServerName;
			$this->sDatabase  = $sDbDatabase;
			$this->sUsername  = $sDbUsername;
			$this->sPassword  = $sDbPassword;
		}
 
		if ($bPersist)
		{
			$bIsConnected = @mysql_pconnect($this->sServerUrl, $this->sUsername, $this->sPassword);
 
			if (!$bIsConnected)
			{
				$bIsConnected = @mysql_connect($this->sServerUrl, $this->sUsername, $this->sPassword);
 
				if (!$bIsConnected)
				{
					$this->sError = @mysql_error();
					$this->iErrNo = @mysql_errno();
 
					@mail($this->sErrorMail, "[{$_SERVER['SERVER_NAME']}] Connection failure", "Failed to connect to the database:nn{$this->iErrNo}: {$this->sError}", "From: ErrorHandler <errors@example.com>rn");
					trigger_error("Failed to connect to the database (error #{$this->iErrNo})", E_USER_ERROR);
				}
				else
				{
					$this->rDb = $bIsConnected;
				}
			}
			else
			{
				$this->rDb = $bIsConnected;
			}
		}
		else
		{
			$bIsConnected = @mysql_connect($this->sServerUrl, $this->sUsername, $this->sPassword);
 
			if (!$bIsConnected)
			{
				$this->sError = @mysql_error();
				$this->iErrNo = @mysql_errno();
 
				@mail($this->sErrorMail, "[{$_SERVER['SERVER_NAME']}] Connection failure", "Failed to connect to the database:nn{$this->iErrNo}: {$this->sError}", "From: ErrorHandler <errors@example.com>rn");
				trigger_error("Failed to connect to the database (error #{$this->iErrNo})", E_USER_ERROR);
			}
			else
			{
				$this->rDb = $bIsConnected;
			}
		}
 
		$bIsReady = @mysql_select_db($this->sDatabase, $this->rDb);
 
		if (!$bIsReady)
		{
			// Set the errors
			$this->sError = @mysql_error();
			$this->iErrNo = @mysql_errno();
 
			@mail($this->sErrorMail, "[{$_SERVER['SERVER_NAME']}] Selection failure", "Failed to select the database:nn{$this->iErrNo}: {$this->sError}", "From: ErrorHandler <errors@example.com>rn");
			trigger_error("The database could not be opened (error #{$this->iErrNo})", E_USER_ERROR);
		}
	}

Dit gebeurt er. Je roept de klasse aan, alle parameters zijn optioneel. Je kan ze weglaten om te verbinden met de databasegegevens die in het settings.database.php-bestand staan, of je kan ze zoals hierboven beschreven invullen.

De functie controleert of om een blijvende verbinding gevraagd wordt, en gebruikt dan mysql_pconnect() of mysql_connect(). Als dat niet lukt, en er was een e-mail adres opgegeven, dan wordt er een e-mail naar dat adres gestuurd met de ontvangen mysql_error(). Daarnaast zal alle verdere scriptuitvoer worden afgebroken met de melding"Failed to connect to the database."

Als een verbinding tot stand kon worden gebracht, wordt geprobeerd de database te openen met mysql_select_db(). Als dat niet lukt wordt er weer een e-mail verstuurd en alle scriptuitvoer wordt afgebroken met de melding "Failed to open the database."

Dat is alles. Als je meer wilt moet je een functie aanroepen van het databse object. Zo kan je, bijvoorbeeld, een string opschonen zodat het (relatief) veilig is deze in een query te gebruiken:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	function Clean(
		$sText = null
	)
	{
		if (ctype_digit($sText)) return "'{$sText}'";
		else if (empty($sText)) return "''";
		else if (!$sText || is_null($sText) || $sText == null) return "NULL";
 
		$bCouldEscape = @mysql_real_escape_string($sText, $this->rDb);
 
		if (!$bCouldEscape)
		{
			$sText = addslashes($sText);
		}
		else $sText = $bCouldEscape;
 
		return "'".$sText."'";
	}

Als de string numeriek is, wordt deze ongewijzigd als string teruggegeven, inclusief enkele quotes. Dit wordt als eerste gedaan, om te voorkomen dat het cijfer 0 als NULL of lege string wordt teruggegeven. Als de string echt een lege string is, wordt een lege string inclusief enkele quotes teruggegeven. Of, als de string een boolean false of een null-waarde is, wordt de string NULL teruggegeven, zonder quotes. MySQL zou dat moeten zien als null-waarde. Voor andere waarden wordt de string met mysql_real_escape_string() opgeschoond, waarvoor een actieve MySQL-verbinding nodig is, dus deze zou wel eens kunnen mislukken. Als dat zo is, wordt addslashes() gebruikt, wat in queries nog wel eens mis wil gaan. De opgeschoonde string wordt uiteindelijk inclusief enkele quotes teruggegeven, dus als je deze functie gebruikt moet je ervoor zorgen dat deze niet al in je query zitten.

Een query uitvoeren:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	function Execute(
		$sQuery,
		$sFilename   = null,
		$iLineNumber = null,
		$bDebug      = false
	)
	{
		$this->rResult = null;
		$this->sQuery   = "";
		$this->iRows    = 0;
		$this->iRowId   = 0;
		$this->sError   = "";
		$this->iErrNo   = 0;
		$this->aResult  = array();
 
		if (empty($sFilename)) $sFilename     = $_SERVER['REQUEST_URI'];
		if (empty($iLineNumber)) $iLineNumber = "(??)";
 
		$this->sQuery = $sQuery;
 
		if ($bDebug) echo "<pre>{$sFilename}:{$iLineNumber}nn".__FILE__.":".(__LINE__ - 3)."nn{$sQuery}nn";
 
		$bIsExecuted = @mysql_query($sQuery, $this->rDb);
 
		if (!$bIsExecuted)
		{
			$this->sError = @mysql_error($this->rDb);
			$this->iErrNo = @mysql_errno($this->rDb);
 
			@mail($this->sErrorMail, "[{$_SERVER['SERVER_NAME']}] Query failure", "Failed to properly execute a query in {$sFilename} on line {$iLineNumber}.nn{$sQuery}nn{$this->iErrNo}: {$this->sError}", "From: ErrorHandler <errors@example.com>rn");
 
			if ($bDebug)
			{
				die($this->iErrNo.": ".$this->sError."nn</p"."re>");
			}
		}
		else
		{
			$this->rResult = $bIsExecuted;
			$this->iRows = 0;
			$type = 0;
 
			if (($this->iRows = @mysql_num_rows($this->rResult))) { $type = 1; }
			else if (($this->iRows = @mysql_affected_rows($this->rDb))) { $type = 2; }
 
			$this->iRowId = 0;
			if (($this->iRowId = @mysql_insert_id($this->rDb))) {}
 
			if ($this->iRows > 0 && $type == 1)
			{
				while ($aResult = @mysql_fetch_assoc($this->rResult))
				{
					$this->aResult[] = $aResult;
				}
			}
 
			if ($bDebug)
			{
				die(print_r($this->aResult, true)."nn</p"."re>");
			}
		}
	}

Wanneer deze functie wordt aangeroepen, is $sQuery vereist, maar $sFileName, $iLineNumber en $bDebug zijn optioneel. De laatste drie zijn erg handig bij het debuggen, echter ik raad wel aan om niet te proberen bij te blijven met de bestandsnaam en regelnummers, omdat je de bestanden waarschijnlijk regelmatig zal veranderen, en queries schuiven steeds op. Gelukkig heeft PHP een aantal constanten ingebouwd, waaronder __FILE__ en __LINE__, die altijd de bestandsnaam en regelnummer vanwaar ze zijn aangeroepen bevatten. Gebruik die.

De functie gooit eerst oude resultaten weg. Dan wordt het huidige bestand bepaald aan de hand van de opgevraagde URL. Vervolgens wordt de query opgeslagen, en als de query in debug-modus wordt uitgevoerd worden een aantal details op het scherm gezet. Let op: In debug-modus worden queries WEL uitgevoerd. Wees dus voorzichtig bij het debuggen van database-veranderende queries, zoals INSERT, UPDATE, DELETE, ALTER, CREATE enz.

Nadat de query is uitgevoerd, en de query is mislukt, wordt de foutmelding opgeslagen, gemaild, en op het scherm gezet (als debug-modus gebruikt wordt). Als de query gelukt is, wordt het aantal resultaten, en indien van toepassing het auto_increment ID, opgeslagen. Daarna worden de resultaten in de array opgeslagen. In debug-modus, wordt die array dan op het scherm gezet, en verdere script-uitvoer wordt onderbroken.

Foutmelding e-mailadres aanpassen tijdens uitvoer:

1
2
3
4
	public function setErrorMail($sEmail)
	{
		$this->sErrorMail = $sEmail;
	}

Het is mogelijk om tijdens het uitvoeren van de code de ontvanger van foutmeldingen aan te passen. Dit is handig, bijvoorbeeld wanneer de applicatie door één programmeur is gebouwd, en een ander komt even iets aanpassen of toevoegen. Als een globaal database object wordt gebruikt, kan de nieuwe ontwikkelaar er hiermee voor zorgren dat de code foutmeldingen naar hem toe stuurt, en niet naar de oorspronkelijke ontwikkelaar.

Huidige ontvanger van foutmeldingen uitlezen:

1
2
3
4
5
6
	public function getErrorMail()
	{
		return $this->sErrorMail;
	}
}
?>

Met deze functie is het mogelijk uit te lezen welk e-mail adres momenteel gebruikt wordt om foutmeldingen naartoe te sturen. Erg handig als de setErrorMail() functie vaak gebruikt wordt.

Momenteel zijn dat alle functies die in de klasse beschikbaar zijn. Hier een aantal voorbeelden hoe je het kan gebruiken:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
require_once("class.Database.php");
$oDb = new Database();
 
// Haal de eerste 5 rijen van mijnkolom1 en mijnkolom2 op uit mijntabel
$oDb->Execute("
	SELECT mijnkolom1,
	       mijnkolom2
	FROM   mijntabel
	LIMIT  0,5
", __FILE__, __LINE__);
 
foreach ($oDb->aResult as $iRowNo => $aRow)
{
	echo "<strong>Rij ".$iRowNo.":</strong><br />n";
	echo "MijnKolom1: ".$aRow['mijnkolom1']."<br />n";
	echo "MijnKolom2: ".$aRow['mijnkolom2']."<br />n";
}
 
/* Geeft de volgende uitvoer als er maar 2 rijen beschikbaar zijn:
Rij 0:
MijnKolom1: waarde
MijnKolom2: waarde
 
Rij 1:
MijnKolom1: waarde
MijnKolom2: waarde
*/
 
// Een waarde ophalen met gebruik van een WHERE clausule
$oDb->Execute("
	SELECT *
	FROM   mijntabel
	WHERE  mijnkolom1 = ".$oDb->Clean($_GET['mijnkolom1'])."
	LIMIT  0,1
", __FILE__, __LINE__);
 
// Enkele quotes worden automatisch om de string gezet door de Clean() functie
?>

Ik weet zeker dat je nog wel vragen zal hebben. Je kan deze stellen bij de blog post, dan zal ik ze proberen te beantwoorden zodat je ermee kan werken.

Succes!

blog comments powered by Disqus