1: <?php
2:
3: /**
4: * This file contains the generic db item class.
5: *
6: * @package Core
7: * @subpackage GenericDB
8: *
9: * @author Timo Hummel
10: * @author Murat Purc <murat@purc.de>
11: * @copyright four for business AG <www.4fb.de>
12: * @license http://www.contenido.org/license/LIZENZ.txt
13: * @link http://www.4fb.de
14: * @link http://www.contenido.org
15: */
16:
17: defined('CON_FRAMEWORK') || die('Illegal call: Missing framework initialization - request aborted.');
18:
19: /**
20: * Class Item
21: * Abstract class for database based items.
22: *
23: * @package Core
24: * @subpackage GenericDB
25: */
26: abstract class Item extends cItemBaseAbstract {
27:
28: /**
29: * Storage of the source table to use for the user informations
30: *
31: * @var array
32: */
33: public $values;
34:
35: /**
36: * Storage of the fields which were modified, where the keys are the
37: * fieldnames and the values just simple bools.
38: *
39: * @var array
40: */
41: protected $modifiedValues;
42:
43: /**
44: * Stores the old primary key, just in case somebody wants to change it
45: *
46: * @var string
47: */
48: protected $oldPrimaryKey;
49:
50: /**
51: * List of funcion names of the filters used when data is stored to the db.
52: *
53: * @var array
54: */
55: protected $_arrInFilters = array(
56: 'htmlspecialchars',
57: 'addslashes'
58: );
59:
60: /**
61: * List of funcion names of the filters used when data is retrieved from the
62: * db
63: *
64: * @var array
65: */
66: protected $_arrOutFilters = array(
67: 'stripslashes',
68: 'htmldecode'
69: );
70:
71: /**
72: * Class name of meta object
73: *
74: * @var string
75: */
76: protected $_metaObject;
77:
78: /**
79: * Last executed SQL statement
80: *
81: * @var string
82: */
83: protected $_lastSQL;
84:
85: /**
86: * Constructor to create an instance of this class.
87: *
88: * @param string $sTable
89: * The table to use as information source
90: * @param string $sPrimaryKey
91: * The primary key to use
92: *
93: * @throws cInvalidArgumentException
94: */
95: public function __construct($sTable, $sPrimaryKey) {
96: parent::__construct($sTable, $sPrimaryKey, get_parent_class($this));
97: }
98:
99: /**
100: * Resets class variables back to default
101: * This is handy in case a new item is tried to be loaded into this class instance.
102: */
103: protected function _resetItem() {
104: parent::_resetItem();
105:
106: // make sure not to reset filters because then default filters would always be used for loading
107: $this->values = null;
108: $this->modifiedValues = null;
109: $this->_metaObject = null;
110: $this->_lastSQL = null;
111: }
112:
113: /**
114: * Loads an item by colum/field from the database.
115: *
116: * @param string $sField
117: * Specifies the field
118: * @param mixed $mValue
119: * Specifies the value
120: * @param bool $bSafe [optional]
121: * Use inFilter or not
122: *
123: * @return bool
124: * True if the load was successful
125: * @throws cDbException
126: * @throws cException if more than one item has been found matching the given arguments
127: */
128: public function loadBy($sField, $mValue, $bSafe = true) {
129: // reset class variables back to default before loading
130: $this->_resetItem();
131:
132: if ($bSafe) {
133: $mValue = $this->_inFilter($mValue);
134: }
135:
136: // check, if cache contains a matching entry
137: $aRecordSet = NULL;
138: if ($sField === $this->_primaryKeyName) {
139: $aRecordSet = $this->_oCache->getItem($mValue);
140: } else {
141: $aRecordSet = $this->_oCache->getItemByProperty($sField, $mValue);
142: }
143:
144: if ($aRecordSet) {
145: // entry in cache found, load entry from cache
146: $this->loadByRecordSet($aRecordSet);
147: return true;
148: }
149:
150: // SQL-Statement to select by field
151: $sql = "SELECT * FROM `%s` WHERE %s = '%s'";
152: $sql = $this->db->prepare($sql, $this->table, $sField, $mValue);
153:
154: // Query the database
155: $this->db->query($sql);
156:
157: $this->_lastSQL = $sql;
158:
159: if ($this->db->numRows() > 1) {
160: $msg = "Tried to load a single line with field $sField and value $mValue from " . $this->table . " but found more than one row";
161: throw new cException($msg);
162: }
163:
164: // Advance to the next record, return false if nothing found
165: if (!$this->db->nextRecord()) {
166: return false;
167: }
168:
169: $this->loadByRecordSet($this->db->toArray());
170: $this->_setLoaded(true);
171: return true;
172: }
173:
174: /**
175: * Loads an item by colums/fields from the database.
176: *
177: * @param array $aAttributes
178: * associative array with field / value pairs
179: * @param bool $bSafe [optional]
180: * Use inFilter or not
181: * @return bool
182: * True if the load was successful
183: * @throws cDbException
184: * @throws cException if more than one item could be found matching the given arguments
185: */
186: public function loadByMany(array $aAttributes, $bSafe = true) {
187: // reset class variables back to default before loading
188: $this->_resetItem();
189:
190: if ($bSafe) {
191: $aAttributes = $this->_inFilter($aAttributes);
192: }
193:
194: // check, if cache contains a matching entry
195: $aRecordSet = NULL;
196: if (count($aAttributes) == 1 && isset($aAttributes[$this->getPrimaryKeyName()])) {
197: $aRecordSet = $this->_oCache->getItem($aAttributes[$this->getPrimaryKeyName()]);
198: } else {
199: $aRecordSet = $this->_oCache->getItemByProperties($aAttributes);
200: }
201:
202: if ($aRecordSet) {
203: // entry in cache found, load entry from cache
204: $this->loadByRecordSet($aRecordSet);
205: return true;
206: }
207:
208: // SQL-Statement to select by fields
209: $sql = 'SELECT * FROM `:mytab` WHERE';
210: foreach (array_keys($aAttributes) as $sKey) {
211: // add quotes if key is a string
212: if (is_string($sKey)) {
213: $sql .= " $sKey = ':$sKey' AND";
214: } else {
215: $sql .= " $sKey = :$sKey AND";
216: }
217: }
218: // strip the last " AND" token
219: $sql = cString::getPartOfString($sql, 0, cString::getStringLength($sql) - 4);
220: $sql = $this->db->prepare($sql, array_merge(array(
221: 'mytab' => $this->table
222: ), $aAttributes));
223:
224: // Query the database
225: $this->db->query($sql);
226:
227: $this->_lastSQL = $sql;
228:
229: if ($this->db->numRows() > 1) {
230: $msg = 'Tried to load a single line with fields ' . print_r(array_keys($aAttributes), true) . ' and values ' . print_r(array_values($aAttributes), true) . ' from ' . $this->table . ' but found more than one row';
231: throw new cException($msg);
232: }
233:
234: // Advance to the next record, return false if nothing found
235: if (!$this->db->nextRecord()) {
236: return false;
237: }
238:
239: $this->loadByRecordSet($this->db->toArray());
240: $this->_setLoaded(true);
241: return true;
242: }
243:
244: /**
245: * Loads an item by passed where clause from the database.
246: * This function is expensive, since it executes allways a query to the
247: * database
248: * to retrieve the primary key, even if the record set is aleady cached.
249: * NOTE: Passed value has to be escaped before. This will not be done by
250: * this function.
251: *
252: * @param string $sWhere
253: * The where clause like 'idart = 123 AND idlang = 1'
254: * @return bool
255: * True if the load was successful
256: *
257: * @throws cDbException
258: * @throws cException if more than one item could be found matching the given where clause
259: */
260: protected function _loadByWhereClause($sWhere) {
261: // SQL-Statement to select by whee clause
262: $sql = "SELECT %s AS pk FROM `%s` WHERE " . (string) $sWhere;
263: $sql = $this->db->prepare($sql, $this->getPrimaryKeyName(), $this->table);
264:
265: // Query the database
266: $this->db->query($sql);
267:
268: $this->_lastSQL = $sql;
269:
270: if ($this->db->numRows() > 1) {
271: $msg = "Tried to load a single line with where clause '" . $sWhere . "' from " . $this->table . " but found more than one row";
272: throw new cException($msg);
273: }
274:
275: // Advance to the next record, return false if nothing found
276: if (!$this->db->nextRecord()) {
277: return false;
278: }
279:
280: $id = $this->db->f('pk');
281: return $this->loadByPrimaryKey($id);
282: }
283:
284: /**
285: * Loads an item by ID from the database.
286: *
287: * @param string $mValue
288: * Specifies the primary key value
289: *
290: * @return bool
291: * True if the load was successful
292: * @throws cDbException
293: * @throws cException
294: */
295: public function loadByPrimaryKey($mValue) {
296: $bSuccess = $this->loadBy($this->_primaryKeyName, $mValue);
297:
298: if ($bSuccess == true && method_exists($this, '_onLoad')) {
299: $this->_onLoad();
300: }
301:
302: return $bSuccess;
303: }
304:
305: /**
306: * Loads an item by it's recordset.
307: *
308: * @param array $aRecordSet
309: * The recordset of the item
310: */
311: public function loadByRecordSet(array $aRecordSet) {
312: $this->values = $aRecordSet;
313: $this->oldPrimaryKey = $this->values[$this->getPrimaryKeyName()];
314: $this->_setLoaded(true);
315: $this->_oCache->addItem($this->oldPrimaryKey, $this->values);
316:
317: if (method_exists($this, '_onLoad')) {
318: $this->_onLoad();
319: }
320: }
321:
322: /**
323: * Function which is called whenever an item is loaded.
324: * Inherited classes should override this function if desired.
325: */
326: protected function _onLoad() {
327: }
328:
329: /**
330: * Gets the value of a specific field.
331: *
332: * @param string $sField
333: * Specifies the field to retrieve
334: * @param bool $bSafe [optional]
335: * Flag to run defined outFilter on passed value
336: * @return mixed
337: * Value of the field
338: */
339: public function getField($sField, $bSafe = true) {
340: if (true !== $this->isLoaded()) {
341: $this->lasterror = 'No item loaded';
342: return false;
343: }
344:
345: if (true == $bSafe) {
346: return $this->outFilter($this->values[$sField]);
347: } else {
348: return $this->values[$sField];
349: }
350: }
351:
352: /**
353: * Wrapper for getField (less to type).
354: *
355: * @param string $sField
356: * Specifies the field to retrieve
357: * @param bool $bSafe [optional]
358: * Flag to run defined outFilter on passed value
359: * @return mixed
360: * Value of the field
361: */
362: public function get($sField, $bSafe = true) {
363: return $this->getField($sField, $bSafe);
364: }
365:
366: /**
367: * Sets the value of a specific field.
368: *
369: * @param string $sField
370: * Field name
371: * @param mixed $mValue
372: * Value to set
373: * @param bool $bSafe [optional]
374: * Flag to run defined inFilter on passed value
375: * @return bool
376: */
377: public function setField($sField, $mValue, $bSafe = true) {
378: if (true !== $this->isLoaded()) {
379: $this->lasterror = 'No item loaded';
380: return false;
381: }
382:
383: if ($sField == $this->getPrimaryKeyName()) {
384: $this->oldPrimaryKey = $this->values[$sField];
385: }
386:
387: // apply filter on value
388: if (true == $bSafe) {
389: $mValue = $this->_inFilter($mValue);
390: }
391:
392: // flag as modified
393: if ($this->values[$sField] != $mValue || cString::getStringLength($this->values[$sField]) != cString::getStringLength($mValue)) {
394: $this->modifiedValues[$sField] = true;
395: }
396:
397: // set new value
398: $this->values[$sField] = $mValue;
399:
400: return true;
401: }
402:
403: /**
404: * Shortcut to setField.
405: *
406: * @param string $sField
407: * Field name
408: * @param mixed $mValue
409: * Value to set
410: * @param bool $bSafe [optional]
411: * Flag to run defined inFilter on passed value
412: * @return bool
413: */
414: public function set($sField, $mValue, $bSafe = true) {
415: return $this->setField($sField, $mValue, $bSafe);
416: }
417:
418: /**
419: * Stores the loaded and modified item to the database.
420: *
421: * @return bool
422: * @throws cDbException
423: * @throws cInvalidArgumentException
424: */
425: public function store() {
426: $this->_executeCallbacks(self::STORE_BEFORE, get_class($this), array(
427: $this
428: ));
429:
430: if (true !== $this->isLoaded()) {
431: $this->lasterror = 'No item loaded';
432: $this->_executeCallbacks(self::STORE_FAILURE, get_class($this), array(
433: $this
434: ));
435: return false;
436: }
437:
438: $sql = 'UPDATE `' . $this->table . '` SET ';
439: $first = true;
440:
441: if (!is_array($this->modifiedValues)) {
442: $this->_executeCallbacks(self::STORE_SUCCESS, get_class($this), array(
443: $this
444: ));
445: return true;
446: }
447:
448: foreach ($this->modifiedValues as $key => $bValue) {
449: $value = $this->values[$key];
450: if (is_string($value)) {
451: $value = $this->db->escape($value);
452: }
453:
454: if ($first == true) {
455: $sql .= "`$key` = '" . $value . "'";
456: $first = false;
457: } else {
458: $sql .= ", `$key` = '" . $value . "'";
459: }
460: }
461:
462: $sql .= " WHERE " . $this->getPrimaryKeyName() . " = '" . $this->oldPrimaryKey . "'";
463:
464: $this->db->query($sql);
465:
466: $this->_lastSQL = $sql;
467:
468: if ($this->db->affectedRows() > 0) {
469: $this->_oCache->addItem($this->oldPrimaryKey, $this->values);
470: $this->_executeCallbacks(self::STORE_SUCCESS, get_class($this), array(
471: $this
472: ));
473: return true;
474: }
475:
476: $this->_executeCallbacks(self::STORE_FAILURE, get_class($this), array(
477: $this
478: ));
479: return false;
480: }
481:
482: /**
483: * Returns current item data as an assoziative array.
484: *
485: * @return array|false
486: */
487: public function toArray() {
488: if (true !== $this->isLoaded()) {
489: $this->lasterror = 'No item loaded';
490: return false;
491: }
492:
493: $aReturn = array();
494: foreach ($this->values as $field => $value) {
495: $aReturn[$field] = $this->getField($field);
496: }
497: return $aReturn;
498: }
499:
500: /**
501: * Returns current item data as an object.
502: *
503: * @return stdClass|false
504: */
505: public function toObject() {
506: $return = $this->toArray();
507: return (false !== $return) ? (object) $return : $return;
508: }
509:
510: /**
511: * Sets a custom property.
512: *
513: * @param string $sType
514: * Specifies the type
515: * @param string $sName
516: * Specifies the name
517: * @param mixed $mValue
518: * Specifies the value
519: * @param int $iClient [optional]
520: * Id of client to set property for
521: *
522: * @return bool
523: * @throws cDbException
524: * @throws cException
525: * @throws cInvalidArgumentException
526: */
527: public function setProperty($sType, $sName, $mValue, $iClient = 0) {
528: // If this object wasn't loaded before, return false
529: if (true !== $this->isLoaded()) {
530: $this->lasterror = 'No item loaded';
531: return false;
532: }
533:
534: // Set the value
535: $oProperties = $this->_getPropertiesCollectionInstance($iClient);
536: $bResult = $oProperties->setValue($this->getPrimaryKeyName(), $this->get($this->getPrimaryKeyName()), $sType, $sName, $mValue);
537: return $bResult;
538: }
539:
540: /**
541: * Returns a custom property.
542: *
543: * @param string $sType
544: * Specifies the type
545: * @param string $sName
546: * Specifies the name
547: * @param int $iClient [optional]
548: * Id of client to set property for
549: *
550: * @return mixed
551: * Value of the given property or false
552: * @throws cDbException
553: * @throws cException
554: */
555: public function getProperty($sType, $sName, $iClient = 0) {
556: // If this object wasn't loaded before, return false
557: if (true !== $this->isLoaded()) {
558: $this->lasterror = 'No item loaded';
559: return false;
560: }
561:
562: // Return the value
563: $oProperties = $this->_getPropertiesCollectionInstance($iClient);
564: $mValue = $oProperties->getValue($this->getPrimaryKeyName(), $this->get($this->getPrimaryKeyName()), $sType, $sName);
565: return $mValue;
566: }
567:
568: /**
569: * Deletes a custom property.
570: *
571: * @param string $sType
572: * Specifies the type
573: * @param string $sName
574: * Specifies the name
575: * @param int $iClient [optional]
576: * Id of client to delete properties
577: *
578: * @return bool
579: *
580: * @throws cDbException
581: * @throws cInvalidArgumentException
582: */
583: public function deleteProperty($sType, $sName, $iClient = 0) {
584: // If this object wasn't loaded before, return false
585: if (true !== $this->isLoaded()) {
586: $this->lasterror = 'No item loaded';
587: return false;
588: }
589:
590: // Delete the value
591: $oProperties = $this->_getPropertiesCollectionInstance($iClient);
592: $bResult = $oProperties->deleteValue($this->getPrimaryKeyName(), $this->get($this->getPrimaryKeyName()), $sType, $sName);
593: return $bResult;
594: }
595:
596: /**
597: * Deletes a custom property by its id.
598: *
599: * @param int $idprop
600: * Id of property
601: *
602: * @return bool
603: *
604: * @throws cDbException
605: * @throws cInvalidArgumentException
606: */
607: public function deletePropertyById($idprop) {
608: $oProperties = $this->_getPropertiesCollectionInstance();
609: return $oProperties->delete($idprop);
610: }
611:
612: ///**
613: // * Deletes the current item
614: // * Method doesn't work, remove in future versions.
615: // */
616: // function delete() {
617: // $this->_collectionInstance->delete($item->get($this->getPrimaryKeyName()));
618: // }
619:
620: /**
621: * Define the filter functions used when data is being stored or retrieved
622: * from the database.
623: *
624: * Examples:
625: * <pre>
626: * $obj->setFilters(array('addslashes'), array('stripslashes'));
627: * $obj->setFilters(array('htmlencode', 'addslashes'), array('stripslashes',
628: * 'htmlencode'));
629: * </pre>
630: *
631: * @param array $aInFilters [optional]
632: * Array with function names
633: * @param array $aOutFilters [optional]
634: * Array with function names
635: */
636: public function setFilters($aInFilters = array(), $aOutFilters = array()) {
637: $this->_arrInFilters = $aInFilters;
638: $this->_arrOutFilters = $aOutFilters;
639: }
640:
641: /**
642: * Filters the passed data using the functions defines in the _arrInFilters
643: * array.
644: *
645: * @todo This method is used from public scope, but it should be protected
646: * @see Item::setFilters()
647: * @param mixed $mData
648: * Data to filter
649: * @return mixed
650: * Filtered data
651: */
652: public function _inFilter($mData) {
653: foreach ($this->_arrInFilters as $_function) {
654: if (function_exists($_function)) {
655: if (is_array($mData)) {
656: foreach ($mData as $key => $value) {
657: $mData[$key] = $_function($value);
658: }
659: } else {
660: $mData = $_function($mData);
661: }
662: }
663: }
664: return $mData;
665: }
666:
667: /**
668: * Filters the passed data using the functions defines in the _arrOutFilters
669: * array.
670: *
671: * @see Item::setFilters()
672: * @param mixed $mData
673: * Data to filter
674: * @return mixed
675: * Filtered data
676: */
677: public function outFilter($mData) {
678: foreach ($this->_arrOutFilters as $_function) {
679: if (function_exists($_function)) {
680: if (is_array($mData)) {
681: foreach ($mData as $key => $value) {
682: $mData[$key] = $_function($value);
683: }
684: } else {
685: $mData = $_function($mData);
686: }
687: }
688: }
689: return $mData;
690: }
691:
692: /**
693: * Set meta object class name.
694: *
695: * @param string $metaObject
696: */
697: protected function _setMetaObject($metaObject) {
698: $this->_metaObject = $metaObject;
699: }
700:
701: /**
702: * Return meta object instance.
703: * This object might be retrieved from a global cache ($_metaObjectCache).
704: *
705: * @return object
706: */
707: public function getMetaObject() {
708: global $_metaObjectCache;
709:
710: if (!is_array($_metaObjectCache)) {
711: $_metaObjectCache = array();
712: }
713:
714: $sClassName = $this->_metaObject;
715: $qclassname = cString::toLowerCase($sClassName);
716:
717: if (array_key_exists($qclassname, $_metaObjectCache)) {
718: if (is_object($_metaObjectCache[$qclassname])) {
719: if (cString::toLowerCase(get_class($_metaObjectCache[$qclassname])) == $qclassname) {
720: $_metaObjectCache[$qclassname]->setPayloadObject($this);
721: return $_metaObjectCache[$qclassname];
722: }
723: }
724: }
725:
726: if (class_exists($sClassName)) {
727: $_metaObjectCache[$qclassname] = new $sClassName($this);
728: return $_metaObjectCache[$qclassname];
729: }
730: }
731: }
732: