2 /** @package verysimple::Phreeze */
7 * Abstract base class for object that are persistable by Phreeze
9 * @package verysimple::Phreeze
10 * @author VerySimple Inc. <noreply@verysimple.com>
11 * @copyright 1997-2005 VerySimple Inc.
12 * @license http://www.gnu.org/licenses/lgpl.html LGPL
15 abstract class Phreezable
implements Serializable
17 private $_cache = array ();
19 protected $_val_errors = array ();
20 protected $_base_validation_complete = false;
22 private $_isPartiallyLoaded;
23 private $_cacheLevel = 0;
24 private $_noCache = false;
26 /** @var these properties will never be cached */
27 private static $NoCacheProperties = array (
31 "_base_validation_complete"
34 /** @var cache of public properties for each type for improved performance when enumerating */
35 private static $PublicPropCache = array ();
38 * Returns true if the current object has been loaded
42 * bool (optional) if provided will change the value
45 public function IsLoaded($value = null)
48 $this->_isLoaded
= $value;
51 return $this->_isLoaded
;
55 * Returns true if the current object has been partially loaded
59 * bool (optional) if provided will change the value
62 public function IsPartiallyLoaded($value = null)
65 $this->_isPartiallyLoaded
= $value;
68 return $this->_isPartiallyLoaded
;
72 * Returns 0 if this was loaded from the DB, 1 if from 1st level cache and 2 if 2nd level cache
76 * bool (optional) if provided will change the value
79 public function CacheLevel($value = null)
82 $this->_cacheLevel
= $value;
85 return $this->_cacheLevel
;
89 * Returns true if the current object should never be cached
93 * bool (optional) if provided will change the value
96 public function NoCache($value = null)
99 $this->_noCache
= $value;
102 return $this->_noCache
;
106 * Returns an array with all public properties, excluding any internal
107 * properties used by the Phreeze framework.
108 * This is cached for performance
109 * when enumerating through large numbers of the same class
113 public function GetPublicProperties()
115 $className = get_class($this);
117 if (! array_key_exists($className, self
::$PublicPropCache)) {
119 $ro = new ReflectionObject($this);
121 foreach ($ro->getProperties() as $rp) {
122 $propname = $rp->getName();
124 if (! in_array($propname, self
::$NoCacheProperties)) {
125 if (! ($rp->isPrivate() ||
$rp->isStatic())) {
126 $props [] = $propname;
131 self
::$PublicPropCache [$className] = $props;
134 return self
::$PublicPropCache [$className];
138 * When serializing, make sure that we ommit certain properties that
139 * should never be cached or serialized.
143 $propvals = array ();
144 $ro = new ReflectionObject($this);
146 foreach ($ro->getProperties() as $rp) {
147 $propname = $rp->getName();
149 if (! in_array($propname, self
::$NoCacheProperties)) {
150 if (method_exists($rp, "setAccessible")) {
151 $rp->setAccessible(true);
152 $propvals [$propname] = $rp->getValue($this);
153 } elseif (! $rp->isPrivate()) {
154 // if < php 5.3 we can't serialize private vars
155 $propvals [$propname] = $rp->getValue($this);
160 return serialize($propvals);
165 * @deprecated use ToObject
167 function GetObject($props = null, $camelCase = false)
169 return $this->ToObject(array (
171 'camelCase' => $camelCase
176 * Return an object with a limited number of properties from this Phreezable object.
177 * This can be used if not all properties are necessary, for example rendering as JSON
179 * This can be overriden per class for custom JSON output. the overridden method may accept
180 * additional option parameters that are not supported by the base Phreezable calss
183 * array assoc array of options. This is passed through from Controller->RenderJSON
184 * props (array) array of props to return (if null then use all public props)
185 * omit (array) array of props to omit
186 * camelCase (bool) if true then first letter of each property is made lowercase
189 function ToObject($options = null)
191 if ($options === null) {
195 $props = array_key_exists('props', $options) ?
$options ['props'] : $this->GetPublicProperties();
196 $omit = array_key_exists('omit', $options) ?
$options ['omit'] : array ();
197 $camelCase = array_key_exists('camelCase', $options) ?
$options ['camelCase'] : false;
199 $obj = new stdClass();
201 foreach ($props as $prop) {
202 if (! in_array($prop, $omit)) {
203 $newProp = ($camelCase) ?
lcfirst($prop) : $prop;
204 $obj->$newProp = $this->$prop;
212 * Reload the object when it awakes from serialization
217 function unserialize($data)
219 $propvals = unserialize($data);
220 $ro = new ReflectionObject($this);
222 foreach ($ro->getProperties() as $rp) {
223 $propname = $rp->name
;
224 if (array_key_exists($propname, $propvals)) {
225 if (method_exists($rp, "setAccessible")) {
226 $rp->setAccessible(true);
227 $rp->setValue($this, $propvals [$propname]);
228 } elseif (! $rp->isPrivate()) {
229 // if < php 5.3 we can't serialize private vars
230 $rp->setValue($this, $propvals [$propname]);
240 * @param Phreezer $phreezer
243 final function __construct(Phreezer
$phreezer, $row = null)
245 $this->_phreezer
= $phreezer;
246 $this->_cache
= array ();
252 $this->LoadDefaults();
258 * Init is called after contruction.
259 * When loading, Init is called prior to Load().
260 * When creating a blank object, Init is called immediately after LoadDefaults()
264 public function Init()
269 * LoadDefaults is called during construction if this object is not instantiated with
271 * The default values as specified in the fieldmap are loaded
275 public function LoadDefaults()
277 $fms = $this->_phreezer
->GetFieldMaps(get_class($this));
279 foreach ($fms as $fm) {
280 $prop = $fm->PropertyName
;
281 $this->$prop = $fm->DefaultValue
;
286 * LoadFromObject allows this class to be populated from another class, so long as
287 * the properties are compatible.
288 * This is useful when using reporters so that you
289 * can easily convert them to phreezable objects. Be sure to check that IsLoaded
290 * is true before attempting to save this object.
294 * object to populate from, which must contain compatible properties
296 public function LoadFromObject($src)
298 $this->IsLoaded(true);
299 $src_cls = get_class($src);
301 foreach (get_object_vars($this) as $key => $val) {
302 if (substr($key, 0, 1) != "_") {
303 if (property_exists($src_cls, $key)) {
304 $this->$key = $src->$key;
305 $this->IsPartiallyLoaded(true);
307 $this->IsLoaded(false);
316 * Validate returns true if the properties all contain valid values.
318 * use GetValidationErrors to see which fields have invalid values.
322 public function Validate()
324 // force re-validation
325 $this->ResetValidationErrors();
327 $is_valid = (! $this->HasValidationErrors());
329 // if validation fails, remove this object from the cache otherwise invalid values can
330 // hang around and cause troubles.
332 $this->_phreezer
->DeleteCache(get_class($this), $this->GetPrimaryKeyValue());
339 * Add a validation error to the error array
342 * string property name
344 * string error message
346 protected function AddValidationError($prop, $msg)
348 $this->_val_errors
[$prop] = $msg;
352 * Returns true if this object has validation errors
356 protected function HasValidationErrors()
358 $this->_DoBaseValidation();
359 return count($this->_val_errors
) > 0;
363 * Returns the error array - containing an array of fields with invalid values.
368 public function GetValidationErrors()
370 $this->_DoBaseValidation();
371 return $this->_val_errors
;
375 * Clears all previous validation errors
377 protected function ResetValidationErrors()
379 $this->_val_errors
= array ();
380 $this->_base_validation_complete
= false;
384 * populates the _val_errors array w/ phreezer
388 private function _DoBaseValidation()
390 $lenfunction = $this->_phreezer
->DataAdapter
->ConnectionSetting
->Multibyte ?
'mb_strlen' : 'strlen';
392 if (! $this->_base_validation_complete
) {
393 $fms = $this->_phreezer
->GetFieldMaps(get_class($this));
395 foreach ($fms as $fm) {
396 $prop = $fm->PropertyName
;
398 if ($fm->FieldType
== FM_TYPE_DECIMAL
&& is_numeric($fm->FieldSize
)) {
399 // decimal validation needs to be treated differently than whole numbers
401 $values = explode('.', ( string ) $this->$prop, 2);
402 $right = count($values) > 1 ?
strlen(( string ) $values [1]) : 0;
403 $left = strlen(( string ) $values [0]);
405 $limits = explode('.', ( string ) $fm->FieldSize
, 2);
406 $limitRight = count($limits) > 1 ?
( int ) $limits [1] : 0;
407 $limitLeft = ( int ) $limits [0] - $limitRight;
409 if ($left > $limitLeft ||
$right > $limitRight) {
410 $this->AddValidationError($prop, "$prop exceeds the maximum length of " . $fm->FieldSize
. "");
412 } elseif (is_numeric($fm->FieldSize
) && ($lenfunction ( $this->$prop )-1 > $fm->FieldSize
)) {
413 $this->AddValidationError($prop, "$prop exceeds the maximum length of " . $fm->FieldSize
. "");
416 if ($this->$prop == "" && ($fm->DefaultValue ||
$fm->IsAutoInsert
)) {
417 // these fields are auto-populated so we don't need to validate them unless
418 // a specific value was provided
420 switch ($fm->FieldType
) {
422 case FM_TYPE_SMALLINT
:
423 case FM_TYPE_TINYINT
:
424 case FM_TYPE_MEDIUMINT
:
426 case FM_TYPE_DECIMAL
:
427 if (! is_numeric($this->$prop)) {
428 $this->AddValidationError($prop, "$prop is not a valid number");
432 case FM_TYPE_DATETIME
:
433 if (strtotime($this->$prop) === '') {
434 $this->AddValidationError($prop, "$prop is not a valid date/time value.");
438 if (! in_array($this->$prop, $fm->GetEnumValues())) {
439 $this->AddValidationError($prop, "$prop is not valid value. Allowed values: " . implode(', ', $fm->GetEnumValues()));
449 // print_r($this->_val_errors);
451 $this->_base_validation_complete
= true;
455 * This static function can be overridden to populate this object with
456 * results of a custom query
459 * @param Criteria $criteria
460 * @return string or null
462 public static function GetCustomQuery($criteria)
468 * Refresh the object in the event that it has been saved to the session or serialized
471 * @param Phreezer $phreezer
474 final function Refresh(&$phreezer, $row = null)
476 $this->_phreezer
= $phreezer;
478 // also refresh any children in the cache in case they are accessed
479 foreach ($this->_cache
as $child) {
480 if (in_array("Phreezable", class_parents($child))) {
481 $child->Refresh($phreezer, $row);
493 * Serialized string representation of this object.
495 * purposes it is recommended to override this method
499 return serialize($this);
503 * Returns the name of the primary key property.
504 * TODO: does not support multiple primary keys.
509 function GetPrimaryKeyName()
511 $fms = $this->_phreezer
->GetFieldMaps(get_class($this));
512 foreach ($fms as $fm) {
513 if ($fm->IsPrimaryKey
) {
514 return $fm->PropertyName
;
521 * $this->_phreezer = null;
522 * $this->_cache = null;
529 throw new Exception("No Primary Key found for " . get_class($this));
533 * Returns the value of the primary key property.
534 * TODO: does not support multiple primary keys.
539 function GetPrimaryKeyValue()
541 $prop = $this->GetPrimaryKeyName();
546 * Returns this object as an associative array with properties as keys and
554 $fms = $this->_phreezer
->GetFieldMaps(get_class($this));
557 foreach ($fms as $fm) {
558 $prop = $fm->PropertyName
;
559 $cols [$fm->ColumnName
] = $this->$prop;
566 * Persist this object to the data store
569 * @param bool $force_insert
571 * @return int auto_increment or number of records affected
573 function Save($force_insert = false)
575 return $this->_phreezer
->Save($this, $force_insert);
579 * Delete this object from the data store
582 * @return int number of records affected
586 return $this->_phreezer
->Delete($this);
590 * Loads the object with data given in the row array.
597 $fms = $this->_phreezer
->GetFieldMaps(get_class($this));
598 $this->_phreezer
->Observe("Loading " . get_class($this), OBSERVE_DEBUG
);
600 $this->IsLoaded(true); // assume true until fail occurs
601 $this->IsPartiallyLoaded(false); // at least we tried
603 // in order to prevent collisions on fields, QueryBuilder appends __tablename__rand to the
604 // sql statement. We need to strip that out so we can match it up to the property names
605 $rowlocal = array ();
606 foreach ($row as $key => $val) {
607 $info = explode("___", $key);
609 // we prefer to use tablename.colname if we have it, but if not
610 // just use the colname
611 $newkey = isset($info [1]) ?
($info [1] . "." . $info [0]) : $info [0];
612 if (isset($rowlocal [$newkey])) {
613 throw new Exception("The column `$newkey` was selected twice in the same query, causing a data collision");
616 $rowlocal [$newkey] = $val;
619 foreach ($fms as $fm) {
620 if (array_key_exists($fm->TableName
. "." . $fm->ColumnName
, $rowlocal)) {
621 // first try to locate the field by tablename.colname
622 $prop = $fm->PropertyName
;
623 $this->$prop = $rowlocal [$fm->TableName
. "." . $fm->ColumnName
];
624 } elseif (array_key_exists($fm->ColumnName
, $rowlocal)) {
625 // if we can't locate the field by tablename.colname, then just look for colname
626 $prop = $fm->PropertyName
;
627 $this->$prop = $rowlocal [$fm->ColumnName
];
629 // there is a required column missing from this $row array - mark as partially loaded
630 $this->_phreezer
->Observe("Missing column '" . $fm->ColumnName
. "' while loading " . get_class($this), OBSERVE_WARN
);
631 $this->IsLoaded(false);
632 $this->IsPartiallyLoaded(true);
636 // now look for any eagerly loaded children - their fields should be available in this query
637 $kms = $this->_phreezer
->GetKeyMaps(get_class($this));
639 foreach ($kms as $km) {
640 if ($km->LoadType
== KM_LOAD_EAGER ||
$km->LoadType
== KM_LOAD_INNER
) {
641 // load the child object that was obtained eagerly and cache so we
642 // won't ever grab the same object twice in one page load
643 $this->_phreezer
->IncludeModel($km->ForeignObject
);
644 $foclass = $km->ForeignObject
;
645 $fo = new $foclass ( $this->_phreezer
, $row );
647 $this->_phreezer
->SetCache($foclass, $fo->GetPrimaryKeyValue(), $fo, $this->_phreezer
->CacheQueryObjectLevel2
);
651 $this->_phreezer
->Observe("Firing " . get_class($this) . "->OnLoad()", OBSERVE_DEBUG
);
656 * Returns a value from the local cache
659 * @deprecated this is handled internally by Phreezer now
663 public function GetCache($key)
665 return (array_key_exists($key, $this->_cache
) ?
$this->_cache
[$key] : null);
669 * Sets a value from in local cache
672 * @deprecated this is handled internally by Phreezer now
676 public function SetCache($key, $obj)
678 $this->_cache
[$key] = $obj;
682 * Clears all values in the local cache
685 * @deprecated this is handled internally by Phreezer now
687 public function ClearCache()
689 $this->_cache
= array ();
693 * Called after object is loaded, may be overridden
697 protected function OnLoad()
702 * Called by Phreezer prior to saving the object, may be overridden.
703 * If this function returns any non-true value, then the save operation
704 * will be cancelled. This allows you to perform custom insert/update queries
708 * @param boolean $is_insert
709 * true if Phreezer considers this a new record
712 public function OnSave($is_insert)
718 * Called by Phreezer after object is updated, may be overridden
722 public function OnUpdate()
727 * Called by Phreezer after object is inserted, may be overridden
731 public function OnInsert()
736 * Called by Phreezer after object is deleted, may be overridden
740 public function OnDelete()
745 * Called by Phreezer before object is deleted, may be overridden.
746 * if a true value is not returned, the delete operation will be aborted
751 public function OnBeforeDelete()
757 * Called after object is refreshed, may be overridden
761 public function OnRefresh()
766 * Throw an exception if an undeclared property is accessed
772 public function __get($key)
774 throw new Exception("Unknown property: $key");
778 * Throw an exception if an undeclared property is accessed
785 public function __set($key, $val)
787 throw new Exception("Unknown property: $key");