De vraag stellen is hem beantwoorden :).
Je zou eerst een (
abstracte) Database klasse kunnen maken waarin je definieert
wat je wilt kunnen doen met deze klasse, zoals:
- het opbouwen van een connectie (kun je in de __construct() regelen)
hierbij zou je uit kunnen gaan van enkele default waarden voor bepaalde instellingen zoals host, poort en character encoding (waarvan je wel kunt afwijken als je afwijkende instellingen hebt maar standaard hoef je niets extra's hiervoor te doen)
- het uitvoeren van een query (doh)
- het starten, committen en rollbacken van transacties (als je database engine dit ondersteunt)
- het ophalen van het laatst toegevoegde auto-increment id
- het ophalen van het aantal "affected" records na uitvoering van een query
Nu zou je zeggen: "waarom zou ik hiervoor een database klasse moeten maken, deze functies / methoden zijn er al in native PHP".
Deze opzet heeft één belangrijk voordeel: je koppelt het aanspreken van je database los van database-specifieke functies, of liever gezegd, je maakt indirect gebruik van deze database-specifieke functies. Dit is eigenlijk precies wat PDO doet.
Ik ga er een beetje stilzwijgend vanuit dat je gebruik maakt van een MySQL-database, en dat je hiermee wilt communiceren via MySQLi-functies of -methoden die je wilt organiseren in een Database klasse. Dit kan, maar er is dus ook een alternatief: PDO. Beide oplossingen hebben voor- en nadelen. Zo is PDO niet specifiek geshreven voor een specifieke database (en dus ook niet voor MySQL), en als je enkel MySQL-databases gebruikt kun je net zo goed MySQLi gebruiken. Als je PDO gebruikt voor communicatie met je MySQL-database dan zul je je enigszins moeten verdiepen in de werking van de PDO_MySQL driver.
Iets wat beide varianten niet oplossen is een abstractie van de SQL; beide varianten (PDO of je eigen database-toegang-abstractie-laag) bieden een uniforme manier om met je database te communiceren maar de onderliggende SQL die je in beide varianten gebruikt is nog steeds
database-specifiek. Als je een nog verdere abstractie wilt zul je een soort van data-abstractie-laag moeten realiseren, bijvoorbeeld door middel van
Object Relational Mapping (als ik het goed heb?).
Maar goed, terug naar het eerdere genoemde voordeel.
Stel je hebt deze verouderde code met mysql_query aanroepen:
some_code.php
<?php
// do stuff
$res = mysql_query(<stuff>);
while ($row = mysql_fetch_assoc($res)) {
<do stuff>
}
// ...
$res = mysql_query(<more stuff>);
while ($row = mysql_fetch_assoc($res)) {
<do more stuff>
}
?>
Wat nu als je op een gegeven moment besluit dat je wilt overstappen naar MySQLi. Een hele "eenvoudige" manier zou zijn: doe een search-and-replace door heel je code op alle mysql_ functies.
Maar wat als je deze mysql_ functies nou al in een database-klasse had gevangen? Dan hoef je niet alle code aan te passen in alle verschillende bestanden
maar enkel de implementatie van deze klasse te veranderen. Als alle methoden van de klasse goed omschreven zijn zou dit redelijk eenvoudig bewerkstelligd kunnen worden. Dit is veel minder werk en veel minder foutgevoeling omdat je je "definities" maar op één plek aanpast. Het probleem wat je hiermee dus voorkomt is
hardcoding van database-specifieke functies overal in je code.
Uiteraard kom je voor hele andere problemen te staan als je naar een andere database overstapt...
Zoals gezegd: nadat je in je abstracte klasse hebt bedacht
wat voor functionaliteit je wilt hebben (nog even los van hoe je dit doet dus) schrijf je vervolgens een klasse die hiervan afgeleid is en een database-(driver-)specifieke implementatie geeft van je abstracte klasse. Hier stop je dus het
hoe in en geef je precies aan hoe de abstracte methoden afgehandeld zouden moeten worden.
Op eenzelfde wijze zou je ook een abstracte klasse (of interface) kunnen definieren voor een result-set. Hier definieer je welke methoden je verwacht bij implementaties van die klasse (dit is dus in feite ook een specificatie van
wat je wilt kunnen doen met een result-set, bijvoorbeeld:
- het ophalen van een resultaatrij
ik gebruik hierbij enkel fetch_assoc, ik ben niet geinteresseerd in andere varianten
- het ophalen van een enkele kolom
als handige shorthand
- het teruggeven van het aantal resultaatrijen
- het "scrollen" naar een specifiek resultaat (dataSeek)
- het vrijgeven van een resultaat
Na de declaraties van je abstracte klasses implementeer je deze bijvoorbeeld met MySQLi, je krijgt dan zoiets:
Database klasse
<?php
abstract class Database
{
protected $connection;
protected $transactionStarted;
abstract public function __construct($username, $password, $database, $settings=array());
abstract public function escape($input);
abstract public function query($query);
abstract public function startTransaction();
abstract public function commitTransaction();
abstract public function rollbackTransaction();
abstract public function insertId();
abstract public function affectedRows();
}
?>
Database Result klasse
<?php
interface Database_Result
{
public function fetchRow();
public function fetchValue();
public function numRows();
public function dataSeek();
public function freeResult();
}
?>
En de implementaties:
Database MySQLi klasse
<?php
class Database_MySQLi extends Database
{
protected static $queryCount;
public function __construct($username, $password, $database, $settings=array()) {
// Default settings are overridden by $settings.
$settings = $settings + array(
'host' => '127.0.0.1',
'port' => '3306',
'charset' => 'utf8',
);
$this->connection = new MySQLi($settings['host'], $username, $password, $database, $settings['port']);
if ($this->connection->connect_error) {
// @todo throw exception instead?
die('[error] '.$this->connection->connect_errno.', '.$this->connection->connect_error);
}
if (!$this->connection->set_charset($settings['charset'])) {
// @todo throw exception instead?
die('[error] failed to set charset: '.$this->connection->error);
}
$this->transactionStarted = false;
self::$queryCount = 0;
}
public function escape($input) {
return $this->connection->real_escape_string($input);
}
public function query($query) {
if ($this->connection->real_query($query)) {
self::$queryCount++;
return new Database_Result_MySQLi($this->connection);
} else {
// Note that the text that the error returns may contain data provided by the user, so it should be escaped.
throw new Exception($this->connection->error."\n".$query);
}
}
public function startTransaction() {
if ($this->transactionStarted) {
throw new Exception('transaction already running');
} else {
$this->connection->autocommit(false);
$this->transactionStarted = true;
register_shutdown_function(array($this, 'shutdownCheck'));
}
}
public function shutdownCheck() {
if ($this->transactionStarted) {
$this->rollbackTransaction();
}
}
public function commitTransaction() {
// Commit pending queries.
$this->connection->commit();
// The next line both commits queries in queue (the transaction) and turns autocommit back on.
// Note that with the addition of commit() it no longer commits any pending queries (as there are none).
$this->connection->autocommit(true);
$this->transactionStarted = false;
}
public function rollbackTransaction($stopTransaction=true) {
$this->connection->rollback();
if ($stopTransaction) {
// Afterwards, turn back on autocommitting.
// It is up to the user to decide whether (s)he wants to continue after a rollback though...
$this->connection->autocommit(true);
$this->transactionStarted = false;
}
}
public function insertId() {
return $this->connection->insert_id;
}
public function affectedRows() {
return $this->connection->affected_rows;
}
public function getQueryCount() {
return self::$queryCount;
}
}
?>
Database Result MySQLi klasse
<?php
class Database_Result_MySQLi extends MySQLi_Result implements Database_Result
{
// Returns an associative array.
public function fetchRow() {
return $this->fetch_assoc();
}
// Returns a single value (first column of current row), for COUNT queries and such.
// If it is not guaranteed a query has (at least) one result, YOU need to check for this yourself.
// Here we assume the query does have at least one result.
// return $this->fetch_row()[0] seems slightly faster, requires PHP 5.4 or later
// @see https://wiki.php.net/rfc/functionarraydereferencing (FAD)
public function fetchValue() {
$row = $this->fetch_row();
return $row[0];
}
public function numRows() {
return $this->num_rows;
}
public function dataSeek($offset=0) {
return $this->data_seek($offset);
}
public function freeResult() {
$this->free();
}
}
?>
De toegevoegde waarde is vervolgens ook de hoeveelheid code die je (minder) moet kloppen bij gebruikmaking van deze klasses, de definities zijn korter (en regelen soms meer dingen tegelijkertijd op de achtergrond) dan wanneer je dit elke keer helemaal uitschrijft.
Dit is mijn "wiel", veel plezier met het maken van je eigen wiel.