From 4ceba82b9863f2c6323cbe00407e4bfbedbfc1cd Mon Sep 17 00:00:00 2001 From: wei <> Date: Mon, 8 Oct 2007 03:24:07 +0000 Subject: Allow active records to have multiple foreign key references to the same table. Add TXCache. --- framework/Caching/TXCache.php | 126 +++++++++++++++++++++ .../Relations/TActiveRecordBelongsTo.php | 13 ++- .../Relations/TActiveRecordHasMany.php | 17 ++- .../Relations/TActiveRecordHasManyAssociation.php | 24 ++-- .../ActiveRecord/Relations/TActiveRecordHasOne.php | 14 ++- .../Relations/TActiveRecordRelation.php | 60 +++++++++- .../Relations/TActiveRecordRelationContext.php | 91 ++++++++++----- framework/Data/ActiveRecord/TActiveRecord.php | 75 +++++++++++- 8 files changed, 361 insertions(+), 59 deletions(-) create mode 100644 framework/Caching/TXCache.php (limited to 'framework') diff --git a/framework/Caching/TXCache.php b/framework/Caching/TXCache.php new file mode 100644 index 00000000..698020af --- /dev/null +++ b/framework/Caching/TXCache.php @@ -0,0 +1,126 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2007 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id: TXCache.php 1994 2007-06-11 16:02:28Z knut $ + * @package System.Caching + */ + +/** + * TXCache class + * + * TXCache implements a cache application module based on {@link http://xcache.lighttpd.net/ xcache}. + * + * By definition, cache does not ensure the existence of a value + * even if it never expires. Cache is not meant to be an persistent storage. + * + * To use this module, the xcache PHP extension must be loaded and configured in the php.ini. + * + * Some usage examples of TXCache are as follows, + * + * $cache=new TXCache; // TXCache may also be loaded as a Prado application module + * $cache->init(null); + * $cache->add('object',$object); + * $object2=$cache->get('object'); + * + * + * If loaded, TXCache will register itself with {@link TApplication} as the + * cache module. It can be accessed via {@link TApplication::getCache()}. + * + * TXCache may be configured in application configuration file as follows + * + * + * + * + * @author Wei Zhuo + * @version $Id: TXCache.php 1994 2007-06-11 16:02:28Z knut $ + * @package System.Caching + * @since 3.1.1 + */ +class TXCache extends TCache +{ + /** + * Initializes this module. + * This method is required by the IModule interface. + * @param TXmlElement configuration for this module, can be null + * @throws TConfigurationException if xcache extension is not installed or not started, check your php.ini + */ + public function init($config) + { + if(!function_exists('xcache_isset')) + throw new TConfigurationException('xcache_extension_required'); + + $enabled = intval(ini_get('xcache.cacher')) !== 0; + $var_size = intval(ini_get('xcache.var_size')); + + if(!($enabled && $var_size > 0)) + throw new TConfigurationException('xcache_extension_not_enabled'); + + parent::init($config); + } + + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string a unique key identifying the cached value + * @return string the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return xcache_isset($key) ? xcache_get($key) : false; + } + + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string the key identifying the value to be cached + * @param string the value to be cached + * @param integer the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key,$value,$expire) + { + return xcache_set($key,$value,$expire); + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string the key identifying the value to be cached + * @param string the value to be cached + * @param integer the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key,$value,$expire) + { + return !xcache_isset($key) ? $this->setValue($key,$value,$expire) : false; + } + + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return xcache_unset($key); + } + + /** + * Deletes all values from cache. + * Be careful of performing this operation if the cache is shared by multiple applications. + */ + public function flush() + { + return xcache_clear_cache(); + } +} + +?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php index d7278d64..d4ff07e5 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php @@ -77,8 +77,7 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation */ protected function collectForeignObjects(&$results) { - $fkObject = $this->getContext()->getForeignRecordFinder(); - $fkeys = $this->findForeignKeys($this->getSourceRecord(),$fkObject); + $fkeys = $this->getRelationForeignKeys(); $properties = array_keys($fkeys); $fields = array_values($fkeys); @@ -87,6 +86,16 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation $fkObjects = $this->findForeignObjects($fields, $indexValues); $this->populateResult($results,$properties,$fkObjects,$fields); } + + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + $fkObject = $this->getContext()->getForeignRecordFinder(); + return $this->findForeignKeys($this->getSourceRecord(),$fkObject); + } /** * Sets the foreign objects to the given property on the source object. diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php index cbba3ebd..f7426862 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php @@ -1,4 +1,4 @@ -getContext()->getForeignRecordFinder(); - $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord()); + $fkeys = $this->getRelationForeignKeys(); $properties = array_values($fkeys); $fields = array_keys($fkeys); @@ -86,6 +85,16 @@ class TActiveRecordHasMany extends TActiveRecordRelation $this->populateResult($results,$properties,$fkObjects,$fields); } + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + $fkObject = $this->getContext()->getForeignRecordFinder(); + return $this->findForeignKeys($fkObject, $this->getSourceRecord()); + } + /** * Updates the associated foreign objects. * @return boolean true if all update are success (including if no update was required), false otherwise . @@ -113,5 +122,5 @@ class TActiveRecordHasMany extends TActiveRecordRelation return $success; } } - + ?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php index 0e176607..564d3d22 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php @@ -37,7 +37,7 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation'); * * public static $RELATIONS = array * ( - * 'Categories' => array(self::HAS_MANY, 'CategoryRecord', 'Article_Category') + * 'Categories' => array(self::MANY_TO_MANY, 'CategoryRecord', 'Article_Category') * ); * * public static function finder($className=__CLASS__) @@ -54,7 +54,7 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation'); * * public static $RELATIONS = array * ( - * 'Articles' => array(self::HAS_MANY, 'ArticleRecord', 'Article_Category') + * 'Articles' => array(self::MANY_TO_MANY, 'ArticleRecord', 'Article_Category') * ); * * public static function finder($className=__CLASS__) @@ -96,17 +96,22 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation */ protected function collectForeignObjects(&$results) { - $association = $this->getAssociationTable(); - $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord()); - + list($sourceKeys, $foreignKeys) = $this->getRelationForeignKeys(); $properties = array_values($sourceKeys); - $indexValues = $this->getIndexValues($properties, $results); + $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys); + } + /** + * @return array 2 arrays of source keys and foreign keys from the association table. + */ + public function getRelationForeignKeys() + { + $association = $this->getAssociationTable(); + $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord(), true); $fkObject = $this->getContext()->getForeignRecordFinder(); $foreignKeys = $this->findForeignKeys($association, $fkObject); - - $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys); + return array($sourceKeys, $foreignKeys); } /** @@ -182,7 +187,7 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation */ protected function fetchForeignObjects(&$results,$foreignKeys,$indexValues,$sourceKeys) { - $criteria = $this->getContext()->getCriteria(); + $criteria = $this->getCriteria(); $finder = $this->getContext()->getForeignRecordFinder(); $registry = $finder->getRecordManager()->getObjectStateRegistry(); $type = get_class($finder); @@ -198,7 +203,6 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $collections[$hash][] = $obj; $registry->registerClean($obj); } - $this->setResultCollection($results, $collections, array_values($sourceKeys)); } diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php index b22428ae..e5a36659 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php @@ -92,9 +92,7 @@ class TActiveRecordHasOne extends TActiveRecordRelation */ protected function collectForeignObjects(&$results) { - $fkObject = $this->getContext()->getForeignRecordFinder(); - $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord()); - + $fkeys = $this->getRelationForeignKeys(); $properties = array_values($fkeys); $fields = array_keys($fkeys); @@ -102,6 +100,16 @@ class TActiveRecordHasOne extends TActiveRecordRelation $fkObjects = $this->findForeignObjects($fields,$indexValues); $this->populateResult($results,$properties,$fkObjects,$fields); } + + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + $fkObject = $this->getContext()->getForeignRecordFinder(); + return $this->findForeignKeys($fkObject, $this->getSourceRecord()); + } /** * Sets the foreign objects to the given property on the source object. diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php index 5a3ea50e..8e9cc9b5 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php @@ -26,10 +26,12 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext'); abstract class TActiveRecordRelation { private $_context; + private $_criteria; - public function __construct(TActiveRecordRelationContext $context) + public function __construct(TActiveRecordRelationContext $context, $criteria) { $this->_context = $context; + $this->_criteria = $criteria; } /** @@ -39,6 +41,14 @@ abstract class TActiveRecordRelation { return $this->_context; } + + /** + * @return TActiveRecordCriteria + */ + protected function getCriteria() + { + return $this->_criteria; + } /** * @return TActiveRecord @@ -75,6 +85,16 @@ abstract class TActiveRecordRelation array_push($stack,$this); //call it later return $results; } + + /** + * Fetch results for current relationship. + * @return boolean always true. + */ + public function fetchResultsInto($obj) + { + $this->collectForeignObjects($obj); + return true; + } /** * Returns foreign keys in $fromRecord with source column names as key @@ -84,7 +104,7 @@ abstract class TActiveRecordRelation * @param TActiveRecord $matchesRecord * @return array foreign keys with source column names as key and foreign column names as value. */ - protected function findForeignKeys($from, $matchesRecord) + protected function findForeignKeys($from, $matchesRecord, $loose=false) { $gateway = $matchesRecord->getRecordGateway(); $matchingTableName = $gateway->getRecordTableInfo($matchesRecord)->getTableName(); @@ -93,13 +113,42 @@ abstract class TActiveRecordRelation $tableInfo = $gateway->getRecordTableInfo($from); foreach($tableInfo->getForeignKeys() as $fkeys) { - if($fkeys['table']===$matchingTableName) - return $fkeys['keys']; + if(strtolower($fkeys['table'])===strtolower($matchingTableName)) + { + if(!$loose && $this->getContext()->hasFkField()) + return $this->getFkFields($fkeys['keys']); + else + return $fkeys['keys']; + } } $matching = $gateway->getRecordTableInfo($matchesRecord)->getTableFullName(); throw new TActiveRecordException('ar_relations_missing_fk', $tableInfo->getTableFullName(), $matching); } + + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + abstract public function getRelationForeignKeys(); + + /** + * Find matching foreign key fields from the 3rd element of an entry in TActiveRecord::$RELATION. + * Assume field names consist of [\w-] character sets. Prefix to the field names ending with a dot + * are ignored. + */ + private function getFkFields($fkeys) + { + $matching = array(); + preg_match_all('/\s*(\S+\.)?([\w-]+)\s*/', $this->getContext()->getFkField(), $matching); + $fields = array(); + foreach($fkeys as $fkName => $field) + { + if(in_array($fkName, $matching[2])) + $fields[$fkName] = $field; + } + return $fields; + } /** * @param mixed object or array to be hashed @@ -122,9 +171,8 @@ abstract class TActiveRecordRelation */ protected function findForeignObjects($fields, $indexValues) { - $criteria = $this->getContext()->getCriteria(); $finder = $this->getContext()->getForeignRecordFinder(); - return $finder->findAllByIndex($criteria, $fields, $indexValues); + return $finder->findAllByIndex($this->_criteria, $fields, $indexValues); } /** diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php index 189d2c5e..d83aa63a 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php @@ -32,13 +32,12 @@ class TActiveRecordRelationContext private $_property; private $_sourceRecord; - private $_criteria; - private $_relation; + private $_relation; //data from an entry of TActiveRecord::$RELATION + private $_fkeys; - public function __construct($source, $property=null, $criteria=null) + public function __construct($source, $property=null) { $this->_sourceRecord=$source; - $this->_criteria=$criteria; if($property!==null) list($this->_property, $this->_relation) = $this->getSourceRecordRelation($property); } @@ -48,7 +47,6 @@ class TActiveRecordRelationContext * property from the $RELATIONS static property in TActiveRecord. * @param string relation property name * @return array array($propertyName, $relation) relation definition. - * @throws TActiveRecordException if property is not defined or missing. */ protected function getSourceRecordRelation($property) { @@ -58,8 +56,6 @@ class TActiveRecordRelationContext if(strtolower($name)===$property) return array($name, $relation); } - throw new TActiveRecordException('ar_undefined_relation_prop', - $property, get_class($this->_sourceRecord), self::RELATIONS_CONST); } /** @@ -71,6 +67,15 @@ class TActiveRecordRelationContext return $class->getStaticPropertyValue(self::RELATIONS_CONST); } + /** + * @return boolean true if the relation is defined in TActiveRecord::$RELATIONS + * @since 3.1.2 + */ + public function hasRecordRelation() + { + return $this->_relation!==null; + } + public function getPropertyValue() { $obj = $this->getSourceRecord(); @@ -85,14 +90,6 @@ class TActiveRecordRelationContext return $this->_property; } - /** - * @return TActiveRecordCriteria sql query criteria for fetching the related record. - */ - public function getCriteria() - { - return $this->_criteria; - } - /** * @return TActiveRecord the active record instance that queried for its related records. */ @@ -109,6 +106,18 @@ class TActiveRecordRelationContext return $this->_relation[1]; } + /** + * @return array foreign key of this relations, the keys is dependent on the + * relationship type. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + if($this->_fkeys===null) + $this->_fkeys=$this->getRelationHandler()->getRelationForeignKeys(); + return $this->_fkeys; + } + /** * @return string HAS_MANY, HAS_ONE, or BELONGS_TO */ @@ -117,6 +126,25 @@ class TActiveRecordRelationContext return $this->_relation[0]; } + /** + * @return string foreign key field names, comma delimited. + * @since 3.1.2 + */ + public function getFkField() + { + return $this->_relation[2]; + } + + /** + * @return boolean true if the 3rd element of an TActiveRecord::$RELATION entry is set. + * @since 3.1.2 + */ + public function hasFkField() + { + $notManyToMany = $this->getRelationType() !== TActiveRecord::MANY_TO_MANY; + return $notManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); + } + /** * @return string the M-N relationship association table name. */ @@ -130,7 +158,8 @@ class TActiveRecordRelationContext */ public function hasAssociationTable() { - return isset($this->_relation[2]); + $isManyToMany = $this->getRelationType() === TActiveRecord::MANY_TO_MANY; + return $isManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); } /** @@ -145,29 +174,33 @@ class TActiveRecordRelationContext * Creates and return the TActiveRecordRelation handler for specific relationships. * An instance of TActiveRecordHasOne, TActiveRecordBelongsTo, TActiveRecordHasMany, * or TActiveRecordHasManyAssocation will be returned. + * @param TActiveRecordCriteria search criteria * @return TActiveRecordRelation record relationship handler instnace. + * @throws TActiveRecordException if property is not defined or missing. */ - public function getRelationHandler() + public function getRelationHandler($criteria=null) { + if(!$this->hasRecordRelation()) + { + throw new TActiveRecordException('ar_undefined_relation_prop', + $property, get_class($this->_sourceRecord), self::RELATIONS_CONST); + } + if($criteria===null) + $criteria = new TActiveRecordCriteria; switch($this->getRelationType()) { case TActiveRecord::HAS_MANY: - if(!$this->hasAssociationTable()) - { - Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany'); - return new TActiveRecordHasMany($this); - } - else - { - Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation'); - return new TActiveRecordHasManyAssociation($this); - } + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany'); + return new TActiveRecordHasMany($this, $criteria); + case TActiveRecord::MANY_TO_MANY: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation'); + return new TActiveRecordHasManyAssociation($this, $criteria); case TActiveRecord::HAS_ONE: Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasOne'); - return new TActiveRecordHasOne($this); + return new TActiveRecordHasOne($this, $criteria); case TActiveRecord::BELONGS_TO: Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo'); - return new TActiveRecordBelongsTo($this); + return new TActiveRecordBelongsTo($this, $criteria); default: throw new TActiveRecordException('ar_invalid_relationship'); } diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php index b53cdf45..446c87e4 100644 --- a/framework/Data/ActiveRecord/TActiveRecord.php +++ b/framework/Data/ActiveRecord/TActiveRecord.php @@ -94,9 +94,10 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext'); */ abstract class TActiveRecord extends TComponent { - const HAS_MANY='HAS_MANY'; const BELONGS_TO='BELONGS_TO'; const HAS_ONE='HAS_ONE'; + const HAS_MANY='HAS_MANY'; + const MANY_TO_MANY='MANY_TO_MANY'; private static $_columnMapping=array(); @@ -110,6 +111,8 @@ abstract class TActiveRecord extends TComponent */ public static $COLUMN_MAPPING=array(); + private static $_relations=array(); + /** * This static variable defines the relationships. * The keys are public variable/property names defined in the AR class. @@ -562,15 +565,77 @@ abstract class TActiveRecord extends TComponent /** * Returns the active record relationship handler for $RELATION with key * value equal to the $property value. - * @param string relationship property name. + * @param string relationship/property name corresponding to keys in $RELATION array. * @param array method call arguments. * @return TActiveRecordRelation */ - protected function getRelationHandler($property,$args) + protected function getRelationHandler($property,$args=array()) { $criteria = $this->getCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1)); - $context = new TActiveRecordRelationContext($this, $property, $criteria); - return $context->getRelationHandler(); + return $this->getRelationContext($property)->getRelationHandler($criteria); + } + + /** + * Gets a static copy of the relationship context for given property (a key + * in $RELATION), returns null if invalid relationship. Keeps a null + * reference to all invalid relations called. + * @param string relationship/property name corresponding to keys in $RELATION array. + * @return TActiveRecordRelationContext object containing information on + * the active record relationships for given property, null if invalid relationship + * @since 3.1.2 + */ + protected function getRelationContext($property) + { + $prop = get_class($this).':'.$property; + if(!isset(self::$_relations[$prop])) + { + $context = new TActiveRecordRelationContext($this, $property); + //keep a null reference to all non-existing relations called? + self::$_relations[$prop] = $context->hasRecordRelation() ? $context : null; + } + return self::$_relations[$prop]; + } + + /** + * Tries to load the relationship results for the given property. The $property + * value should correspond to an entry key in the $RELATION array. + * This method can be used to lazy load relationships. + * + * class TeamRecord extends TActiveRecord + * { + * ... + * + * private $_players; + * public static $RELATION=array + * ( + * 'players' => array(self::HAS_MANY, 'PlayerRecord'), + * ); + * + * public function setPlayers($array) + * { + * $this->_players=$array; + * } + * + * public function getPlayers() + * { + * if($this->_players===null) + * $this->fetchResultsFor('players'); + * return $this->_players; + * } + * } + * Usage example: + * $team = TeamRecord::finder()->findByPk(1); + * var_dump($team->players); //uses lazy load to fetch 'players' relation + * + * @param string relationship/property name corresponding to keys in $RELATION array. + * @return boolean true if relationship exists, false otherwise. + * @since 3.1.2 + */ + protected function fetchResultsFor($property) + { + if( ($context=$this->getRelationContext($property)) !== null) + return $context->getRelationHandler()->fetchResultsInto($this); + return false; } /** -- cgit v1.2.3