<?php
/**
* Generic table model class.
* Handles DB operations and formats data for views
*
* @author Camper (hide@address.com) on 26.03.2009
*/
abstract class Model_MVCTable extends Model {
public $entity_code = null;
protected $id = null; // set with loadData(), identifies the singly entity
protected $table_alias = null; // we need alias always for more simple way in setQueryFields method
protected $dsql=array();
protected $fields_set=false; // turns to true in setQueryFields(). prevents call to setQueryFields()
// during execQuery() event
protected $joins_set=false; // turns to true when joins were added
public $fields;
protected $default_fields; // fields and values for default field (will be used in inserts)
protected $actual_fields=null; // actual field list. defaults to all fields
protected $init_where;
private $range_split_pattern='..';
protected $order=array(); // fields for ordering
/**
* data from some record (should be initialised in loadData method)
*/
public $data=array();
public $original_data=array(); // this array initialized ONLY on data load and
// may be used to compare data before modifications
/**
* list of related entities, format: assoc_array with keys - alias string and items with props:
* 'alias' => array('name'=>name of entity, 'on'=>completed condition, 'join'=>'inner'|'left outer')
*/
protected $join_entities=array();
/**
* mode of check permisssions:
* 'restricted' - need check permission for user
* 'allowed' - modification allow for all users
* 'admin_only' - editable only in admin part
*/
protected $permission_check_mode = 'restricted';
protected $calculated_fields=array(); // array containing queries for calculated fields
protected $changed=false; // set to true when loaded data was updated with set()
protected $debug=false; // call debug to enable debugging
public function init() {
parent::init();
if(!$this->table_alias)$this->table_alias=$this->entity_code;
if(is_null($this->entity_code))throw new Exception_InitError('You should define entity code for '.get_class($this));
$this->addField('id')
->datatype('int')
->system(true)
;
if(method_exists($this,'defineFields')){
//throw new Exception_Obsolete('defineFields method is obsolete');
// obsolete method
$this->defineFields();
}
// trying to set required fields
//$this->api->addHook('post-init',array($this,'setMandatoryConditions'));
}
function debug($what=true){
$this->debug=$what;
return $this;
}
function defineFields(){
// obsolete method
}
/**
* Sets the mandatory conditions such as system reference
*/
protected $is_mandatory_conditions_set=false;
protected function setMandatoryConditions(){
// showing only actual records
if($this->is_mandatory_conditions_set)return;
$this->is_mandatory_conditions_set=true;
if (isset($this->fields['deleted']))
$this->addCondition('deleted','N');
}
/**
* return ALL fields definitions (array of objects FieldDefinition class)
* To get visible fields, use getActualFields()
*/
public function getFields($field_name=null) {
if(!$field_name)return $this->fields;
if(!isset($this->fields[$field_name]))
throw new Exception_InitError('Field '.$field_name.' is not defined in '.$this->name);
return $this->fields[$field_name];
}
public function getAllFields(){
return $this->getFields();
}
/**
* Returns fields that will be in actual query
* Fields included are:
* - those set with setActualFields()
* - marked with system(true)
* Fields are reordered as they set with setActualFields()
* System fields are in the end of array
*/
public function getActualFields(){
$fields=$this->getFields();
$new_fields=array();
$actual_fields=$this->actual_fields;
if(!is_null($actual_fields)){
foreach($actual_fields as $field)if(isset($fields[$field])){
$new_fields[$field]=$fields[$field];
}
}else{
foreach($fields as $field=>$def){
if($def->visible()===true)$new_fields[$field]=$def;
}
}
return $new_fields;
}
public function getSearchableFields(){
$fields=$this->getFields();
$new_fields=array();
foreach($fields as $field=>$def){
if($def->searchable())$new_fields[$field]=$def;
}
return $new_fields;
}
public function getQuickSearchableFields(){
$fields=$this->getFields();
$new_fields=array();
foreach($fields as $field=>$def){
if($def->searchable() && $def->searchable()!=='fullonly')$new_fields[$field]=$def;
}
return $new_fields;
}
public function getSystemFields(){
$fields=array();
// adding system fields
foreach($this->getAllFields() as $field=>$def){
if($def->system()===true){
$fields[$field]=$def;
}
}
return $fields;
}
public function getMandatoryFields(){
$fields=array();
// adding system fields
foreach($this->getAllFields() as $field=>$def){
if($def->required()!==false){
$fields[$field]=$def;
}
}
return $fields;
}
public function setActualFields($actual_fields){
$this->actual_fields=$actual_fields;
return $this;
}
/**
* Returns fields that are:
* - belong to this entity only
* - not calculated
*
*/
public function getOwnFields(){
$r=array();
foreach($this->getFields() as $field=>$def){
if(!$def->calculated()&&!$def->isExternal())
$r[$field]=$def;
}
return $r;
}
/**
* Wrapper for getFields() to return only one field
*/
public function getField($field_name){
$r=$this->getFields($field_name);
if(!$r)throw new Exception_InitError("Field $field_name is not defined for $this->name");
return $r;
}
/**
* Returns the single entity ID
* It is presumed that when getID() is used, ID should be set first
* null cannot be returned by this method, so it throws exception in this case
*/
function getID(){
if(is_null($this->id)||($this->id=='new'))throw new Exception_InitError("Entity ID is not set for ".get_class($this));
return $this->id;
}
/**
* Returns true if entity was loaded from DB with loadData()
* Required for checks as getID() throws exception if entity was not loaded
*/
function isInstanceLoaded(){
// original_data is ONLY set in loadData() and unloadData(), so it can be used to
// define if we loaded this entity
return !empty($this->original_data);
}
/**
* Returns an array with fields that will be used to initialize
* dropdowns
* Usually those are id and name
* Some entities, such as contractors, have other field as name
*/
public function getListFields(){
return array('id'=>'id','name'=>'name');
}
/**
* Create and return DSQL object
* @param string $instance key of dsql object in internal array of objects
* @param boolean $select_mode (optional) if FALSE will not add alias and initial where conditions
* @param string $entity_code (optional) can used for make dsql for external entities (support only update dsql type)
* @return dblite dsql object
*/
function dsql($instance=null,$select_mode=true,$entity_code=null){
$this->setMandatoryConditions();
if (is_null($entity_code))
$entity_code = $this->entity_code;
if (is_null($instance)){
$q=$this->new_dsql($select_mode,$entity_code);
} else {
$q=$this->get_dsql($instance,$select_mode,$entity_code);
}
if($this->debug===true || (is_string($this->debug) && $this->debug==substr($instance,0,strlen($this->debug))))$q->debug();
return $q;
}
function selectQuery(){
return $this->dsql();
}
/**
* Used in dsql() method for the case of new query (instance=null)
*/
protected function new_dsql($select_mode,$entity_code){
$e=$select_mode?((!is_null($this->table_alias)?' '.$this->table_alias:'')):'';
$q=$this->api->db->dsql()
->table($entity_code.$e);
$q->select_mode=$select_mode;
if ($select_mode) {
$this->applyConditions($q);
}
return $q;
}
/**
* Used in dsql() method for the case we get it by instance name
*/
protected function get_dsql($instance,$select_mode,$entity_code){
$e=$select_mode?((!is_null($this->table_alias)?' '.$this->table_alias:'')):'';
if(!isset($this->dsql[$instance])){
$this->dsql[$instance]=$this->api->db->dsql()
->table($entity_code.$e);
$this->dsql[$instance]->select_mode=$select_mode;
if ($select_mode) {
$this->addJoins($instance);
$this->applyConditions($this->dsql[$instance]);
}
}
return $this->dsql[$instance];
}
public function getRef($field){
$f=$this->getField($field);
if(!$f || !is_object($f))throw $this->exception('getRef called on non-existant field')
->addMoreInfo('field',$field);
return $f->refModel();
}
private function addJoins($instance,$join_list=array()){
foreach($this->join_entities as $alias=>$entity){
if(!empty($join_list) && !in_array($alias,$join_list))continue;
$this->dsql[$instance]->join($entity['entity_name'].' '.$alias,
$entity['on'],
$entity['join']);
}
$this->joins_set=true;
}
/**
* Applies conditions and ordering to a provided dsql object
*/
private function applyConditions(&$dsql){
if(!empty($this->init_where)){
foreach($this->init_where as $fieldname=>$value){
$c=substr($value,0,1);
$complex=$c=='>' || $c=='<' || count(explode($this->range_split_pattern,$value))>1;
$this->setCondition($dsql,$fieldname,$value,$complex);
}
}
// applying ordering
if(!empty($this->order)){
foreach($this->order as $field=>$desc){
if(isset($this->fields[$field]) && !$this->fields[$field]->calculated() &&
!$this->fields[$field]->datatype()=='recurring' && !$this->fields[$field]->sortable())
$dsql->order($this->fieldWithAlias($field),$desc);
else $dsql->order($field,$desc);
}
}
return $this;
}
/**
* Return dsql object for view operations (set reference type field like readonly)
*/
public function view_dsql($instance=null) {
foreach ($this->fields as $fieldname=>$field_definition) {
if (($field_definition->datatype() == 'reference') and (!$field_definition->readonly())) {
$this->fields[$fieldname]->readonly(true);
}
}
// only for certain instance, otherwise there will be dead loop
if(!is_null($instance) && !empty($this->order)){
foreach($this->order as $field=>$desc)$this->setOrder($instance,$field,$desc);
}
return $this->dsql($instance);
}
/**
* return dsql object for forms
*/
public function edit_dsql($instance=null) {
if(is_null($instance))$instance='edit_dsql';
return $this->dsql($instance); // no ideas at current time, about special things for forms
}
/**
* Removes the specified query from the list (if exists)
*/
public function resetQuery($instance){
if(isset($this->dsql[$instance]))unset($this->dsql[$instance]);
return $this;
}
/**
* Adds fields defined in this model to a <b>select</b> query specified
* @param string $instance key of instance dsql object
* @param mixed $get_fields see loadData() description
* @return $this
*/
public function setQueryFields($instance,$get_fields=false){
$this->setMandatoryConditions();
$a=array();
if (!is_array($get_fields) && !is_bool($get_fields))throw new Exception_InitError('Field list must be array');
if ($get_fields===false) $fields=array_merge(array_keys($this->getActualFields()),array_keys($this->getSystemFields()));
elseif ($get_fields===true) $fields=array_keys($this->getAllFields());
elseif (is_numeric(key($get_fields))) $fields=array_merge($get_fields,array_keys($this->getSystemFields()));
else $fields=array_merge(array_keys($get_fields),array_keys($this->getSystemFields()));
if (is_null($fields))
$fields = array_keys($this->fields);
else {
// filtering fields
$tmp = array();
foreach ($fields as $fieldname)
if (isset($this->fields[$fieldname]))
$tmp[] = $fieldname;
$fields = $tmp;
}
$joined_entities = array();
foreach($fields as $field_name){
$f='';
if (isset($this->fields[$field_name])) {
$definition = $this->fields[$field_name];
// select reference entities if readonly
if ($definition->datatype()=='reference') {
$withid=$field_name.'_id';
if(!isset($this->fields[$withid])){
$withid=$field_name;
}
$definition_withid = $this->fields[$withid];
$f = $definition_withid->refModel(null,false)->toStringSQL(
(($definition->alias())?$definition->alias():$this->table_alias).
'.'.$definition_withid->dbname(), $field_name, $definition->displayField());
if ($definition->isExternal())
$joined_entities[$definition->alias()] = $definition->alias();
// possible alias
if(is_array($get_fields) && isset($get_fields[$field_name]))$f.=" as ".$get_fields[$field_name];
}
else
if ($definition->calculated()){
// while on signup we don't need those fields, except one of them
// FIXME: review this condition
if(!method_exists($this->api,'isInSystemWizard') || !$this->api->isInSystemWizard() || $field_name=='exp_date'){
// calculated field can be as well aggregated!
$f = $definition->aggregate()?"sum(".$this->calculate($definition->name(),false).")":
$this->calculate($definition->name());
// ... and external
if($definition->isExternal())
$joined_entities[$definition->alias()] = $definition->alias();
if ($definition->aggregate()){
// non-agreated fields should be added to GROUP clause
$this->setGroupBy($instance);
}
// possible alias
if(is_array($get_fields) && isset($get_fields[$field_name]))$f.=" as ".$get_fields[$field_name];
else $f.=" as $field_name";
}
}
// aggregates
elseif ($definition->aggregate()){
$f = "sum(".$definition->getDBfield($this->table_alias).")";
// non-agreated fields should be added to GROUP clause
$this->setGroupBy($instance);
// possible alias
if(is_array($get_fields) && isset($get_fields[$field_name]))$f.=" as ".$get_fields[$field_name];
else $f.=" as $field_name";
}
// other fields
else {
$f = $definition->getDBfield($this->table_alias);
if ($definition->isExternal()){
$joined_entities[$definition->alias()] = $definition->alias();
}
// possible alias
if(is_array($get_fields) && isset($get_fields[$field_name]))$f.=" as ".$get_fields[$field_name];
}
}
else {
$f = $this->fieldWithAlias($field_name); // allow using fields what not defined in fields prop
// possible alias
if(is_array($get_fields) && isset($get_fields[$field_name]))$f.=" as ".$get_fields[$field_name];
}
if($f)$a[]=$f;
}
$this->dsql($instance)->field($a);
// add required joins
if(!$this->joins_set)$this->addJoins($instance,$joined_entities);
$this->fields_set=true;
return $this;
}
/**
* Adds non-aggregated fields into GROUP BY clause of the $instance
* It is called within setQueryFields() whenever aggregated field met
*/
private function setGroupBy($instance){
foreach($this->getFields() as $name=>$def){
// system fields must be skipped
if($def->system()===true)continue;
if($def->calculated()){
// calculated fields should be in group too, but not a name only
$f='calculate_'.$name;
$gf=$this->$f();
}else{
if($def->isExternal())$gf=$def->alias().'.'.$def->dbname();
else $gf=$name;
}
if(!$def->aggregate() && $def->visible()===true && !$this->dsql($instance)->paramExists('group',$gf))
$this->dsql($instance)->group($gf);
}
}
/**
* Returns true if the join already exist
* @param strin $instance instance of the query to check
* @param string $alias alias of the join from the $this->join_entities array
*/
private function joinExists($instance,$alias){
if($this->dsql($instance)->args['join'])
$this->join_entities[$alias]['entity_name'];
}
public function isFieldsSet($instance){
return (!empty($this->dsql($instance)->args['fields'])&&$this->fields_set);
}
/**
* returns true if field is defined for the model, false otherwise
*/
function fieldExists($fieldname){
return isset($this->fields[$fieldname]);
}
function hasField($fieldname){
return $this->fieldExists($fieldname);
}
public function addField($name) {
$this->fields[$name] = new FieldDefinition($this);
return $this->fields[$name]->name($name);//->readonly($this->isReadOnly());
}
function newField($name){
// OBSOLETE: to be removed in 4.1. use addField();
return $this->addField($name);
}
/**
* Sets the SQL for calculated field
* This SQL will be passed to resulting dsql
* Actually this method calls calculate_$field_name() which must exist for model to work
*/
protected function calculate($field_name,$add_alias=true){
$calc=$this->getField($field_name)->calculated();
if($calc===true){
$method='calculate_'.$field_name;
if(!method_exists($this,$method))throw new Exception_InitError("No calculation algorythm for $field_name");
return "(".$this->$method().")";//.($add_alias?" as $field_name":"");
}elseif(is_string($calc)){
$method='calculate__'.$calc;
if(!method_exists($this,$method))throw new Exception_InitError("No calculation algorythm for $field_name"
." (calculate__$calc)");
return "(".$this->$method($field_name).")";//.($add_alias?" as $field_name":"");
}elseif(is_callable($calc)){
call_user_func($calc,$this,$field_name);
}
}
function calculate__ref($name){
$definition=$this->getField($name.'_id');
$f = $definition->refModel()->toStringSQL(
(($definition->alias())?$definition->alias():$this->table_alias).
'.'.$definition->dbname(), $name, $definition->displayField());
if ($definition->isExternal())
$joined_entities[$definition->alias()] = $definition->alias();
// possible alias
if(is_array($get_fields) && isset($get_fields[$field_name]))$f.=" as ".$get_fields[$field_name];
ts("ref $name<br/>");
$r=$this->getField($name);
$r_ref=$this->getField($name.'_id');
$m_ref=$r_ref->refModel(null,false);
ts("out $name");
if($m_ref->fieldExists('name') && !$m_ref->getField('name')->calculated()){
$f=$m_ref->table_alias.'.name';
}else{
$f = $m_ref->toStringSQL(
(($r_ref->alias())?$r_ref->alias():$this->table_alias).
'.'.$r_ref->dbname(), $name, $r_ref->displayField());
}
$q=$r_ref->refModel()->dsql();
$q->field($f);
$q->args['having']=array();
$q->table($m_ref->entity_code.' '.$m_ref->table_alias);
$q->where($m_ref->table_alias.'.id='.$this->table_alias.'.'.$name.'_id');
return $q->select();
}
public function set($field_name,$value=null){
if(is_null($value) && is_array($field_name)){
foreach($field_name as $k=>$v)if($this->fieldExists($k))$this->set($k,$v);
return $this;
}
$this->data[$field_name]=$value;
$this->changed=true;
return $this->setFieldVal($field_name,$value);
}
private function booleanToDb($value){
if($value===true||$value=='Y')$value='Y';
elseif($value===false||$value=='N')$value='N';
// sometimes we get garbage from the form
elseif($value==='')$value='N';
// sometimes there are tricky field values, like 'past' for recurring
else $value=$value;
return $value;
}
public function setFieldVal($field_name, $value) {
if(!isset($this->fields[$field_name]))throw new Exception_InitError('No such field '.$field_name.' in '.$this->name);
$field=$this->fields[$field_name];
if(!$field)return $this;
// readonly fields are not processed ever!
if ($field->readonly())
return $this;
if(is_null($value))$value=$field->defaultValue();
// fixing up the data. some field values may not be in proper format
if (isset($this->fields[$field_name])) {
$def=$this->fields[$field_name];
// boolean fields
if ($def->datatype()=='boolean'){
$value=$this->booleanToDb($value);
}
// integer and numeric fields, MUST be processed last
if($def->datatype()=='reference_id' || $def->datatype()=='reference' || $def->datatype()=='int' ||
$def->datatype()=='real' || $def->datatype()=='money' || $def->datatype()=='date' ||
$def->datatype()=='datetime' || $def->datatype()=='list'){
if($value===''){
if($def->datatype()=='reference')$value=null;
elseif($def->required())$value=0;
else $value=null;
}
}else{
// HTML code
if($def->datatype()!='image' && !$def->allow_html())
$value=strip_tags($value);
}
}
// joined fields may not be defined in fields definition
if ((!isset($this->fields[$field_name])) or (!$this->fields[$field_name]->isExternal())){
$this->dsql('modify',false)->set($this->fields[$field_name]->dbname(),$value);
}else {
$entity=&$this->join_entities[$this->fields[$field_name]->alias()];
$this->dsql('modify_'.$this->fields[$field_name]->alias(),false,$entity['entity_name'])
->set($this->fields[$field_name]->dbname(),$value);
$entity['updated'] = true;
}
$this->data[$field_name]=$value;
return $this;
}
public function setDefaultField($name, $value) {
$this->default_fields[$name]=$value;
return $this;
}
/**
* Add item into $join_entities property
* Calling this method causes join by the following rules:
* 1) related entity is always joined by ID
* 2) two types of join can be created: related entity references to master and master entity references to related
* 3) if MASTER references to related (as contractor references to address):
* $join_type JOIN $entity_name $entity_alias ON $master_alias.$join_field = $entity_alias.id
* 4) if RELATED references to master (as invoice references dochead):
* $join_type JOIN $entity_name $entity_alias ON $entity_alias.$join_field = $master_alias.id
*
* @param string $entity_alias related table alias
* @param string $entity_name related table name
* @param string $join_field field used in join condition
* @param string $reference_type either 'master' or 'related', defines WHICH $join_field to use in join, default 'master'
* @param string $join_type 'inner' or 'left outer' type of join (optional, 'inner' by default)
* @param bool $required if true - related entity MUST be inserted with insertRecord(), even if no values were set
* for corresponding entity
* @param string $master_alias alias of master entity (optional, $this->table_alias by default)
* @return $this
*/
public function addRelatedEntity($entity_alias, $entity_name, $join_field, $join_type = 'inner',
$reference_type='master', $required=false, $master_alias = null) {
if (is_null($master_alias))
$master_alias = $this->table_alias;
//while(isset($this->join_entities[$alias])){$alias.='1';}
$join_alias=$reference_type=='master'?$master_alias:$entity_alias;
$other_alias=$reference_type=='master'?$entity_alias:$master_alias;
$on_condition = $join_alias.'.'.$join_field.' = '.$other_alias.'.id';
$this->join_entities[$entity_alias] = array('readonly'=>false,'entity_name'=>$entity_name,'join_field'=>$join_field,
'reference_type'=>$reference_type,'on'=>$on_condition,'join'=>$join_type,'required'=>$required,
'table'=>($master_alias==$this->table_alias?$this->entity_code:$this->join_entities[$master_alias]['entity_name']));
// Define ID file (used when inserting)
$f=$this->addField($join_field)->system(true);
if($reference_type!='master')$f->relEntity($entity_alias);
return $this;
}
protected function addDefaultFields($dq) {
if (!empty($this->default_fields)) {
$fields_in_update = $dq->getArgsList('set');
foreach ($this->default_fields as $fieldname=>$value){
// default fields may belong to external entity
if (!$fields_in_update||!in_array($fieldname,$fields_in_update))// && !$this->getField($fieldname)->isExternal())
$this->setFieldVal($fieldname,$value);
}
}
return $this;
}
/**
* Used to implement master-detail relationship
* Master-details views might be different, like:
* - invoice-invoice_specs
* - VATperiod-invoices
* - Contractor-invoices
* - etc.
* This method sets the field name which will be used as a reference to master and a value
* of the master record
* Once called, this method assures that:
* 1) model shows records of the specified master_id
* 2) model adds records with predefined master_id
* This method can be called several times for different fields
* @param $field field name of this model which contains reference to master dataset
* @param $value master ID value
*/
function setMasterField($field,$value){
$this->addCondition($field,$value);
// some conditions cannot be used in set clause
if(count(explode(' ',$field))==1){
// FIXME: maybe change to getField()->defaultValue() call right here
$this->setDefaultField($field,$value);
}
$this->getField($field)->system(true);
return $this;
}
/**
* Applies filter of the quicksearch to model query
* Conditions added to all SEARCHABLE fields of the model and are joined
* by OR
*
* @param string $q string to search
* @param string $instance optional - query to apply filter on
*/
function applyQuicksearch($q,$instance=null){
$c=array();
foreach($this->getSearchableFields() as $field=>$def){
$c[]="$field like '%".$this->api->db->escape($q)."%'";
}
if(empty($c))return $this;
if(is_null($instance)){
throw new Exception_NotImplemented("Quicksearch is not available for multiple queries");
}
$this->dsql($instance)->having(join(' OR ',$c));
return $this;
}
private function setOrCondition($instance=null,$cond){
}
/**
* Adds a condition to init array.
* All queries created after call to this method will have this condition
* All queries created so far are also will be appended with this condition
* @param string $field field name
* @param $value
* @param boolean $complex optional. See setCondition() description
*/
function addCondition($field,$value=null,$complex=false){
// complex conditions cannot be added to init array
$this->init_where[$field]=$value;
$this->setCondition(null,$field,$value,$complex);
return $this;
}
/**
* Sets conditions on all existing select queries created so far or to specific instance
* It is useful for setController() method creates query _before_ setMasterField() is called
* @param mixed $instance optional, if null - all queries processed, if object - dsql instance expected which will be processed
* @param string $field field name
* @param $value
* @param boolean $complex optional. If true, $value treated as complex condition, i.e. it can be:
* - '>1000' - numeric field greater than 1000 (or any other figure)
* - '<2000' - numeric field less than 2000
* - '1000:2000' - numeric field greater tahn 1000 and less than 2000
* - '>2010-01-01', '<2010-01-01', '2010-01-01:2010-01-31' - date field, similar to numeric
* Complex conditions applied only to select queries
*/
function setCondition($instance=null,$field,$value=null,$complex=false){
if(is_null($value)&&is_array($field)){
foreach($field as $f=>$v)$this->setCondition($instance,$f,$v,$complex);
return $this;
}
// in case instance is null - setting all existing instances
if(is_null($instance)){
foreach($this->dsql as $instance=>$dsql)$this->setCondition($instance,$field,$value,$complex);
return $this;
}
$_field=$this->parseFieldName($field);
if($this->fieldExists($_field)){
if($this->getField($_field)->datatype()=='boolean')$value=$this->booleanToDb($value);
}
// strpos('.',$field) shows that field was passed w/o prefix. If there is prefix, we certainly need where condition
// FIXME: may be there is better implementation of this
$cond=$this->getField($_field)->calculated() && strpos($field,'.')===false?'having':'where';
// if $field contains field name only - we add an alias (only for where condition)
$where_field=(count(explode(' ',$_field))==1 and strpos($_field,'.')===false and $cond=='where')?$this->fieldWithAlias($field):$field;
if(is_object($instance))$dsql=$instance;
else $dsql=$this->dsql($instance);
if($dsql->select_mode){
// existing condition must be overwritten
if(isset($dsql->args[$cond]))foreach($dsql->args[$cond] as $i=>$where){
if(stripos($where,$where_field)!==false)unset($dsql->args[$cond][$i]);
}
// some conditions may be on external fields
if($this->getField($_field)->isExternal()){
$alias=$this->getField($_field)->alias();
$dsql->join(
$this->join_entities[$alias]['entity_name'].' '.$alias,
$this->join_entities[$alias]['on'],
$this->join_entities[$alias]['join']);
}
// if $value is null, required condition may be fully in $field
if(is_null($value) && $_field!=$field){
$dsql->$cond($where_field);
}else{
// processing complex conditions
if($complex){
// signs at the beginning
$c=substr($value,0,1);
$r=explode($this->range_split_pattern,$value);
if($c=='>' || $c=='<'){
$where_field.=$c;
$value=substr($value,1);
}
// ranges
elseif(count($r)>1){
// must be between condition
$dsql->$cond("$where_field between '{$r[0]}' and '{$r[1]}'");
return $this;
}
}
$dsql->$cond($where_field,$value,false);
if(!$complex)$dsql->set($field,$value);
}
}else if(!$complex)$this->setFieldVal($field,$value);
return $this;
}
/**
* Sets default ordering for all new instances
*/
function setOrder($instance=null,$field,$desc=false,$sticky=false){
// null means set to all queries, including those not yet initialized.
// removing following line breaks reportss' ordering, so you will need to manually set $sticky
// parameter in those calls
if(is_null($instance))$sticky=true;
if($sticky)$this->order[$field]=$desc;
if(is_array($field)){
throw new Exception_InitError("setOrder() does not allow array as a parameter");
}
// in case instance is null - setting all existing instances
if(is_null($instance)){
foreach($this->dsql as $instance=>$dsql)$this->setOrder($instance,$field,$desc,$sticky);
return $this;
}
$dsql=$this->dsql($instance);
if(isset($this->fields[$field]) && !$this->fields[$field]->calculated() &&
!$this->fields[$field]->datatype()=='recurring' && !$this->fields[$field]->sortable())
$dsql->order($this->fieldWithAlias($field),$desc);
else $dsql->order($field,$desc);
return $this;
}
/**
* This defines which column data shouldbe sorted (by default). If first character is minus,
* then sort order is reversed. Actual sorting is performed by Views where aplicable
*/
public function getSortColumn(){
//Example: return '-created_dts';
return null;
}
/**
* Should return either null or ID of the entity which is referenced
* by this entity as a parent (e.g. for docspec entity this method should return dochead_id)
*/
protected function getOwnerId(){
return null;
}
/**
* Calls required method of Model to update data
* Depends on the model status:
* - data updated if model has instance loaded
* - data inserted if no instance loaded into model
* @param array $data
* @param boolean $force_new_record if true, forces model to unload its data so new record is inserted surely
*/
public function update($data=array(),$force_new_record=false){
if($force_new_record)$this->unloadData();//$this->original_data=array();
foreach($data as $key=>$val){
if(!$this->hasField($key))unset($data[$key]);
}
// any action required before update is processed
$this->beforeModify($data);
// with rules of array_merge
if($this->isInstanceLoaded()){
$id=$this->getID();
$r=$this->updateRecord($id,$data);
}else{
$r=$this->insertRecord($data);
$id=$r;
}
// any action after update is processed
$this->afterModify($id); // data is already saved and loaded
if(is_null($r=$this->api->hook('compat-update-returns-id',array($r)))){
$r=$this;
}
return $r;
}
public function insert($data=array()){
$r=$this->update($data,true);
return $r;
}
protected function insertRecord($data=array()) {
// table data often with relations, so we always must have a transaction
$this->api->db->beginTransaction();
try{
$this->beforeInsert($data);
if(!$this->isInstanceLoaded())$data=array_merge($this->data,$data);
if (!empty($data)) {
foreach ($data as $field=>$value){
$this->setFieldVal($field,$value);
}
}
$this->addDefaultFields($this->dsql('modify',false));
$this->validateData($this->data);
// this check is required as some entities may not allow edits for some reason, e.g.:
// - invoice is read-only if its VAT period is closed
if($this->isReadonly())throw new Exception_AccessDenied('Operation not allowed!');
// create records in related entities if need
if (!empty($this->join_entities)){
foreach ($this->join_entities as $alias=>&$entity_item) {
// reference fields should be skipped as we don't update dictionaries automatically
if($this->fieldExists($entity_item['join_field'])&&
$this->getField($entity_item['join_field'])->datatype()==='reference')continue;
// joined entities fields are appended to query only if reference_type was 'master'
// because with reference_type='related' this entity ID is used for join
// for master references we add to THIS entity table
if(!$entity_item['readonly']
&& $entity_item['reference_type']=='master' && isset($entity_item['updated']) &&
($entity_item['updated']==true)) {
$this->setFieldVal($entity_item['join_field'],$entity_id=$this->dsql('modify_'.$alias)->do_insert());
unset($this->dsql['modify_'.$alias]);
$entity_item['updated']=false;
}
}
}
// setting required fields
// tricky thing with system ID
if (isset($this->fields['created_dts']))
$this->dsql('modify',false)->setDate('created_dts');
//$this->logVar($this->dsql('modify',false)->insert());
$res = $this->dsql('modify',false)->do_insert();
unset($this->dsql['modify']); // clear object
// now we should insert mandatory related entities
if(!empty($this->join_entities)){
foreach($this->join_entities as $alias=>&$entity_item){
// reference fields should be skipped as we don't update dictionaries automatically
if($this->fieldExists($entity_item['join_field'])&&
$this->getField($entity_item['join_field'])->datatype()==='reference')continue;
if($entity_item['reference_type']=='related' and $entity_item['required']){
// this item should reference just added entity
$this->setFieldVal($entity_item['join_field'],
$id=$this->dsql('modify_'.$alias,false,$entity_item['entity_name'])
->set($entity_item['join_field'],$res)->do_insert());
// remembering new record ID
$entity_item['id']=$id;
unset($this->dsql['modify_'.$alias]);
}
}
}
// loading data inserted as it will be used then
// Romans Commit this!!!
if($res === false)throw new Exception_InitError("Insert failed");
$this->afterInsert($res);
$this->api->db->commit();
}catch(Exception $e){
$this->api->db->rollback();
throw $e;
}
$this->loadData($res);
//if(!$this->isInstanceLoaded())throw new Exception_InitError("Insert but failed to load back, id=".$res);
return $res;
}
/**
* This method executes before update() method is processed. Override this method if you want
* something to be recalculated/validated before any update
*/
function beforeModify(&$data){
$this->hook('beforeModify',array($this,&$data));
return $this;
}
/* Redefine or use beforeLoad hook */
function beforeLoad($id){
$this->hook('beforeLoad',array($this));
return $this;
}
/* Redefine or use afterLoad hook */
function afterLoad(){
$this->hook('afterLoad',array($this));
return $this;
}
/**
* This method executes last after all modifications were made in update()
*/
function afterModify($id){
$this->hook('afterModify',array($this));
return $this;
}
/**
* This method executes before insertRecord() method is processed, and AFTER beforeModify()
*/
function beforeInsert(&$data){
$this->hook('beforeInsert',array($this,$data));
return $this;
}
/**
* This method executes right after insertRecord() was processed, and BEFORE afterModify()
*/
function afterInsert($new_id){
$this->hook('afterInsert',array($this,$new_id));
return $this;
}
/**
* This method executes before updateRecord() method is processed, and AFTER beforeModify()
*/
function beforeUpdate(&$data){
$this->hook('beforeUpdate',array($this,$data));
return $this;
}
/**
* This method executes right after updateRecord() was processed, and BEFORE afterModify()
*/
function afterUpdate($id){
$this->hook('afterUpdate',array($this));
return $this;
}
function beforeDelete(&$data){
$this->hook('beforeDelete',array($this));
return $this;
}
function afterDelete($old_id){
$this->hook('afterDelete',array($this));
return $this;
}
function updateRecord($id=null, $data=array()) {
if(is_null($id))$id=$this->get('id');
$this->api->db->beginTransaction();
try{
$this->beforeUpdate($data);
// we'll need this in audit function:
$this->data=$data=array_merge($this->data,$data);
$changed=0;
if (!empty($data)) {
foreach ($data as $fieldname=>$value){
if($this->isChanged($fieldname,$value)){
$this->setFieldVal($fieldname,$value);
$field=$this->fields[$fieldname];
if($field && !$field->readonly())
$changed++;
}
}
}
if(!$changed){
$this->afterUpdate($this->getID());
$this->api->db->commit();
return $this;
}
// when setting field values some of them may change:
// e.g. boolean fields change value from true/false to 'Y'/'N'
$this->validateData($data);
// see insertRecord() for explanations on this check
if($this->isReadonly())throw new Exception_AccessDenied('Operation not allowed!');
// create or update records in related entities if need
if (!empty($this->join_entities)){
foreach ($this->join_entities as $alias=>&$entity_item) {
// reference fields should be skipped as we don't update dictionaries automatically
if($this->fieldExists($entity_item['join_field'])&&
$this->getField($entity_item['join_field'])->datatype()=='reference')continue;
// readonly entities should not be update
if($entity_item['readonly']===true)continue;
if($entity_item['reference_type']=='master'&&(isset($entity_item['updated']))&&($entity_item['updated']==true)) {
// get value from DB because controller can not define this field in actual field list
// also, this field can not defined in field list
// field may be in another entity
$joined_field_value = $this->api->db->dsql()->table($entity_item['table'])//($this->entity_code)
->field($entity_item['join_field'])
->where('id',$this->id);
$joined_field_value = $joined_field_value->do_getOne();
if (empty($joined_field_value))
$this->setFieldVal($entity_item['join_field'],
$this->dsql('modify_'.$alias,false)->do_insert());
else{
$this->dsql('modify_'.$alias,false)
->where('id',$joined_field_value)
;
//$this->logVar($this->dsql('modify_'.$alias)->update(), $this->short_name);
$this->dsql('modify_'.$alias,false)->do_update();
}
$this->dsql['modify_'.$alias]=null;
}
// related join type has fields in reverse, so we process it differently
elseif($entity_item['reference_type']=='related' and (isset($entity_item['updated'])) and ($entity_item['updated']==true)){
$joined_field_value = $this->api->db->dsql()->table($entity_item['entity_name'])
->field('id')
->where($entity_item['join_field'],$this->id);
$joined_field_value = $joined_field_value->do_getOne();
if (empty($joined_field_value)&&$entity_item['required'])
$this->setFieldVal($entity_item['join_field'],
$this->dsql('modify_'.$alias)->do_insert());
else{
$this->dsql('modify_'.$alias,false,$entity_item['entity_name'])
->where('id',$joined_field_value)
// FIXME: tricky set to avoid exception, http://adevel.com/fuse/mantis/view.php?id=2698
->set($entity_item['join_field'],$this->id)
;
$this->dsql('modify_'.$alias,false)->do_update();
}
$this->dsql['modify_'.$alias]=null;
}
$entity_item['updated']=false;
}
}
if (isset($this->fields['updated_dts']))
$this->dsql('modify',false)->setDate('updated_dts');
$this->dsql('modify',false)->where('id',$id);
//$this->logVar($this->dsql('modify',false)->update());
$this->dsql('modify',false)->do_update();
unset($this->dsql['modify']); // clear object
$this->afterUpdate($id);
$this->api->db->commit();
}catch(Exception $e){
$this->api->db->rollback();
throw $e;
}
// reloading data as it will be required later
$this->loadData($id);
return $this;
}
public function archive(){
if(!$this->fieldExists('archive'))throw new Exception_InitError($this->short_name.
" does not have archive ability");
$this->set('archive',true)->updateRecord($this->get('id'));
return $this;
}
public function delete(){
$r=$this->deleteRecord($this->get('id'));
return $r;
}
/**
* Restores deleted record if possible
* Only records marked as deleted (deleted='Y') can be restored, until they are deleted permanently
* This method must be overridden as different entities require different processing
* for undelete
*/
function restore($id){
// TODO: implement restore for some controllers
throw new Exception_NotImplemented("Restore is not implemented for ".get_class($this));
}
protected function deleteRecord($id=null) {
if(is_null($id))$id=$this->get('id');
else $this->loadData($id);
// see insertRecord() for explanations on this check
if($this->isReadonly())throw new Exception_AccessDenied('Operation not allowed!');
// some mega-clever developers allow to delete records that are not allowed to delete,
// so we check again and give exception
if($this->getRelatedEntities())throw new Exception_InitError("Entity $this->short_name:$id is not allowed to delete");
$this->api->db->beginTransaction();
try{
$this->beforeDelete($this->data);
if (isset($this->fields['deleted'])) {
$dq = $this->dsql(null,false)->where('id',$id)
->set('deleted','Y');
if (isset($this->fields['deleted_dts']))
$dq->setDate('deleted_dts');
$dq->do_update();
}
else
$this->dsql(null,false)->where('id',$id)->do_delete();
$this->afterDelete($id);
$this->api->db->commit();
}catch(Exception $e){
$this->api->db->rollback();
throw $e;
}
// if data was loaded into model - we should change state to not loaded
$this->unloadData();
return $this;
}
/**
* Get fieldname with associated table alias
* @param string $fieldname name of field (key from fields prop)
* @return string
*/
public function fieldWithAlias($fieldname) {
// dot in fieldname means there is alias already
if(strpos($fieldname,'.')!==false)return $fieldname;
// parenthesis in fieldname means it is a function, no alias required
if(strpos($fieldname,'(')!==false)return $fieldname;
// now we need to remove any < > like in etc. from field name
$field=$this->parseFieldName($fieldname);
$sign=$this->parseFieldName($fieldname,'sign');
if (!isset($this->fields[$field])) {
$res = (is_null($this->table_alias)?'':"$this->table_alias.").$fieldname;
} else {
if ($this->fields[$field]->isExternal())
$alias = $this->fields[$field]->alias();
else
$alias = $this->table_alias;
$res = (empty($alias)?$this->fields[$field]->dbname():$alias.'.'.$this->fields[$field]->dbname()).$sign;
}
return $res;
}
/**
* Parses a string to get field name from it
* Sometimes conditions passed as 'fieldname>' or 'fieldname like', and therefore
* we can't use this string as a field name
* This function parses such string to get actual field name from it
*/
public function parseFieldName($fieldname,$return='field'){
$c=substr($fieldname,-1,1);
if(substr($fieldname,-2,2)=='<>'){
$field=trim(substr($fieldname,0,-2));
$sign='<>';
}
elseif(substr($fieldname,-2,2)=='<='||substr($fieldname,-2,2)=='>='){
$field=trim(substr($fieldname,0,-2));
$sign=substr($fieldname,-2,2);
}
elseif($c=='<' || $c=='>' || $c=='='){
$field=trim(substr($fieldname,0,-1));
$sign=$c;
}
elseif(substr($fieldname,-5,5)==' like'){
$field=trim(substr($fieldname,0,-5));
$sign=' like';
}
elseif(substr($fieldname,-7,7)==' not in'){
$field=trim(substr($fieldname,0,-7));
$sign=' not in';
}
elseif(substr($fieldname,-3,3)==' in'){
$field=trim(substr($fieldname,0,-3));
$sign=' in';
}
elseif(substr($fieldname,-3,3)==' is'){
$field=trim(substr($fieldname,0,-3));
$sign='';
}
elseif(substr($fieldname,-12,12)==' is not null'){
$field=trim(substr($fieldname,0,-12));
$sign=' is not null';
}
else{
$field=$fieldname;
$sign='';
}
// cutting off table aliases
if(strpos($field,'.')!==false)$field=substr($field,strpos($field,'.')+1);
// either field or sign:
return $$return;
}
/**
* load data into model for ID
* Sets the ID for the model so it can be used later
* @param int $id if null, tries to load data from the previous ID (refresh)
* @param string $get_fields may have values:
* - bool true: loads all fields, not only visible
* - bool false: default, loads only getActualFields() fields
* - array(): loads fields set in array
* arrays here are arrays returned by getActualFields(), getOwnFields(), etc.
*/
public function loadData($id=null,$get_fields=false) {
//echo "loading {$this->name} $id<br/>";
$this->beforeLoad($id);
if(is_null($id))$id=$this->id;
else $this->id=$id;
$this->resetQuery('loadData_'.$id)->setQueryFields('loadData_'.$id,$get_fields);
$q=$this
->dsql('loadData_'.$id)
->where($this->fieldWithAlias('id'),$this->id)
;
//if($this instanceof Model_Invoice)
// $this->logVar($q->select(),"$this->short_name:");
//if($this instanceof Model_Invoice)
// $this->logVar($q->do_getHash(),"$this->short_name:");
$this->data=$this->original_data=$q->do_getHash();
if(!$this->data){
$this->api->getLogger()->logLine("No data with id: ".$id." for: ".get_class($this).
" but got no data. Query: ".$q->select()."\n");
}
$this->changed=false;
$this->afterLoad();
return $this;
}
/**
* Unloads all data loaded into model, sets its state as empty
* Controller::update() method will insert new record after calling this method
*/
public function unloadData(){
$this->id=null;
$this->data=array();
$this->original_data=array();
return $this;
}
function __toString() {
try{
$r=$this->toString();
}catch(Exception_InitError $e){
return "Failed to load entity: no ID";
}
return (string)$r;
}
/**
* return array with field values or one value for some field
*/
public function get($field=null,$mandatory=true){
if($field && !$this->fields[$field]){
throw new Exception_InstanceNotLoaded('Field '.$field.' is not defined in '.$this->name);
}
if (empty($this->data) && $mandatory===true)
throw new Exception_InstanceNotLoaded('Data was not loaded for '.$this->name);
$res=null;
if (is_null($field))
$res = $this->data;
elseif (!is_array($field)) {
//if(!isset($this->fields[$field]))throw new Exception_InitError('Field `'.$field.'` is not defined in '.$this);
if(!array_key_exists($field,$this->data)&&method_exists($this->api,'getSysConfig') && $this->api->getSysConfig('debug_global')&&$this->api->getSysConfig('debug_warn_get'))
$this->api->getLogger()->logVar("Field $field does not exist in $this->short_name");
elseif ($this->fields[$field]->datatype()=='boolean'){
$res=$this->data[$field]=='Y'?true:false;
}
else $res = $this->data[$field];
}
else {
$res = array();
foreach ($field as $fieldname){
if(!array_key_exists($fieldname,$this->data)&&$this->api->getSysConfig('debug_global')&&$this->api->getSysConfig('debug_warn_get'))
$this->api->getLogger()->logVar("Field $fieldname does not exist in $this->short_name");
elseif ($this->fields[$field]->datatype()=='boolean'){
$res[$fieldname]=$this->data[$field]=='Y'?true:false;
}
else $res[$fieldname] = $this->data[$fieldname];
}
}
return $res;
}
public function getOriginal($field=null){
if(empty($this->original_data))return null;
return is_null($field)?$this->original_data:$this->original_data[$field];
}
/**
* Checks changes made in entity since last load and returns hash of changed fields in form of
* array($field_name=>array('old'=>$old_value,'new'=>$new_value))
* Data provided is checked against $this->original_data array
*
* @param array $data array of data to check, if empty - equals to $this->data
*/
public function whatChanged($data=array()){
if(is_null($data))$data=$this->data;
$result=array();
// we will go through all entity fields
foreach($this->getAllFields() as $name=>$def){
// if no such field in provided data - it was not changed
if(!array_key_exists($name,$data))continue;
if($data[$name]!=$this->original_data[$name])$result[$name]=array('old'=>$this->original_data[$name],'new'=>$data[$name]);
}
return $result;
}
/**
* Checks if specidied field value was changed since last load
* If $value provided, check against this value, in other way field
* is checked against its current value
*/
public function isChanged($field,$value='** not set **'){
if($value==='** not set **')$value=$this->data[$field];
return (isset($this->original_data[$field])?
$this->original_data[$field]:null)!==$value;
}
/**
* Returns all rows from the Model
* where conditions are applied
* @param array $fields assoc array with fields to retrieve in format 'field'=>'alias'
* if null - return all fields
*/
public function getRows($fields=array()) {
$q=$this->resetQuery('get_rows')->dsql('get_rows');
//$q=$this->resetQuery('get_rows')->view_dsql('get_rows');
$this->setQueryFields('get_rows',empty($fields)?false:$fields);
//$this->logVar($q->select(),$this->short_name);
return $q->do_getAllHash();
}
/**
* Returns totals for specified rows
*/
function getRowTotals($fields){
$sum=array();
foreach($fields as $field){
$sum[$field]=0;
}
$rows=$this->getRows($fields);
foreach($rows as $row){
foreach($fields as $field){
$sum[$field]+=$row[$field];
}
}
return $sum;
}
/**
* Load data by other field than ID
*/
public function loadBy($field,$value=null,$case_insensitive=false){
$id=$this->getBy($field,$value,$case_insensitive);
if($id)$this->loadData($id['id']);
return $this;
}
/**
* Returns the hash by any field of the entity's table
* TODO: if required - returns data by several fields similar to methods like dsql::set()
* @param string $field
* @param mixed $value
* @param boolean $case_insensitive if true - for string fields makes condition case insensitive,
* for other fields makes no sense
*/
public function getBy($field,$value=null,$case_insensitive=false){
if(is_array($field))$instance=join(array_keys($field));
else $instance=$field;
$data = $this->setQueryFields("getby_$instance")
->dsql("getby_$instance");
if(is_null($value)&&is_array($field)){
foreach($field as $key=>$val){
if($case_insensitive&&$this->getField($field)->datatype()=='string')
$data->where('lcase('.$this->fieldWithAlias($key).')',strtolower($val));
else $data->where($this->fieldWithAlias($key),$val);
}
}
else{
if($case_insensitive&&$this->getField($field)->datatype()=='string')
$data->where('lcase('.$this->fieldWithAlias($field).')',strtolower($value));
else $data->where($this->fieldWithAlias($field),$value);
}
//$this->logVar($data->select());
$data=$data->do_getHash();
$this->resetQuery("getby_$instance");
return $data;
}
/**
* return TRUE if current entity is readonly
*/
protected function isReadonly() {
return false;
}
protected function defineAuditFields() {
// some audit fields might be defined in the model explicitely
// as they play important role in system logics
if(!isset($this->fields['created_dts']))
$this->newField('created_dts')
->datatype('datetime')
->caption('Created')
->readonly(true)
->visible(false)
->editable(false)
->system(true)
;
if(!isset($this->fields['upadted_dts']))
$this->newField('updated_dts')
->datatype('datetime')
->caption('Updated')
->readonly(true)
->visible(false)
->editable(false)
;
if(!isset($this->fields['deleted']))
$this->newField('deleted')
->datatype('boolean')
->caption('Deleted')
->readonly(true)
->visible(false)
->editable(false)
;
if(!isset($this->fields['deleted_dts']))
$this->newField('deleted_dts')
->datatype('datetime')
->caption('Deleted')
->readonly(true)
->visible(false)
->editable(false)
;
}
/**
* return string representation of record
*/
public function toString() {
return $this->entity_code.' #'.$this->getId();
}
function getEntityCode(){
return $this->entity_code;
}
function getFriendlyName(){
return $this->entity_code;
}
/**
* Returns entity code (table name) which contains image data for the entity
* It is entity_code by default, but, say, for contractor_self it is other table (company)
*/
function getImageEntity(){
return $this->entity_code;
}
/**
* Return expression for get string representation of record in SQL
* @param string $source_field field in source table (with alias)
* @param string $dest_fieldname name of field in query result
* @param string $expr expression for get data from entity (this param should be only in Model_Table!)
* @return string
*/
public function toStringSQL($source_field, $dest_fieldname, $expr = 'name') {
if($this->fieldExists($expr))
return '(select '.$expr.' from '.$this->entity_code.
' where id = '.$source_field.') as '.$dest_fieldname;
return 'concat("'.$this->entity_code.' #",'.$source_field.') '.$dest_fieldname;
}
/**
* This function returns false if there are no entities in DB which reference this entity
* Otherwise it returns hash in form $name=>$count,
* where $name is the name of the related entity (any name you want to see)
* and $count is count of related entities for this record
* Model uses field definition to get info about related tables and then checks every related entity
* for data with this model ID.
* TODO: Some entities may have cascade relations, like address for contractor. Such entities should be marked with
* TODO: FieldDefinition::relation() method call
* FIXME: for the time being this method introduced in certain Models:
* - Contractor
*/
public function getRelatedEntities(){
return false;
}
/**
* Prepares SQL query to get this entity related records
* Used primarily in getRelatedEntities() method, and also
* in some other places like Document's locked field calculation
*
* This method should return prepared dsql instance or false.
* No fields should be specified as they are parent method specific
*
* @param int $id optional. If specified, condition created by this ID, else it is by a host query ID
*/
protected function getRelatedEntitiesSQL($id=null){
return false;
}
/**
* Performs data validation before it is updated
* It calls all the validations set by FieldDefinition::validate() if any
*/
protected function validateData($data){
foreach($this->getActualFields() as $field=>$def){
// some fields somehow do not have definition...
try {
if(!$this->isChanged($field,isset($data[$field])?$data[$field]:null))continue;
if(is_object($def) && $def->validate()){
$res=call_user_func_array($def->validate(),array($data[$field],$this->owner));
if($res===false)throw new Exception_ValidityCheck('Incorrect format');
if(is_string($res))throw new Exception_ValidityCheck($res);
}
}catch(Exception_ValidityCheck $v){
$v->setField($field);
throw $v;
}
}
return $this;
}
/**
* This method is quite similar to all entities with 'name' field
*/
public function validateName($data){
if($this->isInstanceLoaded() && !$this->isChanged('name',$data['name']))return true;
// should not be duplicate names
if($this->dsql()->where('name',$data['name'])->field('count(*)')->do_getOne()>0)
throw new Exception_ValidityCheck('Duplicate '.$this->getFriendlyName().' name');
return true;
}
function destroy(){
foreach ($this->dsql as $k=>$q){
unset($q);
}
foreach ($this->fields as $k=>$f){
unset($this->fields[$k]);
}
return parent::destroy();
}
}