DataObjects
Ik ben lui, en ik denk dat er meer met mij zijn. Ik wil het liefst gewoon een object manipuleren en voor de rest niet omkijken naar inhoud escapen, datatypen controleren enz. enz. Dus ik 'bedacht' dataobjecten. Deze abstracte klassen. En om het leven nog wat makkelijker te maken, zijn zowel de DataObjecten als de DataSets zo gebakken dat zij voldoen aan de voorwaarden die de interface Iterator stelt. Hierdoor kan je ze probleemloos in een foreach-lus gebruiken! Iedere klasse vertegenwoordigt een tabel in je database. Je zal in iedere klasse dus de structuur van de tabel moeten opgeven. Heel belangrijk : Roep voor het gebruik van de objecten DataSet::buffer() aan, eventueel met het aantal objecten dat je van te voren wilt vullen als parameter. Dit zorgt ervoor dat hij al die objecten in 1 keer voltankt, en niet per object een query uitvoert. Maar genoeg gepraat, een voorbeeld is vast begrijpbaarder: Inserten is ook vrij simpel: gewoon je decorator lief invullen Er zullen nog wel wat meer features inzitten, die ik zelf zelfs al vergeten ben. Ik gebruik hem gewoon, en het werkt. En zo hoort het :) Mocht je vragen of problemen hebben, maak een topic in het forum en post hier een link in de reacties naar het topic. Zo houden we de reacties schoon.
De toegangspoort: een echte klassieke Database-klasse
<?php
class DatabaseException extends Exception {
public function __construct($link, $message, $sql = '') {
$this->sql = $sql;
$this->errmsg = mysql_error($link);
parent::__construct($message, mysql_errno($link));
}
public function getErrorMessage() {
return $this->errmsg;
}
public function getSQL() {
return $this->sql;
}
}
class Database {
const FETCH_ALL = 2;
private $handle;
static private $queries = array();
/* wat debug-functies, altijd handig */
static public function getQueryCount() {
return count(self::$queries);
}
static public function getQueries() {
return self::$queries;
}
public function __construct($host, $username, $password, $database) {
if(!($this->handle = mysql_connect($host, $username, $password))) {
throw new DatabaseException($this->handle, 'Ik heb een kuthost die even niet zo vriendelijk is om een databaseverbinding voor mijn site beschikbaar te houden! :@');
}
if(!mysql_select_db($database, $this->handle)) {
throw new DatabaseException($this->handle, 'Could not select Database');
}
}
public function query($sql) {
if(!($result = mysql_query($sql, $this->handle))) {
throw new DatabaseException($this->handle, 'Error while executing query', $sql);
} else {
self::$queries[] = $sql . '<br>hits: ' . @mysql_num_rows($result) . '/' . @mysql_affected_rows($result);
return $result;
}
}
/* vooral gebruikt in self::prepareSQL */
public function quote($value) {
$specialValues = array(
'NOW()', 'TRUE', 'FALSE'
);
if (get_magic_quotes_gpc()) {
$value = stripslashes($value);
}
if(!in_array($value, $specialValues) && !is_int($value)) {
$value = "'" . mysql_real_escape_string($value) . "'";
}
return $value;
}
/*
* Verantwoordelijk voor de DatabaseObjecten.
* (str) $decorator: naam van de decorator-klasse
* -- (moet afstammen van AbstractDataObject)
* (str) $sql: het gedeelte dat na "WHERE" komt in een query
* (arr) $args: Gaat samen met $sql naar self::prepareSQL()
*/
public function fetch($decorator, $sql = null, $args = array()) {
if($sql == null || $sql == Database::FETCH_ALL) {
$sql = '1';
}
$dummyInstance = new $decorator;
$refl_primaryKey = new ReflectionProperty($decorator, 'primaryKey');
$primaryKey = $refl_primaryKey->getValue($dummyInstance);
$refl_primaryKey = null;
$refl_tableName = new ReflectionProperty($decorator, 'tableName');
$tableName = $refl_tableName->getValue($dummyInstance);
$refl_tableName = null;
$sql = $this->prepareSQL('SELECT ' . $primaryKey . ' FROM ' . $tableName . ' WHERE ' . $sql, $args);
$resultData = array_map('intval',$this->fetchCol($primaryKey, $sql));
$dataset = new DataSet($resultData);
$dataset->setPrimaryKey($primaryKey);
$dataset->setTableName($tableName);
$dataset->setDatabaseInstance($this);
$dataset->setDecorator($decorator);
return $dataset;
}
public function fetchAll($sql, $args = null) {
$sql = $this->prepareSQL($sql, $args);
$result = $this->query($sql);
$buffer = array();
while($row = mysql_fetch_assoc($result)) {
$buffer[] = $row;
}
return $buffer;
}
public function fetchRow($sql, $args = null) {
$sql = $this->prepareSQL($sql, $args);
$result = $this->query($sql);
return mysql_fetch_assoc($result);
}
public function fetchCol($field, $sql, $args = null) {
$sql = $this->prepareSQL($sql, $args);
$result = $this->query($sql);
$rows = mysql_num_rows($result);
$buffer = array();
for($i = 0; $i < $rows; $i++) {
$buffer[] = mysql_result($result, $i, $field);
}
return $buffer;
}
public function insert($table, $fields) {
$buffer = array();
foreach($fields as $key => $value) {
$buffer[] = $key . '=' . $this->quote($value);
}
return $this->query('INSERT INTO ' . $table .' SET ' . implode(', ', $buffer));
}
public function update($table, $fields, $where = null) {
$buffer = array();
foreach($fields as $key => $value) {
$buffer[] = $key . '=' . $this->quote($value);
}
$sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $buffer);
if(!is_null($where)) {
$sql .= ' WHERE ' . $where;
}
$this->query($sql);
return mysql_affected_rows($this->handle) > 0;
}
public function insertId() {
return mysql_insert_id($this->handle);
}
/*
* Verantwoordelijk voor het veilig maken van database-voer.
* Gebruik in $sql :varname (let op dubbele punt!) en in $args
* als key weer 'varname' en als waarde dat waar het door moet
* worden vervangen.
*/
public function prepareSQL($sql, $args = null) {
if(!is_null($args)) {
foreach($args as $key => $value) {
$sql = str_replace(':'.$key . '', $this->quote($value), $sql);
}
}
return $sql;
}
}
?>
Deze is nodig voor DataSet, en maakt het onder andere mogelijk om foreach te gebruiken. Implementeert de interface Iterator, die teruggevonden kan worden in SPL.
<?php
abstract class AbstractIterator implements SeekableIterator, Countable
{
protected $position;
public function current()
{
if($this->hasIndexAt) {
return $this->__getAtIndex($this->position);
} else {
return $this->stack[$this->position];
}
}
public function key()
{
return $this->position + 1;
}
public function next()
{
$this->position++;
}
/* niet onderdeel van de officiele implementatie */
public function previous()
{
$this->position--;
}
public function rewind()
{
$this->position = 0;
}
/* niet onderdeel van de officiele implementatie */
public function end()
{
$this->position = count($this->stack) - 1;
}
public function reset()
{
$this->rewind();
$this->hasIndexAt = is_callable(array($this, '__getAtIndex'));
}
/* implementeert SeekableIterator */
public function seek($position)
{
if($position > $this->count()) {
throw new Exception('This DataSet has only ' . $this->count() . ' elements in stack. It is not posible to go to index ' . $position);
}
$this->position = $position;
}
/*
* implementeert Countable.
* nu kan je count($dirObject) gebruiken!
*/
public function valid()
{
return isset($this->stack[$this->position]);
}
public function count()
{
return count($this->stack);
}
}
?>
DataSet: de 'array' met alle DataObjecten, maar niet met de daadwerkelijke data. Hier worden tevens de DataObjecten gebakken en eventueel van inhoud voorzien. Vergeet niet buffer() op de juiste momenten aan te roepen! Dat komt de snelheid zeker ten goede!
<?php
class DataSet extends AbstractIterator
{
const ALL = -1;
private $databaseInstance;
private $decorator;
protected $tableName;
protected $primaryKey;
public function __construct($data = false)
{
$this->stack = is_array($data) ? $data : array();
$this->reset();
}
protected function __getAtIndex($index)
{
$data = $this->stack[$index];
if(!$data) {
return false;
}
if(!is_object($data)) {
return new $this->decorator($data, $this->databaseInstance);
} else {
return $data;
}
}
public function setDecorator($decorator)
{
$this->decorator = $decorator;
}
public function setDatabaseInstance(Database $instance)
{
$this->databaseInstance = $instance;
}
public function setPrimaryKey($key)
{
$this->primaryKey = $key;
}
public function setTableName($tableName)
{
$this->tableName = $tableName;
}
/* some filling-functions */
public function buffer($length = false, $from = false)
{
if($this->count() < 1) {
return;
}
if(!$from) {
$from = $this->position + 1;
}
if(!$length) {
$length = $this->count() - $from;
}
$ids = array();
$items = array();
$break = $from + $length;
for($i = $from; $i < $break; $i++) {
if(!isset($this->stack[$i])) break; /* end of stack */
if(is_object($this->stack[$i])) continue; /* already in cache */
array_push($items, $i);
array_push($ids, $this->stack[$i]);
}
if(count($ids) <= 1) {
return;
}
$dummyInstance = new $this->decorator;
if(!$this->primaryKey) {
$refl_primaryKey = new ReflectionProperty($this->decorator, 'primaryKey');
$this->primaryKey = $refl_primaryKey->getValue($dummyInstance);
$refl_primaryKey = null;
}
if(!$this->tableName) {
$refl_tableName = new ReflectionProperty($this->decorator, 'tableName');
$this->tableName = $refl_tableName->getValue($dummyInstance);
$refl_tableName = null;
}
$results = $this->databaseInstance->fetchAll('SELECT * FROM ' . $this->tableName . ' WHERE ' . $this->primaryKey . ' IN(' . join(', ', $ids) . ')');
foreach($results as $result) {
$index = array_search(intval($result[$this->primaryKey]), $ids);
$entryObject = new $this->decorator();
$entryObject->setRawData($result);
$entryObject->setDatabaseInstance($this->databaseInstance);
$this->stack[$items[$index]] = $entryObject;
}
}
public function push($item)
{
return array_push($this->stack, $item);
}
public function pop()
{
$data = array_pop($this->stack);
if(!$data) {
return false;
}
if(is_object($data)) {
return $data;
} else {
return new $this->decorator($data, $this->databaseInstance);
}
}
}
?>
Het abstracte DataObject. Werkt natuurlijk niet stand-alone, simpelweg omdat hij niet weet hoe zijn MySQL-tabel eruit ziet. Deze moet je dus extenden. En als bonus: ook hier een iterator! Ook hier kan je foreach op los laten! Nooit meer je aliassen onthouden! Wat is programmeren toch een heerlijk lui en simpel gebeuren ;)
<?php
abstract class AbstractDataObject implements Iterator {
const READ_ONLY = 2;
const STRING = 3;
const INT = 4;
const FLOAT = 5;
private $data = array();
private $databaseInstance;
private $_isNew = true;
private $iteratorPosition = -1;
private $iteratorKeys = array();
/*
* Te gebruiken in de kinderen van deze klasse.
* Haalt gegevens uit de db en vult zichzelf ermee.
*/
protected function populate($conditions, $parameters = array())
{
if(!$this->databaseInstance) {
/*
* Deze mag je lekker zelf implementeren.
* Een hint: Registery Pattern
*
* $this->databaseInstance = Base::acquire('database');
*/
throw new Exception('No databaseInstance available');
}
$selectedFields = array();
$temporaryAliasses = array();
$i = 0;
foreach($this->tableStructure as $alias => $column) {
$tempAlias = 'alias'.$i++;
$temporaryAliasses[$tempAlias] = $column[0];
$selectedFields[] = $column[0] . ' as ' . $tempAlias;
}
$data = $this->databaseInstance->fetchRow('SELECT ' . implode(', ', $selectedFields) . ' FROM ' . $this->tableName . ' WHERE '.$conditions.' LIMIT 1', $parameters);
if(!$data) {
return false;
}
foreach($data as $tempAlias => $value) {
$this->data[$temporaryAliasses[$tempAlias]] = $value;
}
$this->_isNew = false;
return true;
}
public function setDatabaseInstance($databaseInstance)
{
$this->databaseInstance = $databaseInstance;
}
/*
* Voor de db masterclass, om direct alle gegevens erin te pushen.
* Dit is in sommige gevallen efficienter
*/
public function setRawData($data)
{
foreach($data as $key => $value) {
$this->data[$key] = $value;
}
$this->_isNew = false;
}
/*
* magic: indien functie $this->set{$Alias} beschikbaar is,
* laat die het opslaan afhandele, anders kijken of we de waarde toevallig
* in de tablestructure moet komen, en of dat wel mag (READ_ONLY)
*/
public function __set($alias, $value) {
$functionName = 'set'.ucfirst($alias);
if(is_callable(array($this, $functionName))) {
$value = call_user_func_array(array($this, $functionName), array($this->data, $value));
if(!$value) {
return false;
}
} else {
$key = $this->translate($alias);
if(in_array(self::READ_ONLY, $this->tableStructure[$alias])) {
throw new Exception("Field $alias (better known as $key) is marked as read only.");
}
$this->data[$key] = strval($this->convert($alias, $value));
return true;
}
}
/*
* magic: indien functie $this->get{$Alias} beschikbaar is,
* raadpleeg die, anders kijken of we de waarde toevallig
* in de tablestructure hebben zitten.
*/
public function __get($alias) {
$functionName = 'get'.ucfirst($alias);
if(is_callable(array($this, $functionName))) {
return call_user_func_array(array($this, $functionName), array($this->data));
} else {
return $this->convert($alias, $this->data[$this->translate($alias)]);
}
}
/* Wijziginen opslaan */
public function commit() {
if(!$this->databaseInstance) {
/*
* Voor deze geldt hetzelfde
* $this->databaseInstance = Base::acquire('database');
*/
throw new Exception('No databaseInstance available');
}
if(empty($this->primaryKey)) {
throw new Exception("No primary key found in table $this->tableName. A primary key is required to use this method.");
}
if(!empty($this->data[$this->primaryKey])) {
return $this->databaseInstance->update($this->tableName, $this->data, $this->databaseInstance->prepareSQL($this->primaryKey . ' = :value', array('value' => intval($this->data[$this->primaryKey]))));
} else {
if($this->databaseInstance->insert($this->tableName, $this->data)) {
$this->data[$this->primaryKey] = $this->databaseInstance->insertId();
return true;
} else {
return false;
}
}
}
/*
* Van alias (key in tablestructure) naar
* veldnaam van db-tabel.
*/
private function translate($key)
{
if(isset($this->tableStructure[$key])) {
return $this->tableStructure[$key][0];
}
throw new Exception("Alias $key not found in \$tableStructure");
}
/*
* Alles is een String waneer het uit de DB komt,
* dus wij maken er een echte waarde van!
*/
private function convert($key, $value)
{
if(isset($this->tableStructure[$key])) {
$fieldSettings = $this->tableStructure[$key];
switch(true) {
case in_array(self::INT, $fieldSettings):
return intval($value);
case in_array(self::FLOAT, $fieldSettings):
return floatval($value);
case in_array(self::STRING, $fieldSettings):
if(is_callable(array($value, '__toString'))) {
return strval($value->__toString());
} else {
return strval($value);
}
default:
return $value;
}
} else {
return $value;
}
}
/*
* callback functie, $object->isNew
* reden: read-only
*/
public function getIsNew() {
return $this->_isNew;
}
/* Iterator implementatie */
public function current()
{
return $this->__get($this->key());
}
public function key()
{
$keys = array_keys($this->tableStructure);
return $keys[$this->iteratorPosition];
}
public function next()
{
$this->iteratorPosition++;
}
public function rewind()
{
$this->iteratorPosition = 0;
}
public function valid()
{
return $this->iteratorPosition < count($this->tableStructure);
}
}
?>
Reacties
0