00001 <?php
00002
00031 abstract class SModel2 extends SObject {
00033 const COMMIT_NEVER = 0;
00035 const COMMIT_ALWAYS = 1;
00037 const COMMIT_CHANGED = 2;
00039 const COMMIT_FIRST = 3;
00041 const COMMIT_FOREIGN = 4;
00043 const COMMIT_NOSOURCE = 5;
00044
00045 private $attributes = array();
00046 private $currentValues = array();
00047 private $baseValues = array();
00048 private $xmlTree = null;
00049 private $xmlString = null;
00050 private $deletionFlag = false;
00051 private $bStored = false;
00052 private $bDirty = false;
00053
00054 private $_usingSharedAttributes = false;
00055
00056 public static $CHECK_TYPE = true;
00057
00058
00059 private static $nullValue = null;
00060
00061
00062 private static $attrCache = array();
00063 private static $baseCache = array();
00064
00084 public function setXML($xml) {
00085 if(is_string($xml)) {
00086 $oldLUIE = libxml_use_internal_errors(true);
00087 $xml = @simplexml_load_string(utf8_encode($xml));
00088 $errors = libxml_get_errors();
00089 libxml_use_internal_errors($oldLUIE);
00090 if(!$xml || count($errors) > 0) {
00091 $html = "<ul>";
00092 foreach($errors as $e) {
00093 $html .= "<li>On line <b>$e->line</b>: $e->message</li>";
00094 }
00095 $html .= "</ul>";
00096 $this->setError($html);
00097 return false;
00098 }
00099 }
00100
00101 if(!$this->setXMLImpl($xml))
00102 return false;
00103
00104 $this->xmlTree = $xml;
00105 $this->xmlString = $xml->asXML();
00106
00107 return true;
00108 }
00109
00123 protected function setXMLImpl($xml) {
00124 foreach($xml->children() as $child) {
00125 $name = $child->getName();
00126 if(!isset($this->attributes[$name])) {
00127 $this->setWarning("No such attribute '$name'");
00128 continue;
00129 }
00130 if($this->attributes[$name]['array']) {
00131 $arr = $this->xmlToArray($child);
00132 if($arr == null) {
00133 $this->setWarning("Could not process attribute '$name'");
00134 continue;
00135 }
00136 $this->set($name, $arr);
00137 continue;
00138 }
00139 switch($this->attributes[$name]['type']) {
00140 case 'int':
00141 $this->set($name, (int)$child);
00142 break;
00143 case 'string':
00144 $this->set($name, (string)$child);
00145 break;
00146 case 'float':
00147 $this->set($name, (float)$child);
00148 break;
00149 case 'time':
00150 $this->set($name, (string)$child);
00151 break;
00152 default:
00153 $cls = $this->attributes[$name]['type'];
00154 $obj = new $cls();
00155 if(!$obj->setXML($child)) {
00156 $this->setWarning("Could not process attribute '$name'");
00157 $this->getWarningFrom($obj);
00158 $this->getErrorFrom($obj);
00159 continue;
00160 }
00161 $this->set($name, $obj);
00162 break;
00163 }
00164 }
00165
00166 return true;
00167 }
00168
00187 public function getXML($options = array()) {
00188 $forceRegen = isset($options['forceRegen']) ? $options['forceRegen'] : false;
00189 $root = isset($options['root']) ? $options['root'] : 'xml';
00190 $asString = isset($options['asString']) ? $options['asString'] : true;
00191 if($this->xmlTree && !$forceRegen) {
00192 if($asString)
00193 return $this->xmlString;
00194 else
00195 return $this->xmlTree;
00196 }
00197
00198 if(($xml = $this->getXMLImpl($root, $forceRegen)) === false)
00199 return false;
00200
00201 $this->xmlString = $xml;
00202 $this->xmlTree = simplexml_load_string($xml);
00203 return ($asString ? $xml : $this->xmlTree);
00204 }
00205
00220 protected function getXMLImpl($root, $forceRegen) {
00221 $xml = "<$root>\n";
00222 foreach($this->currentValues as $attr => $value) {
00223 if($this->attributes[$attr]['array']) {
00224 $subXML = $this->arrayToXml($attr, $value);
00225 if($subXML)
00226 $xml .= $subXML;
00227 else
00228 $this->setWarning("Could not convert '$attr' to XML");
00229 continue;
00230 }
00231 switch($this->attributes[$attr]['type']) {
00232 case 'int':
00233 case 'string':
00234 case 'float':
00235 case 'time':
00236 $xml .= "<$attr>" . htmlspecialchars($value) . "</$attr>\n";
00237 break;
00238 default:
00239 if($value)
00240 $subXML = $value->getXML(array('forceRegen' => $forceRegen, 'asString' => true));
00241 else
00242 $subXML = '';
00243 if($subXML === false) {
00244 $this->setWarning("Could not convert '$attr' to XML");
00245 if($value) {
00246 $this->getWarningFrom($value);
00247 $this->getErrorFrom($value);
00248 }
00249 continue;
00250 }
00251 $xml .= $subXML;
00252 }
00253 }
00254 $xml .= "</$root>";
00255 return $xml;
00256 }
00257
00264 public function markForDeletion() {
00265 $this->deletionFlag = true;
00266 }
00267
00272 public function isMarkedForDeletion() {
00273 return $this->deletionFlag;
00274 }
00275
00287 public function revert() {
00288 $this->currentValues = array_merge($this->baseValues);
00289 $this->bDirty = false;
00290 $this->deletionFlag = false;
00291 }
00292
00301 public function copy($asNew = false) {
00302 $cls = get_class($this);
00303 $obj = new $cls;
00304
00305
00306 $obj->attributes = array_merge($this->attributes, $obj->attributes);
00307 if(!$asNew)
00308 $obj->baseValues = array_merge($this->baseValues);
00309 $obj->currentValues = array_merge($this->currentValues);
00310 if($this->xmlTree)
00311 $obj->xmlTree = clone $this->xmlTree;
00312 $obj->xmlString = $this->xmlString;
00313 $obj->deletionFlag = $this->deletionFlag;
00314 if($asNew) {
00315 $obj->bStored = false;
00316 $obj->bDirty = true;
00317 }
00318 else {
00319 $obj->bStored = $this->bStored;
00320 $obj->bDirty = $this->bDirty;
00321 }
00322 return $obj;
00323 }
00324
00332 public function getAttributes($metainfo = false) {
00333 if($metainfo)
00334 return array_merge($this->attributes);
00335 else
00336 return array_keys($this->attributes);
00337 }
00338
00344 public function getAttributeType($attr) {
00345 if(!isset($this->attributes[$attr]))
00346 return null;
00347 else
00348 return $this->attributes[$attr]['type'];
00349 }
00350
00359 public function isAttributeNullable($attr) {
00360 if(!isset($this->attributes[$attr]))
00361 return null;
00362 else
00363 return $this->attributes[$attr]['null'];
00364 }
00365
00371 public function isAttributeEditable($attr) {
00372 if(!isset($this->attributes[$attr]))
00373 return null;
00374 else
00375 return $this->attributes[$attr]['edit'];
00376 }
00377
00385 public function isAttributeArray($attr) {
00386 if(!isset($this->attributes[$attr]))
00387 return null;
00388 else
00389 return $this->attributes[$attr]['array'];
00390 }
00391
00399 public function getAttributeForeign($attr) {
00400 if(!isset($this->attributes[$attr]))
00401 return null;
00402 else
00403 return $this->attributes[$attr]['foreign'];
00404 }
00405
00414 public function getAttributeInfo($attr) {
00415 if(!isset($this->attributes[$attr]))
00416 return null;
00417 else
00418 return $this->attributes[$attr];
00419 }
00420
00426 public function attributeExists($attr) {
00427 return array_key_exists($attr, $this->attributes);
00428 }
00429
00438 public function getAttributeDefault($attr) {
00439 if(!isset($this->attributes[$attr]))
00440 return null;
00441 else
00442 return $this->attributes[$attr]['default'];
00443 }
00444
00453 public function getValues($useBase = false) {
00454 if($useBase)
00455 return $this->baseValues;
00456 else
00457 return $this->currentValues;
00458 }
00459
00467 public function getValuesRecursive() {
00468 $out = array();
00469 foreach($this->currentValues as $attr => $val) {
00470 if($this->attributes[$attr]['type'] == 'int'
00471 || $this->attributes[$attr]['type'] == 'string'
00472 || $this->attributes[$attr]['type'] == 'float'
00473 || $this->attributes[$attr]['type'] == 'time')
00474 $out[$attr] = $val;
00475 else if($val instanceof SModel2)
00476 $out[$attr] = $val->getValuesRecursive();
00477 else
00478 $out[$attr] = $val;
00479 }
00480 return $out;
00481 }
00482
00490 public function isStored() {
00491 return $this->bStored;
00492 }
00493
00502 public function isDirty() {
00503 return $this->bDirty;
00504 }
00505
00514 public function isValid() {
00515 if(count($this->attributes) == 0)
00516 return false;
00517 $okay = true;
00518 foreach($this->attributes as $attr => $info) {
00519 $val = $this->currentValues[$attr];
00520 if(!self::$CHECK_TYPE && !$this->checkType($attr, $info, $val))
00521 $okay = false;
00522 }
00523 return $okay;
00524 }
00525
00536 public abstract function populate($constraints);
00537
00547 public abstract function commit($updateObj = false);
00548
00556 public function &__get($attr) {
00557 if(!isset($this->attributes[$attr])) {
00558 $this->setError("No such attribute '$attr'");
00559 return self::$nullValue;
00560 }
00561 return $this->currentValues[$attr];
00562 }
00563
00575 public function __set($attr, $value) {
00576 if(!isset($this->attributes[$attr])) {
00577 $this->setError("No such attribute '$attr'");
00578 return false;;
00579 }
00580 if(!$this->attributes[$attr]['edit']) {
00581 $this->setError("It is not allowed to edit '$attr'");
00582 return false;
00583 }
00584 $mv = $this->massageValue($attr, $value);
00585 if($mv !== null)
00586 $value = $mv;
00587 if(!self::$CHECK_TYPE && !$this->checkType($attr, $this->attributes[$attr], $value))
00588 return false;
00589 $this->xmlTree = null;
00590 $this->xmlString = null;
00591 $this->currentValues[$attr] = $value;
00592 $this->bDirty = true;
00593 return true;
00594 }
00595
00606 public function setValues($values) {
00607 foreach($values as $attr => $val) {
00608 if(isset($this->attributes[$attr]))
00609 $this->__set($attr, $val);
00610 else
00611 $this->setError("No such attribute '$attr'");
00612 }
00613 }
00614
00621 public function isChanged($attr) {
00622 if(!isset($this->attributes[$attr])) {
00623 $this->setError("No such attribute '$attr'");
00624 return null;
00625 }
00626
00627 return $this->currentValues[$attr] !== $this->baseValues[$attr];
00628 }
00629
00640 public function markAsLoaded($recursive = false) {
00641 if($recursive) {
00642 foreach($this->attributes as $attr => $info) {
00643 if($this->currentValues[$attr] instanceof SModel2)
00644 $this->currentValues[$attr]->markAsLoaded(true);
00645 }
00646 }
00647 $this->updateBase();
00648 }
00649
00669 protected function registerAttributes($attrs) {
00670 if(isset(self::$attrCache[get_class($this)])) {
00671 $this->attributes =& self::$attrCache[get_class($this)];
00672 $this->baseValues = self::$baseCache[get_class($this)];
00673 $this->currentValues = array_merge($this->baseValues);
00674 }
00675 else {
00676 $this->baseValues = array();
00677 $this->currentValues = array();
00678 $this->attributes = array();
00679 foreach($attrs as $attr => $info) {
00680 $this->attributes[$attr] = array(
00681 'type' => isset($info['type']) ? $info['type'] : 'int',
00682 'null' => isset($info['null']) ? $info['null'] : true,
00683 'edit' => isset($info['edit']) ? $info['edit'] : true,
00684 'array' => isset($info['array']) ? $info['array'] : false,
00685 'default' => array_key_exists('default', $info) ? $info['default'] : null,
00686 'commit' => isset($info['commit']) ? $info['commit'] : self::COMMIT_CHANGED,
00687 'foreign' => isset($info['foreign']) ? $info['foreign'] : array()
00688 );
00689 $this->baseValues[$attr] = $this->attributes[$attr]['default'];
00690 $this->currentValues[$attr] = $this->attributes[$attr]['default'];
00691 }
00692 self::$attrCache[get_class($this)] = $this->attributes;
00693 self::$baseCache[get_class($this)] = $this->baseValues;
00694 }
00695 $this->_usingSharedAttributes = true;
00696 }
00697
00708 protected function addAttribute($attr, $info) {
00709 if($this->_usingSharedAttributes) {
00710 $orig = array_merge($this->attributes);
00711 $this->attributes = array();
00712 $this->attributes = $orig;
00713 $this->_usingSharedAttributes = false;
00714 }
00715 $this->attributes[$attr] = array(
00716 'type' => isset($info['type']) ? $info['type'] : 'int',
00717 'null' => isset($info['null']) ? $info['null'] : true,
00718 'edit' => isset($info['edit']) ? $info['edit'] : true,
00719 'array' => isset($info['array']) ? $info['array'] : false,
00720 'default' => isset($info['default']) ? $info['default'] : null,
00721 'commit' => isset($info['commit']) ? $info['commit'] : self::COMMIT_CHANGED,
00722 'foreign' => isset($info['foreign']) ? $info['foreign'] : array()
00723 );
00724 $this->baseValues[$attr] = $this->attributes[$attr]['default'];
00725 $this->currentValues[$attr] = $this->attributes[$attr]['default'];
00726 }
00727
00736 protected function removeAttribute($attr) {
00737 if($this->_usingSharedAttributes) {
00738 $orig = array_merge($this->attributes);
00739 $this->attributes = array();
00740 $this->attributes = $orig;
00741 $this->_usingSharedAttributes = false;
00742 }
00743 unset($this->attributes[$attr]);
00744 unset($this->baseValues[$attr]);
00745 unset($this->currentValues[$attr]);
00746 }
00747
00756 protected function changeAttribute($attr, $info, $val) {
00757 if($this->_usingSharedAttributes) {
00758 $orig = array_merge($this->attributes);
00759 $this->attributes = array();
00760 $this->attributes = $orig;
00761 $this->_usingSharedAttributes = false;
00762 }
00763 $this->attributes[$attr][$info] = $val;
00764 }
00765
00782 protected function set($attr, $value, $markAsChanged = true) {
00783 if(!isset($this->attributes[$attr])) {
00784 $this->setError("No such attribute '$attr'");
00785 return false;
00786 }
00787 $mv = $this->massageValue($attr, $value);
00788 if($mv !== null)
00789 $value = $mv;
00790 $this->currentValues[$attr] = $value;
00791 if($markAsChanged)
00792 $this->bDirty = true;
00793 else
00794 $this->baseValues[$attr] = $value;
00795 return true;
00796 }
00797
00805 protected function get($attr) {
00806 if(!isset($this->attributes[$attr])) {
00807 $this->setError("No such attribute '$attr'");
00808 return null;
00809 }
00810 return $this->currentValues[$attr];
00811 }
00812
00824 protected function massageValue($attr, $value) {
00825 if($this->attributes[$attr]['array']) {
00826 $out = array();
00827 if(!is_array($value)) {
00828 if($value == '')
00829 $value = array();
00830 else
00831 return null;
00832 }
00833 foreach($value as $k => $v) {
00834 switch($this->attributes[$attr]['type']) {
00835 case 'int':
00836 if(ctype_digit((string) $v))
00837 $out[$k] = (int)$v;
00838 else
00839 return null;
00840 break;
00841 case 'string':
00842 if(is_string($v) || ctype_digit((string) $v))
00843 $out[$k] = (string)$v;
00844 else
00845 return null;
00846 break;
00847 case 'float':
00848 if(ctype_digit((string) $v))
00849 $out[$k] = (float)$v;
00850 else
00851 return null;
00852 break;
00853 case 'time':
00854 if(is_string($v) || is_array($v))
00855 $out[$k] = $v;
00856 else
00857 return null;
00858 break;
00859 default:
00860 $out[$k] = $v;
00861 }
00862 }
00863 return $out;
00864 }
00865 else {
00866 switch($this->attributes[$attr]['type']) {
00867 case 'int':
00868 if(ctype_digit((string) $value) || intval($value) < 0)
00869 return (int)$value;
00870 break;
00871 case 'string':
00872 if(is_string($value) || ctype_digit((string) $value) || intval($value) < 0)
00873 return (string)$value;
00874 break;
00875 case 'float':
00876 if((float)$value == $value)
00877 return (float)$value;
00878 break;
00879 case 'time':
00880 if(is_string($value) || is_array($value))
00881 return $value;
00882 break;
00883 default:
00884 return $value;
00885 }
00886 return null;
00887 }
00888 }
00889
00901 protected function updateBase() {
00902 $this->baseValues = $this->currentValues;
00903 $this->bDirty = false;
00904 $this->bStored = true;
00905 }
00906
00913 protected function nullify() {
00914 $this->baseValues = array();
00915 $this->currentValues = array();
00916 $this->attributes = array();
00917 $this->xmlTree = null;
00918 $this->xmlString = null;
00919 $this->deletionFlag = false;
00920 $this->bStored = false;
00921 $this->bDirty = false;
00922 }
00923
00933 protected function xmlToArray($xml) {
00934 $array = array();
00935 foreach($xml->children() as $child) {
00936 $name = $child->getName();
00937 if(count($child->children()) > 0)
00938 $value = $this->xmlToArray($child);
00939 else
00940 $value = (string)$child;
00941 if(isset($array[$name])) {
00942 if(is_array($array[$name]))
00943 $array[$name][] = $value;
00944 else
00945 $array[$name] = array($array[$name], $value);
00946 }
00947 else
00948 $array[$name] = $value;
00949 }
00950 return $array;
00951 }
00952
00963 protected function arrayToXml($root, $array) {
00964 if(is_array($array)) {
00965 if(isset($array[0])) {
00966 $xml = '';
00967 foreach($array as $elt)
00968 $xml .= $this->arrayToXml($root, $elt);
00969 return $xml;
00970 }
00971 $xml = "<$root>\n";
00972 foreach($array as $name => $elt)
00973 $xml .= $this->arrayToXml($name, $elt);
00974 $xml .= "</$root>\n";
00975 }
00976 else if(is_object($array))
00977 $xml = $array->getXML();
00978 else
00979 $xml = "<$root>" . htmlspecialchars((string)$array) . "</$root>";
00980 return $xml;
00981 }
00982
00983
00994 private function checkType($attr, $info, $val) {
00995 $nullable = $info['null'];
00996 $array = $info['array'];
00997 $type = $info['type'];
00998 if(!$nullable && ($val === null || $val === '' || $val === false)) {
00999 $this->setError("Attribute '$attr' cannot be null/empty");
01000 return false;
01001 }
01002 if($array) {
01003 if(!is_array($val)) {
01004 $this->setError("Attribute '$attr' is not an array");
01005 return false;
01006 }
01007 foreach($val as $i => $item) {
01008 switch($type) {
01009 case 'int':
01010 if(!is_int($item)) {
01011 $this->setError("Attribute '$attr' element $i must be an integer");
01012 return false;
01013 }
01014 break;
01015 case 'string':
01016 if(!is_string($item)) {
01017 $this->setError("Attribute '$attr' element $i must be a string");
01018 return false;
01019 }
01020 break;
01021 case 'float':
01022 if(!is_float($item)) {
01023 $this->setError("Attribute '$attr' element $i must be a float");
01024 return false;
01025 }
01026 break;
01027 case 'time':
01028 if(!is_string($item) && (!is_array($item) || count($item) != 1)) {
01029 $this->setError("Attribute '$attr' element $i must be a string or single-element array");
01030 return false;
01031 }
01032 break;
01033 default:
01034 if(!is_object($item) || get_class($item) != $type) {
01035 $this->setError("Attribute '$attr' element $i must be an instance of $type");
01036 return false;
01037 }
01038 break;
01039 }
01040 }
01041 }
01042 else {
01043 if(is_array($type)) {
01044 $this->setError("Attribute '$attr' must be a scalar");
01045 return false;
01046 }
01047 switch($type) {
01048 case 'int':
01049 if(!is_int($val)) {
01050 $this->setError("Attribute '$attr' must be an integer");
01051 return false;
01052 }
01053 break;
01054 case 'string':
01055 if(!is_string($val)) {
01056 $this->setError("Attribute '$attr' must be a string");
01057 return false;
01058 }
01059 break;
01060 case 'float':
01061 if(!is_float($val)) {
01062 $this->setError("Attribute '$attr' must be a float");
01063 return false;
01064 }
01065 break;
01066 case 'time':
01067 if(!is_string($item) && (!is_array($item) || count($item) != 1)) {
01068 $this->setError("Attribute '$attr' element $i must be a string or single-element array");
01069 return false;
01070 }
01071 break;
01072 default:
01073 if(!is_object($val) || get_class($val) != $type) {
01074 $this->setError("Attribute '$attr' must be an instance of $type");
01075 return false;
01076 }
01077 break;
01078 }
01079 }
01080 return true;
01081 }
01082 }
01083
01084 ?>