<?php
/**
Pork.dObject version 1.0
By Jelle Ursem
see http://www.schizofreend.nl/ for more info
*/
define('RELATION_SINGLE', 'RELATION_SINGLE');
define('RELATION_FOREIGN', 'RELATION_FOREIGN');
define('RELATION_MANY', 'RELATION_MANY');
define('RELATION_NOT_RECOGNIZED', 'RELATION_NOT_RECOGNIZED');
define('RELATION_NOT_ANALYZED', 'RELATION_NOT_ANALYZED');
class dbObject
{
var $databaseInfo, $databaseValues, $changedValues, $relations, $db;
/**
This initializes the O/R mapper and sets up the connection. This is what you will call in the constructor of your object.
example:
class Blog extends dbObject
{
function __construct($ID=false)
{
$this->__setupDatabase('blogs', // db tabel
array('ID_Blog' => 'ID', // db veld => object property
'strPost' => 'Post',
'datPosted' => 'Posted',
'strPoster' => 'Poster',
'strTitle' => 'Title'),
'ID_Blog', // primary db key
$ID); // primary key value
$this->addRelation('Reaction');
$this->addRelation('Tag', 'Blogtag');
}
*/
function __setupDatabase($table, $fields, $primarykey, $id=false, $altDatabase=false)
{
global $db;
$this->databaseInfo->table = $table;
$this->databaseInfo->fields = $fields;
$this->databaseInfo->primary = $primarykey;
$this->databaseInfo->ID = $id;
$this->databaseValues = false;
$this->changedValues = array();
$this->relations= array();
$this->db = ($altDatabase != false)? $altDatabase : $db;
if($id != false) $this->__init();
}
function __init()
{
if($this->databaseInfo->ID != false)
{
$fieldnames = implode(",", array_keys($this->databaseInfo->fields));
$input = $this->db->fetchRow("select {$fieldnames} from {$this->databaseInfo->table} where {$this->databaseInfo->primary} = {$this->databaseInfo->ID}", 'mysql_fetch_assoc');
$this->databaseValues = $input;
}
}
function __get($property)
{
return $this->__getInternal($property);
}
function __getInternal($property, $isProperty=true)
{
$field = false; //
if(array_key_exists($property, get_object_vars($this))) { // it's private property
return($this->$property);
}
if($isProperty) { // are we calling the 'mapped' way?
$field = $this->fieldForProperty($property);
}
else {
$field = (is_array($this->databaseInfo->fields) && array_key_exists($property, $this->databaseInfo->fields)) ? $property : false;
}
if($field != false && is_array($this->changedValues) && array_key_exists($field, $this->changedValues)) { // is this an udated property?
return($this->changedValues[$field]); // get it from internal changed values list
}
if($field != false && is_array($this->databaseValues) && array_key_exists($field, $this->databaseValues)) {
return($this->databaseValues[$field]); // return the original value from the database
}
if($field == false)
{
// It's an unmapped property, you could die() here
// The framework will throw an excerpt of a debug_backtrace() in a popup
// or return nothing
return;
}
}
function __set($property, $value) // catch the default setter
{
if($this->hasProperty($property)) {
$this->changedValues[$this->fieldForProperty($property)] = $value;
}
else
{
// It's an unmapped property, you could die() here
// The framework will throw an error in a popup
// in this case we do nothing.
}
}
function getFieldNames() { // gets the array of database fields
return(array_values($this->databaseInfo->fields));
}
function hasProperty($property) {
return array_search($property, $this->databaseInfo->fields);
}
function fieldForProperty($property) { // get db field by it's property name
return array_search($property, $this->databaseInfo->fields);
}
function deleteYourSelf() { //deletes the current object from database.
if($this->databaseInfo->ID != false)
{
$this->db->query("delete from {$this->databaseInfo->table} where {$this->databaseInfo->primary} = {$this->databaseInfo->ID}");
}
}
/**
Insert this object into the database:
* prepare the query with just a null value for primary key
* append the changed fields and (addslash'd)values of this object if needed
* execute the query
*/
function InsertNew()
{
$insertfields = $this->databaseInfo->primary;
$insertValues = 'null';
if ($this->changedValues != false) { // do we have any new-set values?
$insertfields .= ', '.implode(",", array_keys($this->changedValues));
foreach ($this->changedValues as $property=>$value) { // append each value escaped to the query
$insertValues .= ', "'.addslashes($value).'"';
$this->databaseValues[$property] = $value; // and store it so we don't save it again
}
$this->changedValues = false; // then clear the changedValues
}
$this->databaseInfo->ID = $this->db->query("insert into {$this->databaseInfo->table} ({$insertfields}) values ($insertValues);");
$this->databaseValues[$this->databaseInfo->primary] = $this->databaseInfo->ID; // update the primary key
return($this->databaseInfo->ID); // and return it
}
function Save()
{
if($this->changedValues != false && $this->databaseInfo->ID == false) { // it's a new record for the db
$id = $this->InsertNew();
$this->analyzeRelations(); // re-analyze the relation types so we can use Find()
return $id;
}
elseif ($this->changedValues != false) { // otherwise just build the update query
$updateQuery = "";
foreach ($this->changedValues as $property=>$value) {
$updateQuery .= ($updateQuery != "") ? ", " : "";
$updateQuery .=" {$property} = '".addslashes($value)."'";
$this->databaseValues[$property] = $value; // store the value so we don't have to save it again
}
$this->db->query("update {$this->databaseInfo->table} set {$updateQuery} where {$this->databaseInfo->primary} = {$this->databaseInfo->ID}");
$this->changedValues = false;
return($this->databaseInfo->ID);
}
return false;
}
/**
Add a new relation to the relation list and set it to be analyzed if used
*/
function addRelation($classname, $connectorclassname=false)
{
$this->relations[$classname]->relationType = RELATION_NOT_ANALYZED;
if($connectorclassname != false) $this->relations[$classname]->connectorClass = $connectorclassname;
if($this->databaseInfo->ID != false) $this->analyzeRelations();
}
/**
This is where the true magic happens
*/
function analyzeRelations()
{
foreach($this->relations as $classname=>$info) {
if(is_subclass_of($classname, 'dbObject')) {// the class to connect is a dbObject
$obj = new $classname(false);
$info->className = $classname;
if(array_key_exists('connectorClass', get_object_vars($info)) && $info->connectorClass != '' && is_subclass_of($info->connectorClass, 'dbObject')) {
// this class has a connector class. It could be many:many relation
$connector = $info->connectorClass;
$connectorobj = new $connector(false);
// if the
if(array_key_exists($this->databaseInfo->primary, $connectorobj->databaseInfo->fields) && array_key_exists($obj->databaseInfo->primary, $connectorobj->databaseInfo->fields)) {
// yes! The primary key of the relation now appears in this object, the connector class and one of the connected class. it's a many:many relation
$info->relationType = RELATION_MANY;
}
else {
unset($info->connectorClass); // it's not connected to our relations
}
}
if( $info->relationType == RELATION_NOT_ANALYZED &&
array_key_exists($obj->databaseInfo->primary, $this->databaseInfo->fields) && array_key_exists($this->databaseInfo->primary, $obj->databaseInfo->fields)) {
// if the primary key of the connected object exists in this object,
// and the primary key of this object exists in the connected object it's a 1:1 relation
$info->relationType = RELATION_SINGLE;
}
elseif($info->relationType == RELATION_NOT_ANALYZED
&& (array_key_exists($this->databaseInfo->primary, $obj->databaseInfo->fields) && !array_key_exists($obj->databaseInfo->primary, $this->databaseInfo->fields) || !array_key_exists($this->databaseInfo->primary, $obj->databaseInfo->fields) && array_key_exists($obj->databaseInfo->primary, $this->databaseInfo->fields)) ) {
// if the primary key of the connected object exists in this object (or the other way around), but
// the primary key of this object does not exist in the connected object (or the other way around)
// it's a many:1 or 1:many relation
$info->relationType = RELATION_FOREIGN;
}
elseif($info->relationType == RELATION_NOT_ANALYZED) {
// we don't recognize this type of relation. in the framework a popup error with a backtrace will be thrown
$info->relationType = RELATION_NOT_RECOGNIZED;
}
$this->relations[$classname] = $info;
}
else
{
// tried to connect a non-dbobject object. in the framework a popup error with a backtrace will be thrown
unset($this->relations[$classname]);
}
}
}
/*
This connects 2 dbObjects together, with a connector class if needed.
*/
function Connect($object)
{
$className = get_class($object);
if($this->databaseInfo->ID == false) $this->Save(); // save both objects if they are new
if($object instanceof dbObject && $object->databaseInfo->ID == false) $object->Save();
if($this->relations[$className]->relationType == RELATION_NOT_ANALYZED) $this->analyzeRelations(); // if we didn't run the analyzer yet, run it.
if(array_key_exists($className, $this->relations)) {
switch($this->relations[$className]->relationType)
{
case RELATION_SINGLE: // link the 2 objects' primary keys
$this->changedValues[$object->databaseInfo->primary] = $object->databaseInfo->ID;
$object->changedValues[$this->databaseInfo->primary] = $this->databaseInfo->ID;
$this->Save();
$object->Save(); //
break;
case RELATION_FOREIGN: // determine wich one needs to have the primary key set for the 1:many or many:one relation
if(array_key_exists($this->databaseInfo->primary, $object->databaseInfo->fields)) {
$object->changedValues[$this->databaseInfo->primary] = $this->databaseInfo->ID;
$object->Save();
}
elseif(array_key_exists($object->databaseInfo->primary, $this->databaseInfo->fields)) {
$this->changedValues[$object->databaseInfo->primary] = $object->databaseInfo->ID;
$this->Save();
}
break;
case RELATION_MANY: // create a new connector class, set both primary keys and save it.
$connector = $this->relations[$className]->connectorClass;
$connector = new $connector(false);
$property = $connector->databaseInfo->fields[$this->databaseInfo->primary];
$connector->$property = $this->databaseInfo->ID;
$property = $connector->databaseInfo->fields[$object->databaseInfo->primary];
$connector->$property = $object->databaseInfo->ID;
$connector->Save();
break;
default:
// the framework will throw a nice error here
break;
}
}
}
// see Connect() for how this works, it's the other way around.
function Disconnect($object, $id=false)
{
if(!$object && !$id) return;
if(!$object instanceof dbObject && $id != false) {
$object = new $object(false);
$object->databaseInfo->ID = $id;
}
$className = get_class($object);
if(array_key_exists($className, $this->relations)) {
switch($this->relations[$className]->relationType)
{
case RELATION_SINGLE:
$this->changedValues[$object->databaseInfo->primary] = '';
$object->changedValues[$this->databaseInfo->primary] = '';
$this->Save();
$object->Save();
break;
case RELATION_FOREIGN:
if(array_key_exists($this->databaseInfo->primary, $object->databaseInfo->fields)) {
$object->changedValues[$this->databaseInfo->primary] = '';
$object->Save();
}
elseif(array_key_exists($object->databaseInfo->primary, $this->databaseInfo->fields)) {
$this->changedValues[$object->databaseInfo->primary] = '';
$this->Save();
}
break;
case RELATION_MANY:
$connectors = $this->Find($this->relations[$className]->connectorClass, array($object->databaseInfo->primary => $object->databaseInfo->ID, $this->databaseInfo->primary => $this->databaseInfo->ID));
$connectors[0]->deleteYourSelf();
break;
}
}
}
/**
Checks if this is a 'connecting' object between 2 tables by checking if the passed classname
is a connection class.
*/
function isConnector($className)
{
foreach ($this->relations as $key => $val) { // walk all relations
if(array_key_exists('connectorClass', get_object_vars($val)) && $val->connectorClass == $className) return true;
}
return false;
}
function Import($values) { // import a pre-filled object (like a table row)
$this->databaseValues = $values;
$this->databaseInfo->ID = (!empty($values[$this->databaseInfo->primary])) ? $values[$this->databaseInfo->primary] :false;
}
/**
Imports an array of e.g. db rows and returns filled instances of $className
This will not run the analyzerelations or other stuff for performance and recursivity reasons.
*/
function importArray($className, $input)
{
$output = array();
foreach ($input as $array)
{
$elm = new $className(false);
$elm->Import($array);
if(sizeof($input) > 0 && $elm->ID != false) {
$output[]= $elm;
}
}
return($output);
}
/*
input: a query with mapped properties ($this->databaseInfo->fields)
output: a query with the corresponding table name prefixed to the actual table field names.
*/
function mapfields($query, $object)
{
$words = preg_split("/([\s|,]+)/", $query, -1, PREG_SPLIT_DELIM_CAPTURE);
if(!empty($words)) {
foreach($words as $key=>$val) {
if(strpos($val, '.') !== false) {
$expl = explode(".", $val);
if(sizeof($expl) == 2 && $expl[0] == $object->databaseInfo->table) $val = $expl[1];
else continue;
}
if($object->hasProperty($val)) {
$words[$key] = $object->databaseInfo->table.'.'.$object->fieldForProperty($val);
}
elseif(array_key_exists($val, $object->databaseInfo->fields)) {
$words[$key] = $object->databaseInfo->table.'.'.$val;
}
}
}
return(implode("", $words));
}
/**
This builds the where clause and eventual order and group by clauses.
*/
function buildQuery($filters=array(), $extra = array() )
{
$where = '';
$filtered = array();
if(!empty($filters)) {
foreach ($filters as $key => $val) { // walk the array of filters, and if there's a non-integer array key, assume where $key = $value. otherwise: assume it's a pre-built where clause: just append it.
$filtered[] = (is_int($key)) ? "{$val}" : "{$key} = '{$val}'";
}
$where = (!empty($filtered)) ? " where ".$where : $where;
$where .= implode(" AND ", $filtered);
}
$where .= (sizeof($extra) > 0) ? " ".implode(" ", $extra) : ""; //append order by, group by, etc.
return($where);
}
/**
More magic happens here. This determines what type of relation we're searching for and fetches connected objects with sql filtering options. examples available online, and if you can read SQL you know how this works.
*/
function Find($className, $filters=array(), $where=array(), $justThese=array())
{
if($className == get_class($this)) {
// this might look weird, but you can create an empty instance of an object, and fetch the objects you need though this function
if(sizeof($justThese) == 0) $justThese = array_keys($this->databaseInfo->fields);
foreach($justThese as $key=>$val){ $justThese[$key] = $this->databaseInfo->table.'.'.$val; } // prepend table name.
$fields = implode(", ", $justThese);
$query = $this->mapfields("select {$fields} from {$this->databaseInfo->table} ".$this->buildQuery($filters, $where),$this);
$input = $this->db->fetchAll($query, 'mysql_fetch_assoc');
return($this->importArray($className, $input));
}
if(array_key_exists($className, $this->relations) || $this->isConnector($className)) {
$ob = new $className(false);
$these = (sizeof($justThese) == 0) ? array_keys($ob->databaseInfo->fields) : $justThese;
foreach($these as $key=>$val){ $these[$key] = $ob->databaseInfo->table.'.'.$val; } // prepend table name.
// $justthese is a list of fields you want to fill
// (usefull if e.g. you don't want to fetch just the large textfields, but do want the other data)
$fields = implode(", ", $these);
switch($this->relations[$className]->relationType) {
case RELATION_SINGLE:
if($this->databaseInfo->ID != false) $filters[$ob->databaseInfo->primary] = $this->__getInternal($ob->databaseInfo->primary, false);
$input = $this->db->fetchAll($this->mapfields("select {$fields} from {$ob->databaseInfo->table} ".$this->buildQuery($filters, $where), $ob), 'mysql_fetch_assoc');
return($this->importArray($className, $input));
break;
case RELATION_FOREIGN:
if($this->databaseInfo->ID != false) $filters[$this->databaseInfo->table.'.'.$this->databaseInfo->primary] = $this->databaseInfo->ID;
if(array_key_exists($this->databaseInfo->primary, $ob->databaseInfo->fields))
{
$input = $this->db->fetchAll($this->mapfields("select {$fields} from {$this->databaseInfo->table} left join {$ob->databaseInfo->table} on {$ob->databaseInfo->table}.{$this->databaseInfo->primary} = {$this->databaseInfo->table}.{$this->databaseInfo->primary} ".$this->buildQuery($filters, $where),$this), 'mysql_fetch_assoc');
}
if(array_key_exists($ob->databaseInfo->primary, $this->databaseInfo->fields))
{
$input = $this->db->fetchAll($this->mapfields("select {$fields} from {$this->databaseInfo->table} left join {$ob->databaseInfo->table} on {$this ->databaseInfo->table}.{$ob->databaseInfo->primary} = {$ob->databaseInfo->table}.{$ob->databaseInfo->primary} ".$this->buildQuery($filters, $where),$ob), 'mysql_fetch_assoc');
}
return($this->importArray($className, $input));
break;
case RELATION_MANY:
$connectorClass = $this->relations[$className]->connectorClass;
$conn = new $connectorClass(false);
if($this->databaseInfo->ID != false) $filters[$conn->databaseInfo->table.'.'.$this->databaseInfo->primary] = $this->databaseInfo->ID;
$input = $this->db->fetchAll($this->mapfields($this->mapfields("select {$fields} from {$conn->databaseInfo->table} left join {$ob->databaseInfo->table} on {$conn->databaseInfo->table}.{$ob->databaseInfo->primary} = {$ob->databaseInfo->table}.{$ob->databaseInfo->primary} ".$this->buildQuery($filters, $where), $conn), $ob), 'mysql_fetch_assoc');
return($this->importArray($className, $input));
break;
case RELATION_NOT_ANALYZED:
if(sizeof($this->changedValues) > 0) $this->Save();
$this->analyzeRelations();
return($this->Find($className, $filters, $where, $justThese));
break;
default:
if($this->isConnector($className)) {
$input = $this->db->fetchAll($this->mapfields("select {$fields} from {$ob->databaseInfo->table} ".$this->buildQuery($filters, $where), $ob), 'mysql_fetch_assoc');
return($this->importArray($className, $input));
}
break;
}
}
}
/**
// try to save the object if changed.
*/
function __destruct()
{
$this->Save();
}
}
?>
|