Source for file _dataobject.class.php
Documentation is available at _dataobject.class.php
* This file implements the abstract DataObject base class.
* This file is part of Quam Plures - {@link http://quamplures.net/}
* See also {@link https://launchpad.net/quam-plures}.
* @copyright (c) 2009 - 2011 by the Quam Plures developers - {@link http://quamplures.net/}
* @copyright (c)2003-2009 by Francois PLANQUE - {@link http://fplanque.net/}
* Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
* Parts of this file are copyright (c)2005-2006 by PROGIDISTRI - {@link http://progidistri.com/}.
* {@internal License choice
* - If you have received this file as part of a package, please find the license.txt file in
* the same folder or the closest folder above for complete license terms.
* - If you have received this file individually (e-g: from http://evocms.cvs.sourceforge.net/)
* then you must choose one of the following licenses before using the file:
* - GNU General Public License 2 (GPL) - http://www.opensource.org/licenses/gpl-license.php
* - Mozilla Public License 1.1 (MPL) - http://www.opensource.org/licenses/mozilla1.1.php
* {@internal Open Source relicensing agreement:
* Daniel HAHLER grants Francois PLANQUE the right to license
* Daniel HAHLER's contributions to this file and the b2evolution project
* under any OSI approved OSS license (http://www.opensource.org/licenses/).
* PROGIDISTRI S.A.S. grants Francois PLANQUE the right to license
* PROGIDISTRI S.A.S.'s contributions to this file and the b2evolution project
* under any OSI approved OSS license (http://www.opensource.org/licenses/).
* {@internal Below is a list of authors who have contributed to design/coding of this file: }}
* @author fplanque: Francois PLANQUE
* @author blueyed: Daniel HAHLER
* @author mbruneau: Marc BRUNEAU / PROGIDISTRI
if( !defined('QP_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
* This is typically an abstract class, useful only when derived.
* Unique ID of object in database
* Please use get/set functions to read or write this param
var $ID = 0; // This will be the ID in the DB
var $dbchanges = array();
* Relations that may restrict deletion.
* Relations that will cascade deletion.
* @param string Name of table in database
* @param string Prefix of fields in the table
* @param string Name of the ID field (including prefix)
* @param string datetime field name
* @param string datetime field name
* @param string User ID field name
* @param string User ID field name
function DataObject( $tablename, $prefix = '', $dbIDname = 'ID', $datecreated_field = '', $datemodified_field = '', $creator_field = '', $lasteditor_field = '' )
$this->dbtablename = $tablename;
$this->dbprefix = $prefix;
$this->dbIDname = $dbIDname;
$this->datecreated_field = $datecreated_field;
$this->datemodified_field = $datemodified_field;
$this->creator_field = $creator_field;
$this->lasteditor_field = $lasteditor_field;
* Records a change that will need to be updated in the db
* @param string Name of parameter
* @param string DB field type ('string', 'number', 'date' )
* @param mixed Pointer to value of parameter - dh> pointer? So it should be a reference? Would make sense IMHO anyway.. fp> I just wonder why it's not already a reference... :@
function dbchange( $dbfieldname, $dbfieldtype, $valuepointer ) // TODO: dh> value by reference? see above..
$this->dbchanges[$dbfieldname]['type'] = $dbfieldtype;
$this->dbchanges[$dbfieldname]['value'] = $valuepointer ;
* Update the DB based on previously recorded changes
* @param boolean do we want to auto track the mod date?
* @return boolean true on success, false on failure to update, NULL if no update necessary
function dbupdate( $auto_track_modification = true )
if( $this->ID == 0 ) { debug_die( 'New object cannot be updated!' ); }
if( count( $this->dbchanges ) == 0 )
return NULL; // No changes!
if( $auto_track_modification )
{ // We wnat to track modification date and author automatically:
if( !empty($this->datemodified_field) )
{ // We want to track modification date:
$this->set_param( $this->datemodified_field, 'date', date('Y-m-d H:i:s',$localtimenow) );
if( !empty($this->lasteditor_field) && is_object($current_User) )
{ // We want to track last editor:
// TODO: the current_User is not necessarily the last editor. Item::dbupdate() gets called after incrementing the view for example!
// fplanque: this should be handled by set() deciding wether the setting changes the last editor or not
$this->set_param( $this->lasteditor_field, 'number', $current_User->ID );
foreach( $this->dbchanges as $loop_dbfieldname => $loop_dbchange )
// Get changed value (we use eval() to allow constructs like $loop_dbchange['value'] = 'Group->get(\'ID\')'):
eval ( '$loop_value = $this->'. $loop_dbchange['value']. ';' );
// Prepare matching statement:
$sql_changes[] = $loop_dbfieldname. ' = NULL ';
switch( $loop_dbchange['type'] )
$sql_changes[] = $loop_dbfieldname. " = '". $DB->escape( $loop_value ). "' ";
$sql_changes[] = $loop_dbfieldname. " = ". $DB->null($loop_value). ' ';
// Prepare full statement:
$sql = "UPDATE $this->dbtablename SET ". implode( ', ', $sql_changes ). "
WHERE $this->dbIDname = $this->ID";
if( ! $DB->query( $sql, 'DataObject::dbupdate()' ) )
// Reset changes in object:
$this->dbchanges = array();
* Insert object into DB based on previously recorded changes.
* @return boolean true on success
if( $this->ID != 0 ) debug_die( 'Existing object cannot be inserted!' );
if( !empty($this->datecreated_field) )
{ // We want to track creation date:
$this->set_param( $this->datecreated_field, 'date', date('Y-m-d H:i:s',$localtimenow) );
if( !empty($this->datemodified_field) )
{ // We want to track modification date:
$this->set_param( $this->datemodified_field, 'date', date('Y-m-d H:i:s',$localtimenow) );
if( !empty($this->creator_field) )
{ // We want to track creator:
if( empty($this->creator_user_ID) )
{ // No creator assigned yet, use current user:
$this->set_param( $this->creator_field, 'number', $current_User->ID );
if( !empty($this->lasteditor_field) )
{ // We want to track last editor:
if( empty($this->lastedit_user_ID) )
{ // No editor assigned yet, use current user:
$this->set_param( $this->lasteditor_field, 'number', $current_User->ID );
foreach( $this->dbchanges as $loop_dbfieldname => $loop_dbchange )
// Get changed value (we use eval() to allow constructs like $loop_dbchange['value'] = 'Group->get(\'ID\')'):
eval ( '$loop_value = $this->'. $loop_dbchange['value']. ';' );
// Prepare matching statement:
$sql_fields[] = $loop_dbfieldname;
switch( $loop_dbchange['type'] )
$sql_values[] = $DB->quote( $loop_value );
$sql_values[] = $DB->null( $loop_value );
// Prepare full statement:
$sql = "INSERT INTO {$this->dbtablename} ( ". implode( ', ', $sql_fields ). ") VALUES (". implode( ', ', $sql_values ). ")";
if( ! $DB->query( $sql, 'DataObject::dbinsert()' ) )
// store ID for newly created db record
$this->ID = $DB->insert_id;
// Reset changes in object:
$this->dbchanges = array();
* Inserts or Updates depending on object state.
* @return boolean true on success, false on failure
{ // Object not serialized yet, let's insert!
{ // Object already serialized, let's update!
* @return boolean true on success
global $DB, $Messages, $app_db_config;
if( $this->ID == 0 ) { debug_die( 'Non persistant object cannot be deleted!' ); }
{ // The are cascading deletes to be performed
if( !isset( $app_db_config['aliases'][$restriction['table']] ) )
{ // We have no declaration for this table, we consider we don't deal with this table in this app:
DELETE FROM '. $restriction['table']. '
WHERE '. $restriction['fk']. ' = '. $this->ID,
// Delete this (main/parent) object:
DELETE FROM $this->dbtablename
WHERE $this->dbIDname = $this->ID",
{ // There were cascading deletes
// Just in case... remember this object has been deleted from DB!
* Check relations for restrictions or cascades
* @param string Which relation should be checked ('delete_restrictions' or 'delete_cascades')?
* @param string An array of foreign key checks to skip.
* @param array An array of callbacks used to display more information about a relation.
* - Array keys (string): Foreign key this callback should apply to.
* - Array values: Arrays with the following keys:
* - 'cb' (callback): Callback to call. Should take one array argument, which
* will contain the following keys:
* - 'fk': The foreign key.
* - 'table': The SQL table for this relation.
* - 'msg': A format string -- a base message to be displayed.
* It normally shows the number of results.
* - 'id': The ID of this object.
function check_relations( $what, $ignore = array(), $verbose_callbacks = array() )
foreach( $this->$what as $restriction )
if( in_array( $restriction['fk'], $ignore ) )
{ // Skip this relation check.
if( ! isset( $verbose_callbacks[$restriction['fk']] ) )
{ // We do not want to display detailed info, just the result count:
FROM '. $restriction['table']. '
WHERE '. $restriction['fk']. ' = '. $this->ID,
0, 0, 'restriction/cascade check' );
$Messages->add( sprintf( $restriction['msg'], $count ), 'restrict' );
{ // We want verbose information.
// We just will call the callback, providing some
// information about this object:
call_user_func( $verbose_callbacks[$restriction['fk']]['cb'],
array_merge( $restriction, array(
* Check relations for restrictions before deleting
* @param array list of foreign keys to ignore
* @return boolean true if no restriction prevents deletion
function check_delete( $restrict_title, $ignore = array() )
if( $Messages->count('restrict') )
{ // There are restrictions:
'container' => $restrict_title,
'restrict' => T_('The following relations prevent deletion:')
$Messages->foot = T_('Please delete related objects before you proceed.');
return false; // Can't delete
return true; // can delete
* Displays form to confirm deletion of this object
* @param string Title for confirmation
* @param string "action" param value to use (hidden field)
* @param array Hidden keys (apart from "action")
* @param string most of the time we don't need a cancel action since we'll want to return to the default display
function confirm_delete( $confirm_title, $delete_action, $hiddens, $cancel_action = NULL )
$block_item_Widget = new Widget( 'block_item' );
$block_item_Widget->title = $confirm_title;
$block_item_Widget->disp_template_replaced( 'block_start' );
if( $Messages->count('restrict') )
{ // The will be cascading deletes, issue WARNING:
echo '<h3>'.T_('WARNING: Deleting this object will also delete:').'</h3>';
$Messages->display( '', '', true, 'restrict', NULL, NULL, NULL );
echo '<p class="warning">'.$confirm_title.'</p>';
echo '<p class="warning">'.T_('THIS CANNOT BE UNDONE!').'</p>';
$Form = new Form( '', 'form_confirm', 'get', '' );
$Form->begin_form( 'inline' );
$Form->hiddens_by_key( $hiddens );
$Form->hidden( 'action', $delete_action );
$Form->hidden( 'confirm', 1 );
$Form->button( array( 'submit', '', T_('I am sure!'), 'DeleteButton' ) );
$Form = new Form( '', 'form_cancel', 'get', '' );
$Form->begin_form( 'inline' );
$Form->hiddens_by_key( $hiddens );
if( !empty( $cancel_action ) )
$Form->hidden( 'action', $cancel_action );
$Form->button( array( 'submit', '', T_('CANCEL'), 'CancelButton' ) );
$block_item_Widget->disp_template_replaced( 'block_end' );
* Get a member param by its name
* @param mixed Name of parameter
* @return mixed Value of parameter
* Get a ready-to-display member param by its name
* Same as disp but don't echo
* @param string Name of parameter
* @param string Output format, see {@link format_to_output()}
function dget( $parname, $format = 'htmlbody' )
// Note: we call get again because of derived objects specific handlers !
return format_to_output( $this->get($parname), $format );
* Display a member param by its name
* @param string Name of parameter
* @param string Output format, see {@link format_to_output()}
function disp( $parname, $format = 'htmlbody' )
// Note: we call get again because of derived objects specific handlers !
echo format_to_output( $this->get($parname), $format );
* By default, all values will be considered strings
* @param string parameter name
* @param mixed parameter value
* @param boolean true to set to NULL if empty value
* @return boolean true, if a value has been set; false if it has not changed
function set( $parname, $parvalue, $make_null = false )
return $this->set_param( $parname, 'string', $parvalue, $make_null );
* @param string Name of parameter
* @param string DB field type ('string', 'number', 'date' )
* @param mixed Value of parameter
* @param boolean true to set to NULL if empty string value
* @return boolean true, if value has been set/changed, false if not.
function set_param( $parname, $fieldtype, $parvalue, $make_null = false )
$dbfield = $this->dbprefix. $parname;
// fplanque: Note: I am changing the "make NULL" test to differentiate between 0 and NULL .
// There might be side effects. In this case it would be better to fix them before coming here.
// i-e: transform 0 to ''
$new_value = ($make_null && ($parvalue === '')) ? NULL : $parvalue;
/* Tblue> Problem: All class member variables originating from the
* DB are strings (unless they were NULL in the DB,
* then they are set to NULL by the PHP MySQL
* If we pass an integer or a double to this function,
* the corresponding member variable gets changed
* on every call, because its type is 'string' and
* we compare using the === operator. Using the
* == operator would be a bad idea, though, because
* somebody could pass a NULL value to this function.
* If the member variable then is set to 0, then
* 0 equals NULL and the member variable does not
* Thus, using the === operator is correct.
* Solution: If $fieldtype is 'number' and the type of the
* passed value is either integer or double, we
* convert it to a string (no data loss). The
* member variable and the passed value can then
* be correctly compared using the === operator.
* fp> It would be nicer to convert numeric values to ints & floats at load time in class constructor x=(int)$y->value or sth.
* THIS IS EXPERIMENTAL! Feel free to revert if something does not
if( $fieldtype == 'number' && ( is_int( $new_value ) || is_float( $new_value ) ) )
settype( $new_value, 'string' );
if( !isset($this->$parname) )
{ // This property has never been set before, set it to NULL now in order for tests to work:
TODO: there's a bug here: you cannot use set_param('foo', 'number', 0), if the $parname member
has not been set before or is null!!
( isset($this->$parname) && $this->$parname === $new_value )
This would also eliminate the isset() check from above.
IIRC you've once said here that '===' would be too expensive and I would misuse the DataObjects,
but IMHO what we have now is not much faster and buggy anyway..
fp> okay let's give it a try...
if( (!is_null($new_value) && $this->$parname == $new_value)
|| (is_null($this->$parname) && is_null($new_value)) )
if( (isset($this->$parname) && $this->$parname === $new_value)
|| ( ! isset ($this->$parname) && ! isset ($new_value) ) )
{ // Value has not changed (we need 2 tests, for NULL and for NOT NULL value pairs)
$Debuglog->add( $this->dbtablename. ' object, already set to same value: '. $parname. '/'. $dbfield. ' = '. var_export( @$this->$parname, true ), 'dataobjects' );
// Set the value in the object:
$Debuglog->add( $this->dbtablename. ' object, setting param '
. $parname. '/'. $dbfield. ' to '. var_export( $new_value, true )
. ' (old: '. ( isset ( $this->$parname ) ? var_export( $this->$parname, true ) : 'NULL' )
$this->$parname = $new_value;
// Remember change for later db update:
$this->dbchange( $dbfield, $fieldtype, $parname );
* Set a parameter from a Request form value.
* @param string Dataobject parameter name
* @param string Request parameter name (NULL means to use Dataobject param name with its prefix)
* @param boolean true to set to NULL if empty string value
* @return boolean true, if value has been set/changed, false if not.
function set_from_Request( $parname, $var = NULL, $make_null = false )
$var = $this->dbprefix. $parname;
return $this->set( $parname, get_param($var), $make_null );
* Template function: Displays object ID.
* Create icon with dataobject history
function history_info_icon()
$UserCache = & get_Cache( 'UserCache' );
if( !empty($this->creator_field) && !empty($this->{$this->creator_field}) )
$creator_User = & $UserCache->get_by_ID( $this->{$this->creator_field} );
if( !empty($this->datecreated_field) && !empty($this->{$this->datecreated_field}) )
{ // We also have a create date:
$history[0] = sprintf( T_('Created on %s by %s'), mysql2localedate( $this->{$this->datecreated_field} ),
$creator_User->dget('preferredname') );
{ // We only have a cretaor:
$history[0] = sprintf( T_('Created by %s'), $creator_User->dget('preferredname') );
elseif( !empty($this->datecreated_field) && !empty($this->{$this->datecreated_field}) )
{ // We only have a create date:
$history[0] = sprintf( T_('Created on %s'), mysql2localedate( $this->{$this->datecreated_field} ) );
// HANDLE LAST UPDATE STUFF
if( !empty($this->lasteditor_field) && !empty($this->{$this->lasteditor_field}) )
$creator_User = & $UserCache->get_by_ID( $this->{$this->lasteditor_field} );
if( !empty($this->datemodified_field) && !empty($this->{$this->datemodified_field}) )
{ // We also have a create date:
$history[1] = sprintf( T_('Last mod on %s by %s'), mysql2localedate( $this->{$this->datemodified_field} ),
$creator_User->dget('preferredname') );
{ // We only have a cretaor:
$history[1] = sprintf( T_('Last mod by %s'), $creator_User->dget('preferredname') );
elseif( !empty($this->datemodified_field) && !empty($this->{$this->datemodified_field}) )
{ // We only have a create date:
$history[1] = sprintf( T_('Last mod on %s'), mysql2localedate( $this->{$this->datemodified_field} ) );
return get_icon( 'history', $what = 'imgtag', array( 'title'=>implode( ' - ', $history ) ), true );
|