find() is using preg_match instead of ==
[AOOS.git] / AOOSModel.php
blob2914238923a8ee16c705bd67282cabcd2c63c611
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);
9 define("AOOSMODEL_PROP_DATA_INCREASING" , 1);
10 define("AOOSMODEL_PROP_DATA_DECREASING" , 2);
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 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 $this->_data[] = array_fill_keys($this->_index, 0);
72 $this->_rows++;
73 if (is_array($values)) {
74 $index = $this->columnIndex();
75 $last = $this->rows()-1;
76 foreach ($values as $key => $value) {
77 if (in_array($key, $index)) {
78 $this->setData($last, $key, $value, $ignore);
82 return true;
85 /**
86 * Sets the data of a single field. Fields updated this way will NOT be
87 * updated in the database when AOOSModel::save() is called. If the type of
88 * the inserted data doesn't match the column type, an AOOSTypeException is
89 * thrown. If $row is less than 0 or greater that the row count, an
90 * AOOSException is throw.
91 * @param $row An integer representing the row
92 * @param $column A column index
93 * @param $value Value
94 * @param $ignore Flags to ignore when inserting data
95 * @return bool
97 public function setData($row, $column, $value, $ignore = 0) {
98 if (!$this->inColumnIndex($column)) {
99 // Exception?
100 return false;
102 $props = $this->_props[$column];
103 $type = $this->_types[$column];
105 try {
106 $this->_checkType($value, $type);
108 catch (AOOSTypeException $e) {
109 throw $e;
110 return false;
113 $value = $this->_fixDataProps($value, $props, $ignore);
115 // XXX
116 if ($row < 0 || $row > $this->rows()) {
117 throw new AOOSException($this->core(), $this->tr("row_out_of_bounds"));
118 return false;
120 $this->_data[$row][$column] = $value;
121 return true;
125 * Sets a row of data
126 * @param $row The row number
127 * @param $values An associative array with column indices as keys and new values as
128 * array values
129 * @param $ignore Flags to ignore when inserting data
130 * @return bool
131 * @sa setData
133 public function setDataRow($row, $values, $ignore = 0) {
134 foreach ($values as $index => $value) {
135 if (!$this->setData($row, $index, $value, $ignore)) {
136 return false;
139 return true;
142 private function _checkType($value, $type) {
143 // Only type-props
144 $correct = false;
145 $continue = true;
146 if ($type & AOOSMODEL_TYPE_UNKNOWN) {
147 $correct = true;
148 $continue = false;
150 elseif($continue && $type & AOOSMODEL_TYPE_STRING) {
151 $correct = is_string($value);
152 $continue = false;
154 elseif($continue && $type & AOOSMODEL_TYPE_INTEGER) {
155 $correct = is_integer((int)$value);
156 $continue = false;
158 elseif($continue && $type & AOOSMODEL_TYPE_ARRAY) {
159 $correct = is_array($value);
160 $continue = false;
162 elseif($continue && $type & AOOSMODEL_TYPE_AOOSMODEL) {
163 $correct = $value instanceof AOOSModel;
164 $continue = false;
166 elseif($continue && $type & AOOSMODEL_TYPE_EMAIL) {
167 $correct = preg_match("^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$", $value);
168 $continue = false;
171 if (!$correct) {
172 throw new AOOSTypeException($this->core(), "", "When inserting ".$value);
173 return false;
175 return true;
178 private function _fixDataProps($value, $props, $ignore = 0) {
179 // Only data props
180 if ($props & AOOSMODEL_PROP_DATA_NOHTML && !(AOOSMODEL_PROP_DATA_NOHTML & $ignore)) {
181 $value = htmlspecialchars($value);
184 if ($props & AOOSMODEL_PROP_DATA_ESCAPE && !(AOOSMODEL_PROP_DATA_ESCAPE & $ignore)) {
185 $value = mysql_real_escape_string($value);
188 if ($props & AOOSMODEL_PROP_DATA_STRIP && !(AOOSMODEL_PROP_DATA_STRIP & $ignore)) {
189 $value = rtrim(trim($value));
192 if ($props & AOOSMODEL_PROP_DATA_HASH && !(AOOSMODEL_PROP_DATA_HASH & $ignore)) {
193 $value = hash("sha256", $value);
195 if ($props & AOOSMODEL_PROP_DATA_QUOTES && !(AOOSMODEL_PROP_DATA_QUOTES & $ignore)) {
196 $first = substr($value, 0,1);
197 if ($first != "'" && $first != '"' && $first != substr($value, -1)) {
198 $value = "'".$value."'";
202 return $value;
206 * Returns a to-dimensional array containing the data.
207 * @return array
209 public function data() {
210 return $this->_data;
214 * Returns the column with the name $column
215 * @param $column The name of the column
216 * @param $offset Column values starts at this row
217 * @param $length The array of column values has this length
218 * @return array
220 public function getColumn($column, $start = 0, $length = null) {
221 if (!$this->inColumnIndex($column)) {
222 return false;
224 if ($length === null) {
225 $length = $this->rows();
227 if ($start < 0) {
228 $start = $this->rows() + $start;
230 $a = array();
231 for ($i= $start; $length > 0; $length--) {
232 $r = $this->getRow($i);
233 $a[] = $r[$column];
234 $i++;
236 return $a;
240 * Returns an array containing the columns of the given $row
241 * @param $row The row number. If this is negative the row selected is the
242 * $row'th number from the end.
243 * @return array
245 public function getRow($row) {
246 return array_pop(array_slice($this->data(), $row, 1));
250 * Returns the number of rows
251 * @return integer
253 public function rows() {
254 return $this->_rows;
258 * Returns only columns which match the given properties
259 * @param $props Properties
261 public function filter($props = 0, $offset = 0, $length = null) {
262 if ($length === null) {
263 $length = $this->rows();
265 $a = array();
266 for ($i = $offset; $length > 0; $length--) {
267 $r = $this->getRow($i);
268 $ra = array();
269 foreach ($this->_props as $col => $colprops) {
270 if ($colprops & $props) {
271 $ra[$col] = $r[$col];
274 $a[] = $ra;
275 $i++;
277 return $a;
281 * Returns the first row from $start which matches $match
282 * @param $match An associative array in which keys should match column
283 * indices and values should match values. Values of the match array can be
284 * regular expressions.
285 * @param $start An integer representing the row from which search should.
286 * Defaults to 0.
287 * start
288 * @return Integer
289 * @sa findAll
291 public function find($match, $start = 0) {
292 if ($match && !($match = $this->_fixWhere($match))) {
293 return false;
295 $fkey = key($match);
296 for ($i = $start; $i < $this->rows(); $i++) {
297 $r = $this->getRow($i);
298 if (preg_match($r[$fkey], "/".$match[$fkey]."/")) {
299 foreach ($match as $key => $val) {
300 if (!preg_match($r[$key], "/".$val."/")) {
301 continue 2;
304 return $i;
307 return false;
311 * Returns all rows which match $match, starting from $start
312 * @param $match The row to match against.
313 * @param $start An integer representing the row from which to start from.
314 * Defaults to 0.
315 * @return An array containing integers.
316 * @sa find
318 public function findAll($match, $start = 0, $length = null) {
319 if ($length === null) {
320 $length = $this->rows();
322 $i = $start;
323 $a = array();
324 while (true) {
325 if (!($f = $this->find($match, $i))) {
326 break;
328 $a[] = $f;
329 $i = ++$f;
331 $a = array_slice($a, 0, $length);
332 return count($a) ? $a : false;
336 * Sets a columns properties to be $property
337 * @param $column Column index name
338 * @param $type The type of the column
339 * @param $property An integer representing the property
340 * @return true
342 public function setProperty($column, $type, $property) {
343 if (!$this->inColumnIndex($column)) {
344 // Exception?
345 return false;
347 if ($property & AOOSMODEL_PROP_FROM_DATABASE) {
348 $property |= AOOSMODEL_PROP_DATA_QUOTES;
350 $this->_types[$column] = $type;
351 $this->_props[$column] = $property;
352 return true;
356 * Returns the properties of the given $column
357 * @param $column Name of the column
358 * @return integer
360 public function getProperty($column) {
361 if (!$this->inColumnIndex($column)) {
362 return false;
364 return $this->_props[$column];
368 * Toggles a given property using the ... operation.
369 * @param $column Column index name
370 * @param $property An integer representing the property to toggle
371 * @return true
373 public function toggleProperty($column, $property) {
374 if (!$this->inColumnIndex($column)) {
375 // Exception?
376 return false;
378 if ($property & AOOSMODEL_PROP_FROM_DATABASE) {
379 $property |= AOOSMODEL_PROP_DATA_QUOTES;
381 $this->_props[$column] ^= $property;
382 return true;
386 * Returns the type of the given $column
387 * @param $column Name of the column
388 * @return string
390 public function getType($column) {
391 if (!$this->inColumnIndex($column)) {
392 // Exception?
393 return false;
395 $type = $this->_types[$column];
396 $typename = 0;
397 $continue = true;
398 if ($type & AOOSMODEL_TYPE_UNKNOWN) {
399 $typename = "UNKNOWN";
400 $continue = false;
402 elseif($continue && $type & AOOSMODEL_TYPE_STRING) {
403 $typename = "STRING";
404 $continue = false;
406 elseif($continue && $type & AOOSMODEL_TYPE_INTEGER) {
407 $typename = "INTEGER";
408 $continue = false;
410 elseif($continue && $type & AOOSMODEL_TYPE_ARRAY) {
411 $typename = "ARRAY";
412 $continue = false;
414 elseif($continue && $type & AOOSMODEL_TYPE_AOOSMODEL) {
415 $typename = "AOOSModel";
416 $continue = false;
419 return $typename;
423 * Returns true if the given $property is set, otherwise return false
424 * @param $column Column index name
425 * @param $property An integer representing the property
426 * @return boolean
428 public function checkProperty($column, $property) {
429 if (!$this->inColumnIndex($column)) {
430 // Exception?
431 return false;
433 if ($this->_props[$column] & $property) {
434 return true;
436 return false;
440 * Sets the column index to be $index. Note: this resets the properties AND
441 * data
442 * If $index isn't an array an AOOSException is thrown.
443 * @param $index An array containing the column indices
444 * @return true
446 public function setColumnIndex($index) {
447 if (!is_array($index)) {
448 throw new AOOSTypeException($this->core(), $this->tr("not_array"));
449 return false;
451 $this->_index = $index;
452 $this->_props = array_fill_keys($index, 0);
453 return true;
457 * Returns the column index
458 * @return array
460 public function columnIndex() { return $this->_index; }
463 * Returns true if the given $index is in the column index. Otherwise returns
464 * false.
465 * @param $index Name of the index
466 * @return bool
468 public function inColumnIndex($index) {
469 return in_array($index, $this->_index);
474 * Sets the table from in which data is contained
475 * @param $table Table name
476 * @return true
478 public function setTable($table) {
479 $this->_table = $this->core()->getSetting("DBPrefix").$table;
480 return true;
484 * Returns the table name
485 * @return string
487 public function table() {
488 return $this->_table;
492 * Sets the type of source from which data is fetched, inserted and deleted.
493 * @param $source The type of source, e.g. "mysql".
494 * @return true
496 public function setSource($source) {
497 $this->_source = strtolower($source);
498 return true;
502 * Returns the type of source in a string
503 * @return string
505 public function source() {
506 return $this->_source;
510 * Populates the model. If the model is successfully populated $this is
511 * returned. Otherwise false is returned.
512 * @param $where An associative array containing the where-keys and values.
513 * This means that array("FOO" => "BAR") will expand to WHERE FOO = 'BAR'
514 * @param $order An array with two rows. First row defines the row to order
515 * by, the second row defines the order. This means that array("BAZ",
516 * "DESC") will expand to ORDER BY BAZ DESC.
517 * @param $limit An array with two rows, where the first row is the start
518 * limit and the second row is the end limit. This means that array(0,2)
519 * will expand into LIMIT 0,2
520 * @return
522 public function populate($where = null, $order = null, $limit = null) {
523 $this->_reset();
524 if ($where && !($where = $this->_fixWhere($where))) {
525 return false;
527 $fields = array();
528 foreach ($this->_props as $col => $prop) {
529 if ($prop & AOOSMODEL_PROP_FROM_DATABASE) {
530 $fields[] = $col;
533 if (!($values = $this->_connection()->select($fields, $where, $order, $limit))) {
534 throw new AOOSException($this->core(), $this->tr("couldnt_select_values"));
535 return false;
537 foreach ($values as $row) {
538 $this->appendRow($row, AOOSMODEL_PROP_DATA_HASH);
540 return $this;
544 * Inserts the given array into the model. Note that data isn't saved to
545 * database before calling AOOSModel::save()
546 * @param $values An associative array, in which the array keys corresponds
547 * to column indices and the array values are the value that should be
548 * inserted.
550 public function insert($values) {
551 $this->appendRow($values);
552 $values = $this->filter(AOOSMODEL_PROP_FROM_DATABASE, -1, 1);
553 $this->_intern[] = array(AOOSModel::INTERN_DATA_INSERTED, $values);
554 return true;
558 * Deletes the row which matches $where. As with AOOSModel::insert() and
559 * AOOSModel::update(), changes aren't saved to database before AOOSModel::save() is
560 * called. An AOOSException is thrown if no rows match $where.
561 * @param $where Either an associative array containing the where-keys and values.
562 * This means that array("FOO" => "BAR") will expand to WHERE FOO = 'BAR' or
563 * an integer representing a row number.
564 * @param $limit An array with two rows, where the first row is the start
565 * limit and the second row is the end limit. This means that array(0,2)
566 * will expand into LIMIT 0,2 or a single integer
568 public function remove($where, $limit = null) {
569 $limit = $this->_fixLimit($limit);
570 if (is_array($where)) {
571 if (!($w = $this->findAll($where, $limit[0], $limit[1]))) {
572 throw new AOOSException($this->core(), $this->tr("no_rows_found"), "", true, 2);
573 return false;
576 elseif (is_integer($where)) {
577 $w = array($where);
578 $where = $this->getRow($where);
580 else {
581 return false;
583 foreach($w as $row) {
584 unset($this->_data[$row]);
585 $this->_rows--;
587 $where = $this->_fixWhere($where);
588 $this->_intern[] = array(AOOSModel::INTERN_DATA_REMOVED, $where, $limit);
589 return 0;
593 * Updates the row that matches $where with the values, $values. Changes
594 * aren't saved to database until AOOSModel::save() is called..
595 * @param $values An associative array, in which the array keys matches
596 * column indices and the array values are the values to use to update the
597 * selected row
598 * @param $where Either an associative array containing the where-keys and values.
599 * This means that array("FOO" => "BAR") will expand to WHERE FOO = 'BAR' or
600 * an integer representing a row number
601 * @param $limit An array with two rows, where the first row is the start
602 * limit and the second row is the end limit. This means that array(0,2)
603 * will expand into LIMIT 0,2
605 public function update($values, $where, $limit = null) {
606 $limit = $this->_fixLimit($limit);
607 $w = $this->findAll($where, $limit[0], $limit[1]);
608 foreach ($w as $row) {
609 $this->setDataRow($row, $values);
611 $where = $this->_fixWhere($where);
612 $values = $this->_fixWhere($values);
613 $this->_intern[] = array(AOOSModel::INTERN_DATA_UPDATED, $values, $where, $limit);
614 return 0;
618 * Creates the model as a new table in the database.
619 * @return bool
621 public function create() {
622 if (!$this->_connection()) {
623 return false;
625 return $this->_connection()->create($this->_types, $this->_props);
629 * Saves the changes caused by AOOSModel::insert(), AOOSModel::remove() or
630 * AOOSModel::update()
632 public function save() {
633 if (!$this->_connection()) {
634 return false;
636 foreach ($this->_intern as $key => $row) {
637 switch($row[0]) {
638 case(AOOSModel::INTERN_DATA_INSERTED):
639 if (!$this->_connection()->insert($row[1])) {
640 return false;
642 break;
643 case(AOOSModel::INTERN_DATA_REMOVED):
644 if (!$this->_connection()->remove($row[1], $row[2])) {
645 return false;
647 case(AOOSModel::INTERN_DATA_UPDATED):
648 if (!$this->_connection()->update($row[1], $row[2], $row[3])) {
649 return false;
652 unset($this->_intern[$key]);
654 return true;
657 private function _connect() {
658 if ($this->_connection) {
659 return $this->_connection;
661 if (!$this->source() || !$this->table()) {
662 throw new AOOSException($this->core(), $this->tr("no_source_or_table_set"));
663 return false;
665 switch ($this->source()) {
666 case("mysql"):
667 require_once("lib/AOOSMysqlInterface.php");
668 $this->_connection = new AOOSMysqlInterface($this->core());
669 break;
671 $this->_connection->setTable($this->table());
674 private function _connection() {
675 if (!$this->_connection) {
676 try {
677 $this->_connect();
679 catch (AOOSException $e) {
680 throw $e;
681 return false;
684 return $this->_connection;
688 * \internal Fixes where clauses so that the given values are correctly
689 * formatted according to the properties for their column
691 private function _fixWhere($where) {
692 $ci = $this->columnIndex();
694 if (!is_array($where)) {
695 return false;
698 foreach ($where as $key => $value) {
699 if (!in_array($key, $ci)) {
700 unset($where[$ci]);
701 continue;
703 $where[$key] = $this->_fixDataProps($value, $this->getProperty($key));
705 return $where;
709 * \internal Fixes a limit
711 private function _fixLimit($l) {
712 $limit = array(0,$this->rows());
713 if (is_integer($l)) {
714 $limit[1] = $l;
716 elseif(is_array($l)) {
717 $limit = $l;
719 return $limit;
722 private function _reset() {
723 $this->_data = array();
724 $this->_connect();
725 $this->_rows = 0;
729 * Dummy function
731 public function dataModelDefinition() { return 0; }
733 // vim: textwidth=80