+ initial work towards a new tdo/doa abstraction on the database level
[vsc.git] / _res / _libs / models / tdoabstract.class.php
blob925c896f31a43b31b21ee36fbd31373b77822ec5
1 <?php
2 /**
3 * @desc The Abstract Data Objects series
5 * @author Marius Orcsik <marius.orcsik@gmail.com>
6 * @version 0.0.1
7 */
9 class tdoAbstract {
10 /**
11 * @var interfaceSql
13 public $db;
14 public $wheres = array();
15 public $groups;
16 public $limit;
17 public $refers = array();
19 /**
20 * @var tdoAbstractField
22 protected $id;
23 protected $name;
24 protected $alias;
25 protected $fields;
27 /**
28 * Function to implement get_ | set_ virtual methods
29 * @param string $method
30 * @param [] $args
31 * @return mixed
32 * @see http://www.ibm.com/developerworks/xml/library/os-php-flexobj/ by Jack Herrington <jherr@pobox.com>
34 function __call ($method, $args) {
35 $diff = $this->get_members();
36 $all = get_object_vars($this);
38 if ( preg_match( '/set_(.*)/', $method, $found ) ) {
39 // check for fields with $found[1] name
40 if ( array_key_exists( $found[1], $diff) ) {
41 $this->fields[$found[1]]->setValue($args[0]);
42 return true;
43 // check for obj members with $found[1] name
44 } elseif (array_key_exists( $found[1], $all)){
45 $this->$found[1] = $args[0];
46 return true;
48 } else if ( preg_match( '/get_(.*)/', $method, $found ) ) {
49 if ( array_key_exists( $found[1], $diff ) ) {
50 return $this->fields[$found[1]]->getValue();
51 } elseif (array_key_exists( $found[1], $all)){
52 return $this->$found[1];
55 return false;
58 /**
59 * @desc A function to implement a virtual getter member of the class
61 * @param string $key
62 * @return mixed
64 * @see http://www.ibm.com/developerworks/xml/library/os-php-flexobj/ by Jack Herrington <jherr@pobox.com>
66 function __get ( $key ) {
67 // fixed a bug where the method wasn't working for $key == 'id' (which doesn't exist in fields array)
68 if ($key == 'id')
69 return $this->id;
70 return $this->fields[$key];
73 /**
74 * @desc A function to implement a virtual setter member for this class
76 * @param string $key
77 * @param mixed $value
78 * @return bool
80 * @see http://www.ibm.com/developerworks/xml/library/os-php-flexobj/ by Jack Herrington <jherr@pobox.com>
82 function __set ( $key, $value ) {
83 if (
84 array_key_exists ($key, $this->get_members()) /*&&
85 $this->isValidMember($this->fields[$key])*/
86 ) {
87 $this->fields[$key]->set_value ($value);
88 return true;
89 } else {
90 $this->fields[$key] = new tdoAbstractField ($key, $this->alias);
91 $this->fields[$key]->set_value ($value);
95 public function __construct(&$db) {
96 $this->db = &$db;
97 $this->alias = 'm1';
99 $this->instantiateMembers();
102 public function __destruct() {}
105 * gets the members we consider table fields
107 * @return array ('fieldName' => tdoAbstractField)
109 public function &get_members () {
110 return $this->fields;
114 * checks if a field is a valid member of the current object
116 * @param tdoAbstractField $incMember
117 * @return bool
119 public function isValidMember ($incMember) {
120 if (
121 ! ($incMember instanceof tdoAbstractField ) ||
122 // this prevents an error in php > 5.2 object comparison
123 ! in_array ($incMember, $this->get_members() /**/, true/**/)
125 return false;
126 } else
127 return true;
131 * instantiating the object's members
133 public function instantiateMembers () {
134 foreach ($this->get_members() as $key => $field) {
135 if ( is_int ($key) && is_string ($field) ) {
136 $this->fields[$field] = new tdoAbstractField ($field, $this->alias);
138 // FIXME: this is so bad :D - it implies that the primary key must be the first field containing _id :D
139 if (stristr ($field,'_id') && !isset ($this->id) && $this->isValidMember ($this->fields[$field])) {
140 $this->id = &$this->fields[$field];
142 unset ($this->fields[$key]);
146 $this->wheres = array();
147 $this->groups = '';
148 // $this->orders = '';
149 $this->refers = array();
152 * setting the table's index field
154 * @param tdoAbstractField $obj
156 public function set_index (&$obj) {
157 if ($this->isValidMember($obj)) {
158 $this->id = &$obj;
159 $this->id->flags = PRIMARY;
164 * setting an alias for the table in case of joins.
165 * this method also sets the alias on all the fields of the current object
167 * @param string $alias
169 public function set_alias ($alias) {
170 foreach ($this->get_members() as $field){
171 if ($field->table == $this->alias) {
172 $field->table = 't'.$alias;
175 $this->alias = 't'.$alias;
179 * For adding custom sql functions for certain fields
181 * @param string $modif
182 * @param tdoAbstractField $field
184 public function set_fieldModifier ($modif, &$field) {
185 if ($this->isValidMember ($field) && stristr ($modif, '%'))
186 $field->set_modifier ($modif);
190 * method used in join cases to add the joined object's fields
191 * to the current one
193 * @param array ('fieldName' => tdoAbstractField) $incArr
195 public function addFields (&$incArr) {
196 if (is_array($incArr)) {
197 foreach ($incArr as $fieldName => $field) {
198 $this->fields[$fieldName] = $field;
199 if (key_exists($fieldName, $this->fields)) {
200 // if we have the field already in the fields array
201 // we only need to keep it's alias
202 $curentAlias = $this->fields[$fieldName]->get_table ();
203 $this->fields[$fieldName]->set_table ($curentAlias);
210 * method to return an array of fieldNames=>fieldValues
211 * @return array
213 public function fieldsAsArray () {
214 foreach ($this->fields as $key => $val) {
215 $ret[$key] = $val->value;
217 return $ret;
221 * Based on field values, we get the _first_ row in the table that matches
222 * TODO: maybe we can have a function that returns _all_ the rows that match
224 * @return bool
226 public function buildObj () {
227 $sql = $this->buildSql(1);
228 $this->db->query( $sql );
230 $arr = $this->db->getAssoc();
231 if (is_array($arr)) {
232 foreach ($arr as $field => $var) {
233 $this->fields[$field]->set_value ($var);
235 return true;
237 return false;
241 * Gets the row in the table for $id
243 * @param mixed $id
246 public function get ($id) {
247 if (empty ($id))
248 return false;
249 $id = $this->db->escape($id);
251 $this->instantiateMembers();
253 $this->id->set_value ($id);
254 $this->buildObj ();
257 * Returns the last id of the table
258 * OBS: this assumes that we didn't delete any rows from the table.
260 * @return int
262 public function getLastInsertedId () {
263 $sql = $this->db->_SELECT ($this->id->name)
264 .$this->db->_FROM ($this->name) . $this->db->_AS ($this->alias);
266 $this->addOrder ($this->id, false);
268 $sql .= $this->db->_ORDER ($this->outputOrders ());
270 $sql .= $this->db->_LIMIT (1);
272 $this->db->query ($sql);
273 return $this->db->getScalar ();
277 * encapsulating the $this->db->escape() method
279 * @param mixed $value
280 * @return mixed
282 public function escape ($value) {
283 if (is_numeric ($value)) {
284 return (int)$value;
285 } else {
286 // this behavior is broken for PostgreSQL as it encloses the field values' in apostrophes
287 // TODO for each driver either:
288 // 1. develop a static method for escaping variables
289 // 2. either treating all cases in this method (probably 1)
290 return $this->db->STRING_OPEN_QUOTE . $this->db->escape ($value) . $this->db->STRING_CLOSE_QUOTE;
294 * inserting into the database
295 * TODO: multiple inserts to use with loadFromArray
297 * @return int
299 public function insert () {
300 $sql = $this->db->_INSERT ($this->name);
302 $fieldStr = '';
303 $valueStr = '';
304 $values = array ();
305 $f = $this->get_members();
307 // $sql .= ' '.$this->outputRefers().' SET';
309 foreach ($f as $fieldName => $field) {
310 if ($this->isValidMember ($field) && ($field != $this->id) && !is_null ($field->value)) {
311 // $valueStr .= $this->escape ($field->value);
312 $value = $field->value;
314 $fieldStr.= (!empty ($fieldStr) ? ', ' : ' ') . $fieldName;
315 $valueStr.= (!empty ($valueStr) ? ', ' : ' ') . $value;
316 $values[] = $value;
320 // $sql.= ' ('.$fieldStr.') VALUES ('.$value.')';
321 $sql.= ' ('.$fieldStr.')' . $this->db->_VALUES ($values); // VALUES ('.$valueStr.')';
322 if ($fieldStr) {
323 $this->db->query($sql);
324 return $this->getLastInsertedId();
328 public function update ($id = null) {
329 if (is_null ($id)) {
330 $id = $this->id->value;
333 if (is_null ($id)) {
334 throw new Exception('Cannot update record in table '.$this->name.' because an id hasn\'t been provided');
335 return false;
338 $sql = $this->db->_UPDATE (array ($this->name, $this->alias) );
340 if (is_array ($this->refers) && !empty ($this->refers)){
341 $this->refers = array_reverse ($this->refers);
343 foreach ($this->refers as $ref)
344 $sql .= $ref;
347 $sql .= $this->db->_SET();
349 $fields = $this->get_members();
351 foreach ($fields as $fieldName => $field) {
352 if (($field instanceof tdoAbstractField) && $field != $this->id) { // TODO: make a more real check for field is an id
353 $value = $field->value;
356 if ((isset($value) && !is_null($value))) {
357 $sql.= $field->table.'.'.$fieldName.' = '.$this->escape ($value).', ';
360 // echo substr ($sql,-2).'<br/><br/>';
361 if (substr ($sql,-2) == ', ') {
362 $sql = substr ($sql, 0, -2);
364 // die;
365 $sql.= $this->db->_WHERE($this->alias.'.'.$this->id->name.' = '.$this->escape($id));
367 // var_dump($sql);die ('<br/>update');
368 $this->db->query($sql);
370 return $id;
373 public function replace ( $id = null ) {
374 if (is_null($id)) {
375 $id = $this->id->value;
378 if (is_null($id) || !$this->idExists ($id) ) {
379 return $this->insert ();
380 } else {
381 return $this->update ($id);
385 // TODO: make this the same way the find first method works
386 public function delete ($id = null) {
387 $id = (!is_null ($id) ? $id :$this->id->value);
389 if (!is_null ($id)) {
390 // no need for other wheres
391 $this->wheres = array();
392 $this->addWhere ($this->id, '=', $id);
395 if (empty($this->wheres)) {
396 return false;
399 $temp = $this->alias;
400 $this->set_alias(null);
403 $sql = 'DELETE FROM '.$this->name.' WHERE '; //$this->id->name.' = '.$this->escape($value).' LIMIT 1';
404 $sql.= $this->outputWheres();
405 // echo $sql;die;
406 $affRows = $this->db->query($sql);
408 $this->set_alias($temp);
409 return $affRows;
412 public function reset () {
413 foreach ($this->fields as $key => $field) {
414 if ($field instanceof tdoAbstractField) {
415 $this->fields[$key] = new tdoAbstractField($key, $this->alias);
419 $this->wheres = array();
420 $this->groups = array();
421 // $this->orders = array();
422 $this->refers = array();
423 return true;
426 public function idExists ($id = null) {
427 if (is_null($id)) {
428 $id = $this->id->value;
430 if (is_null($id))
431 return false;
433 $this->id->set_modifier ('COUNT(%s)');
435 $t = sprintf ($this->id->modifier, $this->id->name);
437 $sql = $this->db->_SELECT ($t).
438 $this->db->_FROM ($this->name).
439 $this->db->_WHERE ($this->id->name.' = '.$this->escape($id));
441 $this->db->query($sql);
443 if ($this->db->getScalar()) {
444 return true;
445 } else {
446 return false;
450 protected function outputFieldList () {
451 $fields = '';
452 $f = $this->get_members();
453 // var_dump($f);
454 foreach ($f as $fieldName => $field) {
455 if ($this->isValidMember ($field)) {
457 if (!is_null ($field->modifier)) {
458 // i replaced str_replace ('%s', $curField, '%s', $curField)
459 // as sometimes I might need it for something else than %s
460 $fields .= sprintf ($field->modifier, (!is_null ($field->table) ? $field->table . '.' : '') . $field->name) .
461 $this->db->_AS($field->name) . ', ';
462 } elseif ( !$field->inWhere ()) {
463 $fields .= (!is_null ($field->table) ? $field->table.'.' : '') . $field->name . ', ';
466 } else {
467 trigger_error ($fieldName . ' is not a valid member of ' . get_class($this));
468 // throw new Exception ($fieldName . ' is not a valid member of ' . get_class($this));
469 return false;
472 return substr ($fields,0,-2);//$fields;
475 protected function outputWheres ($bIW = true) {
476 // var_dump($this->wheres);
477 return implode ($this->db->_AND(), $this->wheres);
480 protected function outputGroups () {
481 // groups
482 $groups = '';
483 $f = $this->get_members ();
484 foreach ($f as $field) {
485 if (!is_null ($field->group) ) {
486 $groups .= (!empty($groups) ? ', ' : '') .
487 $field->table . '.' . $field->name;
491 return $groups;
494 protected function outputOrders () {
495 $orders = '';
496 $f = $this->get_members();
497 foreach ($f as $field)
498 if (!is_null($field->order)) {
499 $orders .= (!empty($orders) ? ', ': '') .
500 $field->table . '.' . $field->name .
501 ($field->order == true ? ' ASC' : ' DESC');
503 return $orders;
506 protected function outputRefers () {
507 $refers= '';
508 if (!empty($this->refers) && is_array($this->refers)){
509 $rs = array_reverse ($this->refers);
510 foreach ($rs as $ref) // using the __toString magic function
511 $refers .= $ref;
513 return $refers;
516 protected function buildInherentWheres () {
517 $diff = $this->get_members();
518 // let's hope this doesn't break stuff.
519 // it's needed when we use more queries on the same instance of the object :D
521 if (is_array ($diff)) {
522 foreach ($diff as $fieldName => $field) {
523 if ( $this->isValidMember ($field) ) {
524 if (!is_null($field->value)) {
525 $this->addWhere ($field, '=', $this->escape ($field->value));
527 } else {
528 trigger_error ($fieldName . ' is not a valid member of ' . get_class($this));
529 throw new Exception ($fieldName . ' is not a valid member of ' . get_class($this));
534 // fix a bug where if we were calling a object->get(null)
535 // it ended by having a query with "where 1" - wich resulted in gettina all rows - BAD
536 if (empty ($this->wheres)) {
537 $t = $this->db->FALSE;
538 $this->wheres[] = new tdoAbstractClause ($t);
543 * function to add an abstract clause to the current object if it doesn't exist
545 * @param tdoAbstractField|tdoAbstractClause $field1
546 * @param string $condition
547 * @param string $field2
549 public function addWhere (&$field1, $condition = null, $field2 = null) {
550 if (($field1 instanceof tdoAbstractClause) && ($condition == null || $field2 == null)) {
551 $w = &$field1;
552 } else {
553 $w = new tdoAbstractClause ($field1, $condition, $field2);
555 // this might generate an infinite recursion error on some PHP > 5.2 due to object comparison
556 if (!in_array ($w, $this->wheres /*, true */)) {
557 $this->wheres[] = &$w;
561 public function addOrder (&$orderField, $asc = true) {
562 if (!($orderField instanceof tdoAbstractField) && is_string ($orderField)) {
563 $orderField = &$this->fields[$orderField];
565 if ($this->isValidMember ($orderField))
566 $orderField->set_order ($asc);
569 public function addGroup (&$groupField) {
570 if (!($groupField instanceof tdoAbstractField) && is_string ($groupField)) {
571 $groupField = &$this->fields[$groupField];
573 if (($groupField instanceof tdoAbstractField)) {
574 $groupField->set_group (true);
579 * @param int $start
580 * @param int $count
583 public function addLimit ($start = 0, $count=null) {
584 if (empty($this->limit))
585 $this->limit = $this->db->_LIMIT ($start, $count);
589 * building a normal SELECT query
591 * @param int $start
592 * @param int $end
593 * @return string
595 protected function buildSql ($start = 0, $count = 0) {
596 $sql = $this->db->_SELECT($this->outputFieldList()). ' FROM '.$this->name.' AS '.$this->alias.' ';
598 $this->buildInherentWheres(); // will it work
600 $sql .= $this->outputRefers();
602 $sql .= $this->db->_WHERE ($this->outputWheres());
604 $sql .= $this->db->_GROUP ($this->outputGroups());
606 $sql .= $this->db->_ORDER ($this->outputOrders());
608 if (empty ($this->limit)) {
609 $this->addLimit ($start, $count);
611 $sql .= $this->limit;
612 return $sql;
615 public function find ($start = 0, $count = 0) {
616 $result = $this->db->query ($this->buildSql(), $start, $count);
617 return $result;
620 public function findFirst () {
621 $this->buildInherentWheres();
623 $this->db->query($this->buildSql(), 0, 1);
624 $row = $this->db->getAssoc();
625 if (is_array($row))
626 foreach ($row as $field => $value){
627 $this->fields[$field]->value = $value;
631 public function getArray ($start = 0, $count = 0, $orderBy = null) {
632 // $this->buildInherentWheres ();
633 // generally when we getArray and don't have any where clauses
634 // we would like _all_ rows
635 //var_dump((string)$this->wheres[0], sizeof ($this->wheres) );
636 if (empty ($this->wheres) || (sizeof ($this->wheres) == 1 && (string)$this->wheres[0] == '0'))
637 $this->wheres[] = new tdoAbstractClause ($this->db->TRUE);
638 $sql = $this->buildSql ($start, $count, $orderBy);
643 $this->db->query ($sql);
644 $ret = $this->db->getArray ();
646 // in the case of an empty return - we make sure that we
647 // have the keys we need in what we return
648 if (!is_array($ret)) {
649 $ret[0] = $this->fieldsAsArray ();
652 return $ret;
656 * execute a select count () on the current object
658 * @return null
661 public function getCount() {
662 // this is bad:
663 // it takes into account the counting rows in many2many table relationshit
664 // but it does not for more than one group by
665 foreach ($this->get_members() as $fieldName => $field) {
666 if ($field->get_group() == true) {
667 $what = 'DISTINCT(' . $field->table .'.' . $fieldName . ')';
671 if (empty ($what))
672 $what = '*';
674 // made it a bit more clean and less sql portable by adding hard coded
675 // MY SQL stuff
676 $sql = $this->db->_SELECT (' COUNT(' . $what . ') ');
678 $sql .= $this->db->_FROM( $this->name. $this->db->_AS($this->alias) );
680 $this->buildInherentWheres();
682 $sql .= $this->outputRefers();
684 $sql .= $this->db->_WHERE ($this->outputWheres());
686 // this would need to be replaced with a count(distinct(group by column))
687 // because we're using many2many relations
688 // also I do not know how this behaves for:
689 // 1. multiple group by's
690 // 2. !many2many relations.
691 // $sql .= $this->db->_GROUP ($this->outputGroups());
692 // echo $sql; die;
693 $this->db->query($sql);
694 return $this->db->getScalar();
697 public function getVector() {
698 // I really don't see why we need this. ?
702 * Function to load the values of current object from an array
703 * of type field_name => field_value
704 * If strict is false, the current object can have fields that are not already
705 * present in $this->fields[]
707 * @param array $valArray
708 * @param bool $strict
710 public function loadFromArray ($valArray, $strict = true) {
711 foreach ($valArray as $fieldName => $value) {
712 if (array_key_exists ($fieldName, $this->fields)) {
713 $this->fields[$fieldName]->set_value($value);
714 } elseif (!$strict) {
715 // if the field name is not in the field list of the current object
716 // it means that the valArray object is got from an JOIN sql
717 $this->fields[$fieldName] = new tdoAbstractField ($value,'j1');
718 $this->fields[$fieldName]->set_value ($value);
724 * FIXME: make it work when composing with the same object
726 * @param tdoAbstract $incOb
727 * @return void
729 private function composeObj ($incOb) {
730 if (!($incOb instanceof tdoAbstract))
731 return false;
732 $refs = count($this->refers);
734 foreach ($incOb->refers as $alias => $ref) {
735 $tAl = $refs++;
737 $this->refers[$tAl] = $ref;//str_replace(array($alias, $aliases[$alias][1]), array($tAl, $aliases[$alias][2]), $ref);
738 $this->refers[$tAl]->set_state ($tAl);
743 * Function to execute a join between two tdoAbstract objects
745 * @param string $jType
746 * @param tdoAbstractField $thisJField
747 * @param tdoAbstract $incOb
748 * @param tdoAbstractField $incObJField
749 * @return unknown
751 public function joinWith ($jType = null, &$thisJField = null, &$incOb = null, &$incObJField = null) {
752 if (
753 !tdoAbstractJoin::isValidType ($jType) ||
754 !$this->isValidMember ($thisJField)
755 ) return false;
757 $this->composeObj ($incOb);
759 $tAl = (count($this->refers));
760 if ($tAl > 59) {
761 trigger_error ('Join aborted for table '.$this->name.': Too many tables; MySQL can only use 61 tables in a join', E_USER_NOTICE);
762 return;
763 } else {
764 $incOb->set_alias($tAl);
766 if ($thisJField == null || !($thisJField instanceof tdoAbstractField))
767 $thisJField = $this->id;
769 if ($incObJField == null || !($incObJField instanceof tdoAbstractField))
770 $incObJField = $incOb->id;
772 $this->refers[$tAl] = new tdoAbstractJoin ($jType, $this, $incOb, $thisJField, $incObJField, $tAl);
773 $this->refers[$tAl]->set_state ($tAl);
775 return $this;
779 * Wrapper method for joinWith ('INNER'..
780 * @param $thisJField
781 * @param $incOb
782 * @param $incObJField
783 * @return tdoAbstract (TODO)
785 public function innerJoin (&$thisJField = null, $incOb = null, $incObJField = null) {
786 if (is_string($incOb)) {
787 $incOb = new $incOb ($this->db);
788 // $incOb->setAlias (sizeof ($this->refers));
791 if (is_string($incObJField) && $this->isValidMember($incObJField)) {
792 $incObJField = $incOb->$incObJField;
793 // $incObJField->set_table ($incObJField->get_table());
796 $this->joinWith ('INNER', $thisJField, $incOb, $incObJField);
797 return $this;
801 * Wrapper method for joinWith ('LEFT'..
802 * @param $thisJField
803 * @param $incOb
804 * @param $incObJField
805 * @return tdoAbstract (TODO)
807 public function leftJoin (&$thisJField = null, &$incOb = null, &$incObJField = null) {
808 $this->joinWith ('LEFT', $thisJField, $incOb, $incObJField);
809 return $this;
813 * Wrapper for addWhere - assures chainability and will help migrate to
814 * a better structured object (ie, better differentiation between protected
815 * and private methods
816 * @param tdoAbstractField $field1
817 * @param string $condition
818 * @param $field2
819 * @return tdoAbstract
821 public function where (&$field1, $condition, $field2) {
822 $this->addWhere($field1, $condition, $field2);
823 return $this;
828 * Wrapper for addGroup (...
829 * @param tdoAbstractField $groupField
830 * @return tdoAbstract
832 public function group (&$groupField) {
833 $this->addGroup($groupField);
834 return $this;
838 * Wrapper for addLimit
839 * @param $start
840 * @param $count
841 * @return tdoAbstract
843 public function limit ($start, $count = null) {
844 $this->addLimit($start, $count);
845 return $this;
849 * Wrapper method for addOrder
850 * @param tdoAbstractField$orderField
851 * @param bool $asc
852 * @return tdoAbstract
854 public function order (&$orderField, $asc = true) {
855 $this->addOrder($orderField, $asc);
856 return $this;
860 * function to dump a <type>Sql
861 * problem with the field types. :D
863 * @param tdoAbstract $obj
865 static public function dumpSchema ($obj) {
866 if ($obj instanceof tdoAbstract)
867 throw new Exception('Can\'t generate sql dump');
869 $sql = 'CREATE TABLE '. $obj->name . ' (';
871 foreach ($obj->getFields() as $fieldName => $data) {
872 $sql .= $fieldName.' '.$data[0].', ';
874 $sql .= ')';
875 return $sql;