Paginator: Various fixes
[AOOS.git] / AOOSModel.php
blobb0b84f1f0929e125771dc21c28a81efbfd8ce856
1 <?php
2 define("AOOSMODEL_TYPE_STRING" , 1);
3 define("AOOSMODEL_TYPE_INTEGER" , 2);
4 define("AOOSMODEL_TYPE_ARRAY" , 4);
5 define("AOOSMODEL_TYPE_AOOSMODEL" , 8);
6 define("AOOSMODEL_TYPE_UNKNOWN" , 16);
7 define("AOOSMODEL_TYPE_EMAIL" , 32);
8 define("AOOSMODEL_TYPE_DATETIME" , 64);
10 define("AOOSMODEL_PROP_DATA_INCREASING" , 1);
11 define("AOOSMODEL_PROP_DATA_QUOTES" , 4);
12 define("AOOSMODEL_PROP_DATA_ESCAPE" , 8);
13 define("AOOSMODEL_PROP_DATA_NOHTML" , 16);
14 define("AOOSMODEL_PROP_DATA_HASH" , 32);
15 define("AOOSMODEL_PROP_DATA_STRIP" , 64);
17 define("AOOSMODEL_PROP_GUI_NOTEDITABLE" , 1<<8);
18 define("AOOSMODEL_PROP_GUI_PRIVATE" , 2<<8);
19 define("AOOSMODEL_PROP_FROM_DATABASE" , 4<<8);
20 define("AOOSMODEL_PROP_UNIQUE" , 8<<8);
22 /**
23 * Second-generation AOOSModel
24 * On the good side:
25 * - The addition of properties allowing easy per-column settings
26 * @author Sebastian Skejø
28 class AOOSModel extends AOOSModule {
29 const INTERN_DATA_UPDATED = 1;
30 const INTERN_DATA_REMOVED = 2;
31 const INTERN_DATA_INSERTED = 3;
33 private $_data = array();
34 private $_index = array();
35 private $_props = array();
36 private $_types = array();
37 private $_intern = array();
39 private $_source = null;
40 private $_table = null;
41 private $_connection = null;
44 private $_rows = 0;
45 private $_curRow = 0;
46 private $_curCol = 0;
48 /**
49 * Standard destructor
51 public function __destruct() {
54 /**
55 * Transforms to a printable string
57 public function __toString() {
58 return nl2br(str_replace(" ", "&nbsp;", print_r($this->data(), true)));
61 /**
62 * Appends a row to the end of the model with 0 as values unless $values is
63 * provided. Appended rows will NOT be inserted into database when AOOSModel::save() is called.
64 * @param $values If given null an empty row will be added. Otherwise an
65 * associative array or an AOOSModelRow is assumed.
66 * @param $ignore Flags to ignores when inserting data.
67 * @see insert
68 * @return true
70 public function appendRow($values = null, $ignore = 0) {
71 if ($values instanceof AOOSModelRow) {
72 $values = $values->toArray();
74 $this->_data[] = new AOOSModelRow($this, $this->columnIndex()); //array_fill_keys($this->_index, 0);
75 $this->_rows++;
76 if (is_array($values)) {
77 $index = $this->columnIndex();
78 $last = $this->rows()-1;
79 foreach ($index as $i) {
80 if (in_array($i, array_keys($values))) {
81 $this->setData($last, $i, $values[$i], $ignore);
83 else {
84 $this->setData($last, $i, null, $ignore);
88 return true;
91 /**
92 * Sets the data of a single field. Fields updated this way will NOT be
93 * updated in the database when AOOSModel::save() is called. If the type of
94 * the inserted data doesn't match the column type, an AOOSTypeException is
95 * thrown. If $row is less than 0 or greater that the row count, an
96 * AOOSException is throw.
97 * @param $row An integer representing the row
98 * @param $column A column index
99 * @param $value Value
100 * @param $ignore Flags to ignore when inserting data
101 * @return bool
103 public function setData($row, $column, $value, $ignore = 0) {
104 if (!$this->inColumnIndex($column)) {
105 // Exception?
106 return false;
108 $props = $this->_props[$column];
109 $type = $this->_types[$column];
111 try {
112 $this->_checkType($value, $type);
114 catch (AOOSTypeException $e) {
115 throw $e;
116 return false;
119 try {
120 $value = $this->_fixDataProps($column, $value, $props, $ignore);
122 catch (AOOSException $e) {
123 throw $e;
124 return false;
127 // XXX
128 if ($row < 0 || $row > $this->rows()) {
129 throw new AOOSException($this->core(), $this->tr("row_out_of_bounds"));
130 return false;
132 $this->_data[$row]->setData($column, $value);
133 return true;
138 public function setDataRowSingle($row, $column, $value, $ignore = 0) {
139 if (!$this->inColumnIndex($column)) {
140 // Exception?
141 return false;
143 $props = $this->_props[$column];
144 $type = $this->_types[$column];
146 try {
147 $this->_checkType($value, $type);
149 catch (AOOSTypeException $e) {
150 throw $e;
151 return false;
154 try {
155 $value = $this->_fixDataProps($column, $value, $props, $ignore);
157 catch (AOOSException $e) {
158 throw $e;
159 return false;
162 $row->setData($column, $value);
163 return true;
166 public function setDataRow($row, $values, $ignore = 0) {
167 if (is_int($row)) {
168 $row = $this->getRow($row);
171 foreach ($values as $key => $value) {
172 $this->setDataRowSingle($row, $key, $value, $ignore);
176 private function _checkType($value, $type) {
177 // Only type-props
178 $correct = false;
179 $continue = true;
180 if ($type & AOOSMODEL_TYPE_UNKNOWN) {
181 $correct = true;
182 $continue = false;
184 elseif($continue && $type & AOOSMODEL_TYPE_STRING) {
185 $correct = is_string($value);
186 $continue = false;
188 elseif($continue && $type & AOOSMODEL_TYPE_INTEGER) {
189 $correct = is_integer((int)$value);
190 $continue = false;
192 elseif($continue && $type & AOOSMODEL_TYPE_ARRAY) {
193 $correct = is_array($value);
194 $continue = false;
196 elseif($continue && $type & AOOSMODEL_TYPE_AOOSMODEL) {
197 $correct = $value instanceof AOOSModel;
198 $continue = false;
200 elseif($continue && $type & AOOSMODEL_TYPE_EMAIL) {
201 $correct = preg_match("^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$", $value);
202 $continue = false;
204 elseif($continue && $type & AOOSMODEL_TYPE_DATETIME) {
205 $correct = is_integer((int) $value) && strlen((string) $value) == 10;
206 $continue = false;
210 if (!$correct) {
211 throw new AOOSTypeException($this->core(), "", "When inserting ".$value);
212 return false;
214 return true;
217 private function _fixDataProps($col, $value, $props, $ignore = 0) {
218 // Only data props
219 if ($props & AOOSMODEL_PROP_DATA_NOHTML && !(AOOSMODEL_PROP_DATA_NOHTML & $ignore)) {
220 $value = htmlspecialchars($value);
223 if ($props & AOOSMODEL_PROP_DATA_ESCAPE && !(AOOSMODEL_PROP_DATA_ESCAPE & $ignore)) {
224 $value = mysql_real_escape_string($value);
227 if ($props & AOOSMODEL_PROP_DATA_STRIP && !(AOOSMODEL_PROP_DATA_STRIP & $ignore)) {
228 $value = rtrim(trim($value));
231 if ($props & AOOSMODEL_PROP_DATA_HASH && !(AOOSMODEL_PROP_DATA_HASH & $ignore)) {
232 $value = hash("sha256", $value);
234 if ($props & AOOSMODEL_PROP_DATA_QUOTES && !(AOOSMODEL_PROP_DATA_QUOTES & $ignore)
235 && !($this->_types[$col] & (AOOSMODEL_TYPE_INTEGER|AOOSMODEL_TYPE_DATETIME))) {
236 $first = substr($value, 0,1);
237 if (($first != "'" && $first != '"' && $first != substr($value, -1)) || $value == "") {
238 $value = "'".$value."'";
241 if ($props & AOOSMODEL_PROP_DATA_INCREASING && !(AOOSMODEL_PROP_DATA_INCREASING & $ignore)) {
242 if ($this->rows() == 1 && $value) {
243 $value = $value;
245 elseif ($this->rows() == 1) {
246 $value = 0;
248 else {
249 $r = $this->getRow(-2);
250 $value = $r->$col + 1;
253 if ($props & AOOSMODEL_PROP_UNIQUE && !(AOOSMODEL_PROP_UNIQUE & $ignore)) {
254 $cols = $this->getColumn($col);
255 if (in_array($value, $cols, true)) {
256 throw new AOOSException($this->core(), $this->tr("not_unique"));
257 return false;
261 return $value;
265 * Returns a to-dimensional array containing the data.
266 * @return array
268 public function data() {
269 return $this->_data;
273 * Returns the column with the name $column
274 * @param $column The name of the column
275 * @param $offset Column values starts at this row
276 * @param $length The array of column values has this length
277 * @return array
279 public function getColumn($column, $start = 0, $length = null) {
280 if (!$this->inColumnIndex($column)) {
281 return false;
283 if ($length === null) {
284 $length = $this->rows();
286 if ($start < 0) {
287 $start = $this->rows() + $start;
289 $a = array();
290 for ($i= $start; $length > 0; $length--) {
291 $r = $this->getRow($i);
292 $a[] = $r[$column];
293 $i++;
295 return $a;
299 * Returns an AOOSModelRow object
300 * @param $row The row number.
301 * @return AOOSModel
303 public function getRow($row) {
304 if ($row < 0) {
305 $row = $this->rows() + $row;
307 return $this->_data[$row];
311 * Returns the row number
312 * @param $row The AOOSModelRow object
313 * @return integer
315 /* public function getRowNumber($row) {
316 foreach*/
319 * Returns the number of rows
320 * @return integer
322 public function rows() {
323 return $this->_rows;
327 * Returns only columns which match the given properties
328 * @param $props Properties
330 public function filter($props = 0, $offset = 0, $length = null) {
331 if ($length === null) {
332 $length = $this->rows();
334 $a = array();
335 for ($i = $offset; $length > 0; $length--) {
336 $r = $this->getRow($i);
337 $ra = array();
338 foreach ($this->_props as $col => $colprops) {
339 if ($colprops & $props) {
340 $ra[$col] = $r[$col];
343 $a[] = $ra;
344 $i++;
346 return $a;
350 * Returns the first row from $start which matches $match
351 * @param $match An AOOSModelRow or associative array in which keys should match column
352 * indices and values should match values. Values of the match array can be
353 * regular expressions.
354 * @param $start An integer representing the row from which search should.
355 * Defaults to 0.
356 * start
357 * @return Integer
358 * @sa findAll
360 public function find($match, $start = 0) {
361 if ($match && !($match = $this->_fixWhere($match))) {
362 return false;
364 for ($i = $start; $i < $this->rows(); $i++) {
365 $r = $this->getRow($i);
366 if ($r->match($match)) {
367 $this->_lastMatchRow = $i;
368 return $r;
371 return false;
375 * Returns all rows which match $match, starting from $start
376 * @param $match The row to match against.
377 * @param $start An integer representing the row from which to start from.
378 * Defaults to 0.
379 * @return An array containing integers.
380 * @sa find
382 public function findAll($match, $start = 0, $length = null) {
383 if ($length === null) {
384 $length = $this->rows();
386 $i = $start;
387 $a = array();
388 while (true) {
389 if (false === ($r = $this->find($match, $i))) {
390 break;
392 $a[] = $r;
393 $i = $this->_lastMatchRow+1;
395 $a = array_slice($a, 0, $length);
396 return count($a) ? $a : false;
400 * Sets a columns properties to be $property
401 * @param $column Column index name
402 * @param $type The type of the column
403 * @param $property An integer representing the property
404 * @return true
406 public function setProperty($column, $type, $property) {
407 if (!$this->inColumnIndex($column)) {
408 // Exception?
409 return false;
411 if ($property & AOOSMODEL_PROP_FROM_DATABASE && !($type & AOOSMODEL_TYPE_INTEGER)) {
412 $property |= AOOSMODEL_PROP_DATA_QUOTES;
414 $this->_types[$column] = $type;
415 $this->_props[$column] = $property;
416 return true;
420 * Returns the properties of the given $column
421 * @param $column Name of the column
422 * @return integer
424 public function getProperty($column) {
425 if (!$this->inColumnIndex($column)) {
426 return false;
428 return $this->_props[$column];
432 * Toggles a given property using the ... operation.
433 * @param $column Column index name
434 * @param $property An integer representing the property to toggle
435 * @return true
437 public function toggleProperty($column, $property) {
438 if (!$this->inColumnIndex($column)) {
439 // Exception?
440 return false;
442 if ($property & AOOSMODEL_PROP_FROM_DATABASE) {
443 $property |= AOOSMODEL_PROP_DATA_QUOTES;
445 $this->_props[$column] ^= $property;
446 return true;
450 * Returns the type of the given $column
451 * @param $column Name of the column
452 * @return string
454 public function getType($column) {
455 if (!$this->inColumnIndex($column)) {
456 // Exception?
457 return false;
459 $type = $this->_types[$column];
460 $typename = 0;
461 $continue = true;
462 if ($type & AOOSMODEL_TYPE_UNKNOWN) {
463 $typename = "UNKNOWN";
464 $continue = false;
466 elseif($continue && $type & AOOSMODEL_TYPE_STRING) {
467 $typename = "STRING";
468 $continue = false;
470 elseif($continue && $type & AOOSMODEL_TYPE_INTEGER) {
471 $typename = "INTEGER";
472 $continue = false;
474 elseif($continue && $type & AOOSMODEL_TYPE_ARRAY) {
475 $typename = "ARRAY";
476 $continue = false;
478 elseif($continue && $type & AOOSMODEL_TYPE_AOOSMODEL) {
479 $typename = "AOOSModel";
480 $continue = false;
483 return $type;
487 * Returns true if the given $property is set, otherwise return false
488 * @param $column Column index name
489 * @param $property An integer representing the property
490 * @return boolean
492 public function checkProperty($column, $property) {
493 if (!$this->inColumnIndex($column)) {
494 // Exception?
495 return false;
497 if ($this->_props[$column] & $property) {
498 return true;
500 return false;
504 * Sets the column index to be $index. Note: this resets the properties AND
505 * data
506 * If $index isn't an array an AOOSException is thrown.
507 * @param $index An array containing the column indices
508 * @return true
510 public function setColumnIndex($index) {
511 if (!is_array($index)) {
512 throw new AOOSTypeException($this->core(), $this->tr("not_array"));
513 return false;
515 $this->_index = $index;
516 $this->_props = array_fill_keys($index, 0);
517 return true;
521 * Returns the column index
522 * @return array
524 public function columnIndex() { return $this->_index; }
527 * Returns true if the given $index is in the column index. Otherwise returns
528 * false.
529 * @param $index Name of the index
530 * @return bool
532 public function inColumnIndex($index) {
533 return in_array($index, $this->_index);
538 * Sets the table from in which data is contained
539 * @param $table Table name
540 * @return true
542 public function setTable($table) {
543 $this->_table = $this->core()->getSetting("DBPrefix").$table;
544 return true;
548 * Returns the table name
549 * @return string
551 public function table() {
552 return $this->_table;
556 * Sets the type of source from which data is fetched, inserted and deleted.
557 * @param $source The type of source, e.g. "mysql".
558 * @return true
560 public function setSource($source) {
561 $this->_source = strtolower($source);
562 return true;
566 * Returns the type of source in a string
567 * @return string
569 public function source() {
570 return $this->_source;
574 * Populates the model. If the model is successfully populated $this is
575 * returned. Otherwise false is returned.
576 * @param $where An associative array containing the where-keys and values.
577 * This means that array("FOO" => "BAR") will expand to WHERE FOO = 'BAR'
578 * @param $order An array with two rows. First row defines the row to order
579 * by, the second row defines the order. This means that array("BAZ",
580 * "DESC") will expand to ORDER BY BAZ DESC.
581 * @param $limit An array with two rows, where the first row is the start
582 * limit and the second row is the end limit. This means that array(0,2)
583 * will expand into LIMIT 0,2
584 * @return
586 public function populate($where = null, $order = null, $limit = null) {
587 $this->_reset();
588 if ($where && !($where = $this->_fixWhere($where))) {
589 return false;
591 $fields = array();
592 foreach ($this->_props as $col => $prop) {
593 if ($prop & AOOSMODEL_PROP_FROM_DATABASE) {
594 $fields[] = $col;
597 if (false === ($values = $this->_connection()->select($fields, $where, $order, $limit))) {
598 throw new AOOSException($this->core(), $this->tr("couldnt_select_values"));
599 return false;
601 foreach ($values as $row) {
602 $this->appendRow($row, AOOSMODEL_PROP_DATA_HASH);
604 return $this;
608 * Inserts the given array into the model. Note that data isn't saved to
609 * database before calling AOOSModel::save()
610 * @param $values An associative array, in which the array keys corresponds
611 * to column indices and the array values are the value that should be
612 * inserted.
614 public function insert($values) {
615 $this->appendRow($values);
616 $values = $this->filter(AOOSMODEL_PROP_FROM_DATABASE, -1, 1);
617 $this->_intern[] = array(AOOSModel::INTERN_DATA_INSERTED, $values);
618 return true;
622 * Deletes the row which matches $where. As with AOOSModel::insert() and
623 * AOOSModel::update(), changes aren't saved to database before AOOSModel::save() is
624 * called. An AOOSException is thrown if no rows match $where.
625 * @param $where Either an associative array containing the where-keys and values.
626 * This means that array("FOO" => "BAR") will expand to WHERE FOO = 'BAR' or
627 * an integer representing a row number.
628 * @param $limit An array with two rows, where the first row is the start
629 * limit and the second row is the end limit. This means that array(0,2)
630 * will expand into LIMIT 0,2 or a single integer
632 public function remove($where, $limit = null) {
633 $limit = $this->_fixLimit($limit);
634 if (is_array($where) || $where instanceof AOOSModelRow) {
635 if (!($w = $this->findAll($where, $limit[0], $limit[1]))) {
636 throw new AOOSException($this->core(), $this->tr("no_rows_found"), "", true, 2);
637 return false;
640 elseif (is_integer($where)) {
641 $w = array($where);
642 $where = $this->getRow($where);
644 else {
645 return false;
647 foreach($w as $row) {
648 unset($row);//$this->_data[$nr]);
649 $this->_rows--;
651 $where = $this->_fixWhere($where);
652 $this->_intern[] = array(AOOSModel::INTERN_DATA_REMOVED, $where, $limit);
653 return true;
657 * Updates the row that matches $where with the values, $values. Changes
658 * aren't saved to database until AOOSModel::save() is called..
659 * @param $values An associative array, in which the array keys matches
660 * column indices and the array values are the values to use to update the
661 * selected row
662 * @param $where Either an associative array containing the where-keys and values.
663 * This means that array("FOO" => "BAR") will expand to WHERE FOO = 'BAR' or
664 * an integer representing a row number
665 * @param $limit An array with two rows, where the first row is the start
666 * limit and the second row is the end limit. This means that array(0,2)
667 * will expand into LIMIT 0,2
669 public function update($values, $where, $limit = null) {
670 $limit = $this->_fixLimit($limit);
671 $w = $this->findAll($where, $limit[0], $limit[1]);
672 foreach ($w as $row) {
673 $this->setDataRow($row, $values);
675 $where = $this->_fixWhere($where);
676 $values = $this->_fixWhere($values);
677 $this->_intern[] = array(AOOSModel::INTERN_DATA_UPDATED, $values, $where, $limit);
678 return 0;
682 * Creates the model as a new table in the database.
683 * @return bool
685 public function create() {
686 if (!$this->_connection()) {
687 return false;
689 return $this->_connection()->create($this->_types, $this->_props);
693 * Creates the model as a new table in the database.
694 * @return bool
696 public function drop() {
697 if (!$this->_connection()) {
698 return false;
700 return $this->_connection()->drop();
704 * Saves the changes caused by AOOSModel::insert(), AOOSModel::remove() or
705 * AOOSModel::update()
707 public function save() {
708 if (!$this->_connection()) {
709 return false;
711 foreach ($this->_intern as $key => $row) {
712 switch($row[0]) {
713 case(AOOSModel::INTERN_DATA_INSERTED):
714 if (!$this->_connection()->insert($row[1])) {
715 return false;
717 break;
718 case(AOOSModel::INTERN_DATA_REMOVED):
719 if (!$this->_connection()->remove($row[1], $row[2])) {
720 return false;
722 case(AOOSModel::INTERN_DATA_UPDATED):
723 if (!$this->_connection()->update($row[1], $row[2], $row[3])) {
724 return false;
727 unset($this->_intern[$key]);
729 return true;
732 private function _connect() {
733 if ($this->_connection) {
734 return $this->_connection;
736 if (!$this->source() || !$this->table()) {
737 throw new AOOSException($this->core(), $this->tr("no_source_or_table_set"));
738 return false;
740 switch ($this->source()) {
741 case("mysql"):
742 require_once("lib/AOOSMysqlInterface.php");
743 $this->_connection = new AOOSMysqlInterface($this->core());
744 break;
746 $this->_connection->setTable($this->table());
749 private function _connection() {
750 if (!$this->_connection) {
751 try {
752 $this->_connect();
754 catch (AOOSException $e) {
755 throw $e;
756 return false;
759 return $this->_connection;
763 * \internal Fixes where clauses so that the given values are correctly
764 * formatted according to the properties for their column
766 private function _fixWhere($where) {
767 $ci = $this->columnIndex();
768 if ($where instanceof AOOSModelRow) {
769 $where = $where->toArray();
771 if (!is_array($where)) {
772 return false;
775 foreach ($where as $key => $value) {
776 if (!in_array($key, $ci)) {
777 unset($where[$ci]);
778 continue;
780 $where[$key] = $this->_fixDataProps($key, $value, $this->getProperty($key));
782 return $where;
786 * \internal Fixes a limit
788 private function _fixLimit($l) {
789 $limit = array(0,$this->rows());
790 if (is_integer($l)) {
791 $limit[1] = $l;
793 elseif(is_array($l)) {
794 $limit = $l;
796 return $limit;
799 private function _reset() {
800 $this->_data = array();
801 $this->_connect();
802 $this->_rows = 0;
806 * Dummy function
808 public function dataModelDefinition() { return 0; }
810 static public function stripQuotes($string) {
811 if (substr($string,0,1) == "'") {
812 $string = substr($string,1,strlen($string)-2);
814 return $string;
819 class AOOSModelRow extends AOOSModule implements ArrayAccess {
820 private $_data = array();
821 private $_columnIndex = array();
823 public function __construct($parent, $columnIndex) {
824 parent::__construct($parent);
825 $this->_columnIndex = $columnIndex;
826 $this->_data = array_fill_keys($columnIndex, null);
829 public function dataModelDefinition() {
830 return 0;
833 public function __set($name, $value) {
834 return $this->parent()->setDataRow($this, $name, $value);
837 public function __get($name) {
838 return $this->_data[$name];
841 public function __toString() {
842 $str = "Row:\n";
843 foreach ($this->_data as $key => $value) {
844 $str .= "\t[".$key."] => ".$value."\n";
846 return $str;
849 public function toArray() {
850 return $this->_data;
853 public function columnIndex() {
854 return $this->_columnIndex;
857 // Array Access
858 public function offsetExists($offset) {
859 return in_array($offset, array_keys($_data));
862 public function offsetSet($offset, $value) {
863 $this->$offset = $value;
866 public function offsetGet($offset) {
867 return $this->$offset;
870 public function offsetUnset($offset) {
871 unset($this->_data[$offset]);
875 * Simply inserts the data into the row. This should NOT be used since it
876 * ignores column flags completely
878 public function setData($name, $value) {
879 $this->_data[$name] = $value;
880 return true;
883 public function save() {
884 return $this->parent()->save();
887 public function match($match) {
888 foreach ($match as $key => $value) {
889 $matchString = "/".str_replace("/","\/", $value)."/";
890 if (!preg_match($matchString, $this->_data[$key])) {
891 return false;
894 return true;
897 // vim: textwidth=80