diff --git a/CHANGELOG b/CHANGELOG index 0da595c0e0..ff1159fd9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,9 +8,11 @@ Version 1.1a to be released Version 1.0.5 to be released ---------------------------- - Bug #234: Multi-line Yii::t() not found by 'yiic message' (Qiang) +- Bug #235: Dynamic content does not work when page caching is used together with fragment caching (Qiang) - Bug #239: Syntax error in translated Portuguese error view file (Qiang) - Bug #246: Undefined variable in CMaskedTextField (Qiang) - Bug #252: mimeTypes.php contains clashing types (Qiang) +- Bug #258: Some eager loading queries may result in extra lazy loading queries (Qiang) - Bug #261: CWsdlGenerator should not use 'tns:' namespace when declaring a complex type (Qiang) - Bug #262: Setting 'charset' of CDbConnection causes exception when working with SQLite (Qiang) - Bug #263: Exception is thrown when column names contain "=" symbol (Qiang) @@ -18,16 +20,21 @@ Version 1.0.5 to be released - Bug #290: date formatter generates incorrect narrow day output (Qiang) - Bug: Lazy loading HAS_MANY or MANY_MANY properties will get NULL instead of empty array when the result set is empty (Qiang) - Bug: CDateFormatter::formatYear() only returns one digit when the year pattern is 'yy' (Qiang) +- Bug: The ON option is not respected for MANY_MANY relations (Qiang) - New #210: Added support for named scope of AR (Qiang) - New #211: Enhanced AR by supporting lazy relational query with on-the-fly query parameters (Qiang) - New #224: Added CModel::addErrors() method (Qiang) +- New #241: Added support to define root path aliases in configuration (Qiang) - New #247: Added support to allow using Web services in PHP versions lower than 5.2.0 (Qiang) - New #249: Added option to CHtml to allow generate tags without encoding attribute values (Qiang) - New #254: Added support to allow input widgets to be used with tabular inputs (Qiang) - New #265: Added support to validate time and datetime inputs (Qiang) - New #268: Added support to allow using dot syntax to generate list options with CHtml (Qiang) +- New #274: Added support to allow using route sub-patterns in URL rules (Qiang) - New #284: Refactored code about page states to simplify overriding efforts (Qiang) - New #291: Added support to validate emails with name part (Qiang) +- New #293: Added support to allow Yii to be used with other libraries which rely on autoload (Qiang) +- New #294: Added CDummyCache component (Qiang) - New: Deprecated CHtml::getActiveId() (Qiang) - New: Added CDbCriteria::mergeWith() (Qiang) - New: Added Oracle support for Active Record (Ricardo) @@ -39,7 +46,8 @@ Version 1.0.5 to be released - New: Added new message placeholder to CCompareValidator (Qiang) - New: Added trace statements to auth components (Qiang) - New: Added CHtml::value() (Qiang) - +- New: Enhanced 'yiic shell model' command so that it generates attribute labels by default (Qiang) +- New: Added CDbConnection::enableParamLogging to allow logging parameters bound to SQL statements (Qiang) Version 1.0.4 April 5, 2009 --------------------------- @@ -79,7 +87,8 @@ Version 1.0.4 April 5, 2009 - New #212: 'Readline' support in yiic console script (olafure) - New #225: Added trace statements to CActiveRecord and CActiveFinder (Qiang) - New #230: Enhanced CHtml so that it can be used in situations where controller is absent (Qiang) -- New #233: allow CFileValidator::types to be set with an array (Qiang) +- New #233: Allow CFileValidator::types to be set with an array (Qiang) +- New #292: Refactored CActiveRecord so that attribute assignment can be overridden more easily (Qiang) - New: Added support to GROUP BY and HAVING in eager loading of AR (Qiang) - New: Added CClientScript::scriptFiles and CClientScript::cssFiles (Qiang) - New: Added SQL Server support for Active Record (Christophe) diff --git a/docs/guide/caching.overview.txt b/docs/guide/caching.overview.txt index 65967d2f7a..95701f754a 100644 --- a/docs/guide/caching.overview.txt +++ b/docs/guide/caching.overview.txt @@ -50,6 +50,14 @@ it will create and use a SQLite3 database under the runtime directory. You can explicitly specify a database for it to use by setting its [connectionID|CDbCache::connectionID] property. + - [CDummyCache]: presents dummy cache that does no caching at all. The purpose +of this component is to simplify the code that needs to check the availability of cache. +For example, during development or if the server doesn't have actual cache support, we +can use this cache component. When an actual cache support is enabled, we can switch +to use the corresponding cache component. In both cases, we can use the same code +`Yii::app()->cache->get($key)` to attempt retrieving a piece of data without worrying +that `Yii::app()->cache` might be `null`. This component has been available since version 1.0.5. + > Tip: Because all these cache components extend from the same base class [CCache], one can switch to use a different type of cache without modifying the code that uses cache. diff --git a/docs/guide/caching.page.txt b/docs/guide/caching.page.txt index 0be1f2b527..119bd35190 100644 --- a/docs/guide/caching.page.txt +++ b/docs/guide/caching.page.txt @@ -26,7 +26,7 @@ public function filters() { return array( array( - 'system.web.widgets.COutputCache', + 'COutputCache', 'duration'=>100, 'varyByParam'=>array('id'), ), diff --git a/docs/guide/changes.txt b/docs/guide/changes.txt index 92c721c905..3a1166f32c 100644 --- a/docs/guide/changes.txt +++ b/docs/guide/changes.txt @@ -8,10 +8,14 @@ Version 1.0.5 * Enhanced active record by supporting named scopes. See: - [Named Scopes](/doc/guide/database.ar#named-scopes) + - {Default Named Scope](/doc/guide/database.ar#default-named-scope) - [Relational Query with Named Scopes](/doc/guide/database.arr#relational-query-with-named-scopes) * Enhanced active record by supporting lazy loading with dynamic query options. See: - [Dynamic Relational Query Options](/doc/guide/database.arr#dynamic-relational-query-options) + * Enhanced [CUrlManager] to support parameterizing the route part in URL rules. See: + - [Parameterizing Routes in URL Rules](/doc/guide/topics.url#parameterizing-routes) +
$Id$
\ No newline at end of file diff --git a/docs/guide/database.ar.txt b/docs/guide/database.ar.txt index 0ec8710999..cf00979a43 100644 --- a/docs/guide/database.ar.txt +++ b/docs/guide/database.ar.txt @@ -43,6 +43,7 @@ used for modeling database tables in PHP constructs and performing queries that do not involve complex SQLs. Yii DAO should be used for those complex scenarios. + Establishing DB Connection -------------------------- @@ -149,13 +150,15 @@ a column in the `Post` table, and CActiveRecord makes it accessible as a property with the help of the PHP `__get()` magic method. An exception will be thrown if we attempt to access a non-existing column in the same way. -> Info: For better readability, we suggest naming database tables and -columns in camel case. In particular, table names are formed by -capitalizing each word in the name and joining them without spaces; column -names are similar to table names except their first letter is in lower -case. For example, we use `Post` to name the table storing posts; and we -use `createTime` to name the table's primary key column. This makes tables -look more like class types and columns like variables. Note, however, using camel case may also bring you inconvenience for some DBMS, such as MySQL, as they may behave differently on different operation systems. +> Info: For better readability, we suggest naming table columns in camel case. +In particular, column names are formed by capitalizing each word except +the first one in the name and concatenating them without spaces. For example, we +may use `createTime` to name a column storing the creation time of a row. +Naming of tables depends on your personal taste. In this guide, we also follow +camel case naming convention except that the first letter is in upper case as well. +For example, we use `Post` to name the table storing post data. +Note, however, using camel case for table names may bring you inconvenience +for some DBMS, such as MySQL, as they may behave differently on different operation systems. Creating Record @@ -212,6 +215,15 @@ $post->createTime=new CDbExpression('NOW()'); $post->save(); ~~~ +> Tip: While AR allows us to perform database operations without writing +cumbersom SQL statements, we often want to know what SQL statements are executed +by AR underneath. This can be achieved by turning on the [logging feature](/doc/guide/topics.logging) +of Yii. For example, we can turn on [CWebLogRoute] in the application configuration, +and we will see the executed SQL statements being displayed at the end of each Web page. +Since version 1.0.5, we can set [CDbConnection::enableParamLogging] to be true in +the application configuration so that the parameter values bound to the SQL +statements are also logged. + Reading Record -------------- @@ -555,7 +567,7 @@ $posts=Post::model()->published()->recently()->findAll(); In general, named scopes must appear to the left of a `find` method call. Each of them provides a query criteria, which is combined with other criterias, including the one passed to the `find` method call. The net effect is like adding a list of filters to a query. -===Parameterized Named Scopes +### Parameterized Named Scopes Named scopes can be parameterized. For example, we may want to customize the number of posts specified by the `recently` named scope. To do so, instead of declaring the named scope in the [CActiveRecord::scopes] method, we need to define a new method whose name is the same as the scope name: @@ -581,7 +593,7 @@ $posts=Post::model()->published()->recently(3)->findAll(); If we do not supply the parameter 3 in the above, we would retrieve the 5 recently published posts by default. -===Default Named Scope +### Default Named Scope A model class can have a default named scope that would be applied for all queries (including relational ones) about the model. For example, a website supporting multiple languages may only want to display contents that are in the language the current user specifies. Because there may be many queries about the site contents, we can define a default named scope to solve this problem. To do so, we override the [CActiveRecord::defaultScope] method as follows, diff --git a/docs/guide/topics.url.txt b/docs/guide/topics.url.txt index 2bc959126a..b82ed50852 100644 --- a/docs/guide/topics.url.txt +++ b/docs/guide/topics.url.txt @@ -106,6 +106,8 @@ corresponding to a single rule. The pattern of a rule is a string used to match the path info part of URLs. And the route of a rule should refer to a valid controller [route](/doc/guide/basics.controller#route). +### Using Named Parameters + A rule can be associated with a few GET parameters. These GET parameters appear in the rule's pattern as special tokens in the following format: @@ -167,11 +169,45 @@ when a user requests for `/index.php/post/100`, the second rule in the above example will apply, which resolves in the route `post/read` and the GET parameter `array('id'=>100)` (accessible via `$_GET`). + > Note: Using URL rules will degrade application performance. This is because when parsing the request URL, [CUrlManager] will attempt to match -it with each rule until one can be applied. Therefore, a high-traffic Web +it with each rule until one can be applied. The more the number of rules, +the more the performance impact. Therefore, a high-traffic Web application should minimize its use of URL rules. + +### Parameterizing Routes + +Starting from version 1.0.5, we may reference named parameters in the route part +of a rule. This allows a rule to be applied to multiple routes based on matching +criteria. It may also help reduce the number of rules needed for an application, +and thus improve the overall performance. + +We use the following example rules to illustrate how to parameterize routes +with named parameters: + +~~~ +[php] +array( + '<_c:(post|comment)>//<_a:(create|update|delete)>' => '<_c>/<_a>', + '<_c:(post|comment)>/' => '<_c>/read', + '<_c:(post|comment)>s' => '<_c>/list', +) +~~~ + +In the above, we use two named parameters in the route part of the rules: +`_c` and `_a`. The former matches a controller ID to be either `post` or `comment`, +while the latter matches an action ID to be `create`, `update` or `delete`. +You may name the parameters differently as long as they do not conflict with +GET parameters that may appear in URLs. + +Using the aboving rules, the URL `/index.php/post/123/create` +would be parsed as the route `post/create` with GET parameter `id=123`. +And given the route `comment/list` and GET parameter `page=2`, we can create a URL +`/index.php/comments?page=2`. + + ### Hiding `index.php` There is one more thing that we can do to further clean our URLs, i.e., diff --git a/framework/YiiBase.php b/framework/YiiBase.php index da563c4a07..dea062ac6c 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -296,6 +296,7 @@ public static function setPathOfAlias($alias,$path) * Class autoload loader. * This method is provided to be invoked within an __autoload() magic method. * @param string class name + * @return boolean whether the class has been loaded successfully */ public static function autoload($className) { @@ -305,7 +306,11 @@ public static function autoload($className) else if(isset(self::$_classes[$className])) include(self::$_classes[$className]); else - include($className.'.php'); + { + @include($className.'.php'); + return class_exists($className,false); + } + return true; } /** @@ -458,6 +463,7 @@ public static function t($category,$message,$params=array(),$source=null,$langua 'CApcCache' => '/caching/CApcCache.php', 'CCache' => '/caching/CCache.php', 'CDbCache' => '/caching/CDbCache.php', + 'CDummyCache' => '/caching/CDummyCache.php', 'CEAcceleratorCache' => '/caching/CEAcceleratorCache.php', 'CMemCache' => '/caching/CMemCache.php', 'CXCache' => '/caching/CXCache.php', diff --git a/framework/base/CModule.php b/framework/base/CModule.php index 8760c51230..92fe793f1c 100644 --- a/framework/base/CModule.php +++ b/framework/base/CModule.php @@ -203,6 +203,31 @@ public function setImport($aliases) Yii::import($alias); } + /** + * Defines the root aliases. + * @param array list of aliases to be defined. The array keys are root aliases, + * while the array values are paths or aliases corresponding to the root aliases. + * For example, + *
+	 * array(
+	 *    'models'=>'application.models',              // an existing alias
+	 *    'extensions'=>'application.extensions',      // an existing alias
+	 *    'backend'=>dirname(__FILE__).'/../backend',  // a directory
+	 * )
+	 * 
+ * @since 1.0.5 + */ + public function setAliases($mappings) + { + foreach($mappings as $name=>$alias) + { + if(($path=Yii::getPathOfAlias($alias))!==false) + Yii::setPathOfAlias($name,$path); + else + Yii::setPathOfAlias($name,$alias); + } + } + /** * @return CModule the parent module. Null if this module does not have a parent. */ diff --git a/framework/caching/CDummyCache.php b/framework/caching/CDummyCache.php new file mode 100644 index 0000000000..210fcfda71 --- /dev/null +++ b/framework/caching/CDummyCache.php @@ -0,0 +1,126 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDummyCache is a placeholder cache component. + * + * CDummyCache does not do/cache anything. It is used as the default 'cache' application component. + * + * @author Qiang Xue + * @version $Id$ + * @package system.caching + * @since 1.0 + */ +class CDummyCache extends CApplicationComponent implements ICache, ArrayAccess +{ + /** + * Retrieves a value from cache with a specified key. + * @param string a key identifying the cached value + * @return mixed the value stored in cache, false if the value is not in the cache, expired or the dependency has changed. + */ + public function get($id) + { + return false; + } + + /** + * Stores a value identified by a key into cache. + * If the cache already contains such a key, the existing value and + * expiration time will be replaced with the new ones. + * + * @param string the key identifying the value to be cached + * @param mixed the value to be cached + * @param integer the number of seconds in which the cached value will expire. 0 means never expire. + * @param ICacheDependency dependency of the cached item. If the dependency changes, the item is labeled invalid. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + public function set($id,$value,$expire=0,$dependency=null) + { + return true; + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * Nothing will be done if the cache already contains the key. + * @param string the key identifying the value to be cached + * @param mixed the value to be cached + * @param integer the number of seconds in which the cached value will expire. 0 means never expire. + * @param ICacheDependency dependency of the cached item. If the dependency changes, the item is labeled invalid. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + public function add($id,$value,$expire=0,$dependency=null) + { + return true; + } + + /** + * Deletes a value with the specified key from cache + * @param string the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + public function delete($id) + { + return true; + } + + /** + * Deletes all values from cache. + * Be careful of performing this operation if the cache is shared by multiple applications. + * Child classes may implement this method to realize the flush operation. + * @throws CException if this method is not overridden by child classes + */ + public function flush() + { + } + + /** + * Returns whether there is a cache entry with a specified key. + * This method is required by the interface ArrayAccess. + * @param string a key identifying the cached value + * @return boolean + */ + public function offsetExists($id) + { + return false; + } + + /** + * Retrieves the value from cache with a specified key. + * This method is required by the interface ArrayAccess. + * @param string a key identifying the cached value + * @return mixed the value stored in cache, false if the value is not in the cache or expired. + */ + public function offsetGet($id) + { + return false; + } + + /** + * Stores the value identified by a key into cache. + * If the cache already contains such a key, the existing value will be + * replaced with the new ones. To add expiration and dependencies, use the set() method. + * This method is required by the interface ArrayAccess. + * @param string the key identifying the value to be cached + * @param mixed the value to be cached + */ + public function offsetSet($id, $value) + { + } + + /** + * Deletes the value with the specified key from cache + * This method is required by the interface ArrayAccess. + * @param string the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + public function offsetUnset($id) + { + } +} diff --git a/framework/cli/commands/shell/ModelCommand.php b/framework/cli/commands/shell/ModelCommand.php index 4f6cf054e4..0f98f02273 100644 --- a/framework/cli/commands/shell/ModelCommand.php +++ b/framework/cli/commands/shell/ModelCommand.php @@ -111,6 +111,7 @@ public function generateModel($source,$params) list($className,$tableName)=$params; $content=file_get_contents($source); $rules=''; + $labels=''; if(($db=Yii::app()->getDb())!==null) { $db->active=true; @@ -121,6 +122,10 @@ public function generateModel($source,$params) $numerical=array(); foreach($table->columns as $column) { + $label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?name))))); + if(strcasecmp(substr($label,-2),'id')===0 && strlen($label)>2) + $label=substr($label,0,-2); + $labels.="\n\t\t\t'{$column->name}'=>'$label',"; if($column->isPrimaryKey && $table->sequenceName!==null || $column->isForeignKey) continue; if(!$column->allowNull && $column->defaultValue===null) @@ -148,6 +153,7 @@ public function generateModel($source,$params) $tr=array( '{ClassName}'=>$className, '{TableName}'=>$tableName, + '{Labels}'=>$labels, '{Rules}'=>$rules); return strtr($content,$tr); diff --git a/framework/cli/views/shell/model/model.php b/framework/cli/views/shell/model/model.php index 4796e22389..8f5c861c57 100644 --- a/framework/cli/views/shell/model/model.php +++ b/framework/cli/views/shell/model/model.php @@ -42,7 +42,7 @@ public function relations() */ public function attributeLabels() { - return array( + return array({Labels} ); } } \ No newline at end of file diff --git a/framework/db/CDbCommand.php b/framework/db/CDbCommand.php index cafb6d2400..63c83d0c17 100644 --- a/framework/db/CDbCommand.php +++ b/framework/db/CDbCommand.php @@ -38,6 +38,7 @@ class CDbCommand extends CComponent private $_connection; private $_text=''; private $_statement=null; + private $_params; /** * Constructor. @@ -109,9 +110,11 @@ public function prepare() try { $this->_statement=$this->getConnection()->getPdoInstance()->prepare($this->getText()); + $this->_params=array(); } catch(Exception $e) { + Yii::log('Error in preparing SQL: '.$this->getText(),CLogger::LEVEL_ERROR,'system.db.CDbCommand'); throw new CDbException(Yii::t('yii','CDbCommand failed to prepare the SQL statement: {error}', array('{error}'=>$e->getMessage()))); } @@ -146,6 +149,8 @@ public function bindParam($name, &$value, $dataType=null, $length=null) $this->_statement->bindParam($name,$value,$dataType); else $this->_statement->bindParam($name,$value,$dataType,$length); + if($this->_connection->enableParamLogging) + $this->_params[]=$name.'=['.gettype($value).']'; } /** @@ -165,6 +170,8 @@ public function bindValue($name, $value, $dataType=null) $this->_statement->bindValue($name,$value,$this->_connection->getPdoType(gettype($value))); else $this->_statement->bindValue($name,$value,$dataType); + if($this->_connection->enableParamLogging) + $this->_params[]=$name.'='.var_export($value,true); } /** @@ -176,7 +183,8 @@ public function bindValue($name, $value, $dataType=null) */ public function execute() { - Yii::trace('Executing SQL: '.$this->getText(),'system.db.CDbCommand'); + $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; + Yii::trace('Executing SQL: '.$this->getText().$params,'system.db.CDbCommand'); try { if($this->_statement instanceof PDOStatement) @@ -189,6 +197,7 @@ public function execute() } catch(Exception $e) { + Yii::log('Error in executing SQL: '.$this->getText().$params,CLogger::LEVEL_ERROR,'system.db.CDbCommand'); throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}', array('{error}'=>$e->getMessage()))); } @@ -266,7 +275,8 @@ public function queryColumn() */ private function queryInternal($method,$mode) { - Yii::trace('query with SQL: '.$this->getText(),'system.db.CDbCommand'); + $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; + Yii::trace('Querying SQL: '.$this->getText().$params,'system.db.CDbCommand'); try { if($this->_statement instanceof PDOStatement) @@ -281,7 +291,7 @@ private function queryInternal($method,$mode) } catch(Exception $e) { - Yii::log('Error in executing SQL: '.$this->getText(),CLogger::LEVEL_ERROR,'system.db.CDbCommand'); + Yii::log('Error in querying SQL: '.$this->getText().$params,CLogger::LEVEL_ERROR,'system.db.CDbCommand'); throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}', array('{error}'=>$e->getMessage()))); } diff --git a/framework/db/CDbConnection.php b/framework/db/CDbConnection.php index d154994c47..1f3f69b452 100644 --- a/framework/db/CDbConnection.php +++ b/framework/db/CDbConnection.php @@ -130,6 +130,15 @@ class CDbConnection extends CApplicationComponent * the buggy native prepare support. Note, this property is only effective for PHP 5.1.3 or above. */ public $emulatePrepare=false; + /** + * @var boolean whether to log the values that are bound to a prepare SQL statement. + * Defaults to false. During development, you may consider setting this property to true + * so that parameter values bound to SQL statements are logged for debugging purpose. + * You should be aware that logging parameter values could be expensive and have significant + * impact on the performance of your application. + * @since 1.0.5 + */ + public $enableParamLogging=false; private $_attributes=array(); private $_active=false; diff --git a/framework/db/ar/CActiveFinder.php b/framework/db/ar/CActiveFinder.php index 197237533c..c368e126c0 100644 --- a/framework/db/ar/CActiveFinder.php +++ b/framework/db/ar/CActiveFinder.php @@ -246,10 +246,12 @@ private function buildJoinTree($parent,$with) if(($relation=$parent->model->getActiveRelation($with))!==null) { + $relation=clone $relation; + $model=CActiveRecord::model($relation->className); + if(($scope=$model->defaultScope())!==array()) + $relation->mergeWith($scope); if(isset($scopes) && !empty($scopes)) { - $model=CActiveRecord::model($relation->className); - $relation=clone $relation; $scs=$model->scopes(); foreach($scopes as $scope) { @@ -602,6 +604,8 @@ private function populateRecord($query,$row) $attributes[$aliases[$alias]]=$value; } $record=$this->model->populateRecord($attributes,false); + foreach($this->children as $child) + $record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation); $this->records[$pk]=$record; } @@ -925,6 +929,8 @@ private function joinManyMany($joinTable,$fks,$parent) $join.=' ON ('.implode(') AND (',$parentCondition).')'; $join.=' '.$this->relation->joinType.' '.$this->getTableNameWithAlias(); $join.=' ON ('.implode(') AND (',$childCondition).')'; + if(!empty($this->relation->on)) + $join.=' AND ('.str_replace($this->relation->aliasToken.'.', $this->tableAlias.'.', $this->relation->on).')'; return $join; } else diff --git a/framework/db/ar/CActiveRecord.php b/framework/db/ar/CActiveRecord.php index 021af8dfdf..648b4a7d57 100644 --- a/framework/db/ar/CActiveRecord.php +++ b/framework/db/ar/CActiveRecord.php @@ -426,11 +426,7 @@ public function __get($name) */ public function __set($name,$value) { - if(isset($this->getMetaData()->columns[$name])) - $this->_attributes[$name]=$value; - else if(isset($this->getMetaData()->relations[$name])) - $this->_related[$name]=$value; - else + if($this->setAttribute($name,$value)===false) parent::__set($name,$value); } @@ -900,7 +896,6 @@ public function hasAttribute($name) * You may also use $this->AttributeName to obtain the attribute value. * @param string the attribute name * @return mixed the attribute value. Null if the attribute is not set or does not exist. - * @throws CException if the attribute does not exist. * @see hasAttribute */ public function getAttribute($name) @@ -909,11 +904,6 @@ public function getAttribute($name) return $this->$name; else if(isset($this->_attributes[$name])) return $this->_attributes[$name]; - else if(isset($this->getMetaData()->columns[$name])) - return null; - else - throw new CDbException(Yii::t('yii','{class} does not have attribute "{name}".', - array('{class}'=>get_class($this), '{name}'=>$name))); } /** @@ -921,7 +911,7 @@ public function getAttribute($name) * You may also use $this->AttributeName to set the attribute value. * @param string the attribute name * @param mixed the attribute value. - * @throws CException if the attribute does not exist. + * @return boolean whether the attribute exists and the assignment is conducted successfully * @see hasAttribute */ public function setAttribute($name,$value) @@ -931,8 +921,8 @@ public function setAttribute($name,$value) else if(isset($this->getMetaData()->columns[$name])) $this->_attributes[$name]=$value; else - throw new CDbException(Yii::t('yii','{class} does not have attribute "{name}".', - array('{class}'=>get_class($this), '{name}'=>$name))); + return false; + return true; } /** diff --git a/framework/db/schema/pgsql/CPgsqlSchema.php b/framework/db/schema/pgsql/CPgsqlSchema.php index ddb2d8858c..5fc5f40c96 100644 --- a/framework/db/schema/pgsql/CPgsqlSchema.php +++ b/framework/db/schema/pgsql/CPgsqlSchema.php @@ -200,7 +200,7 @@ protected function findConstraints($table) */ protected function findPrimaryKey($table,$indices) { - $indices=join(', ',split(' ',$indices)); + $indices=implode(', ',preg_split('/\s+/',$indices)); $sql=<<getClientScript()->render($output); // if using page caching, we should delay dynamic output replacement - if(!$this->usePageCaching && $this->_dynamicOutput) + if($this->_dynamicOutput!==null && $this->isCachingStackEmpty()) $output=$this->processDynamicOutput($output); if($this->_pageStates===null) @@ -846,15 +840,27 @@ public function recordCachingAction($context,$method,$params) } /** + * @param boolean whether to create a stack if it does not exist yet. Defaults to true. * @return CStack stack of {@link COutputCache} objects */ - public function getCachingStack() + public function getCachingStack($createIfNull=true) { if(!$this->_cachingStack) $this->_cachingStack=new CStack; return $this->_cachingStack; } + /** + * @return whether the caching stack is empty. If not empty, it means currently there are + * some output cache in effect. Note, the return result of this method may change when it is + * called in different output regions, depending on the partition of output caches. + * @since 1.0.5 + */ + public function isCachingStackEmpty() + { + return $this->_cachingStack===null || !$this->_cachingStack->getCount(); + } + /** * This method is invoked right before an action is to be executed (after all possible filters.) * You may override this method to do last-minute preparation for the action. diff --git a/framework/web/CUrlManager.php b/framework/web/CUrlManager.php index c3a4109eba..c386447544 100644 --- a/framework/web/CUrlManager.php +++ b/framework/web/CUrlManager.php @@ -56,6 +56,25 @@ * and vice versa applies when constructing such a URL. * * + * Starting from version 1.0.5, the route part may contain references to named parameters defined + * in the pattern part. This allows a rule to be applied to different routes based on matching criteria. + * For example, + *
+ * array(
+ *      '<_c:(post|comment)>//<_a:(create|update|delete)>'=>'<_c>/<_a>',
+ *      '<_c:(post|comment)>/'=>'<_a>/view',
+ *      '<_c:(post|comment)>s/*'=>'<_a>/list',
+ * )
+ * 
+ * In the above, we use two named parameters '<_c>' and '<_a>' in the route part. The '<_c>' + * parameter matches either 'post' or 'comment', while the '<_a>' parameter matches an action ID. + * + * Like normal rules, these rules can be used for both parsing and creating URLs. + * For example, using the rules above, the URL '/index.php/post/123/create' + * would be parsed as the route 'post/create' with GET parameter 'id' being 123. + * And given the route 'post/list' and GET parameter 'page' being 2, we should get a URL + * '/index.php/posts/page/2'. + * * CUrlManager is a default application component that may be accessed via * {@link CWebApplication::getUrlManager()}. * @@ -70,6 +89,10 @@ class CUrlManager extends CApplicationComponent const GET_FORMAT='get'; const PATH_FORMAT='path'; + /** + * @var array the URL rules (pattern=>route). + */ + public $rules=array(); /** * @var string the URL suffix used when in 'path' format. * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. @@ -111,7 +134,6 @@ class CUrlManager extends CApplicationComponent private $_urlFormat=self::GET_FORMAT; private $_rules=array(); - private $_groups=array(); private $_baseUrl; @@ -129,41 +151,21 @@ public function init() */ protected function processRules() { - if(empty($this->_rules) || $this->getUrlFormat()===self::GET_FORMAT) + if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) return; if($this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null) { - $hash=md5(serialize($this->_rules)); + $hash=md5(serialize($this->rules)); if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) { - $this->_groups=$data[0]; + $this->_rules=$data[0]; return; } } - foreach($this->_rules as $pattern=>$route) - $this->_groups[$route][]=new CUrlRule($route,$pattern); + foreach($this->rules as $pattern=>$route) + $this->_rules[]=new CUrlRule($route,$pattern); if(isset($cache)) - $cache->set(self::CACHE_KEY,array($this->_groups,$hash)); - } - - /** - * @return array the URL rules - */ - public function getRules() - { - return $this->_rules; - } - - /** - * Sets the URL rules. - * @param array the URL rules (pattern=>route) - */ - public function setRules($value) - { - if($this->_rules===array()) - $this->_rules=$value; - else - $this->_rules=array_merge($this->_rules,$value); + $cache->set(self::CACHE_KEY,array($this->_rules,$hash)); } /** @@ -185,13 +187,10 @@ public function createUrl($route,$params=array(),$ampersand='&') } else $anchor=''; - if(isset($this->_groups[$route])) + foreach($this->_rules as $rule) { - foreach($this->_groups[$route] as $rule) - { - if(($url=$rule->createUrl($params,$this->urlSuffix,$ampersand))!==false) - return $this->getBaseUrl().'/'.$url.$anchor; - } + if(($url=$rule->createUrl($route,$params,$this->urlSuffix,$ampersand))!==false) + return $this->getBaseUrl().'/'.$url.$anchor; } return $this->createUrlDefault($route,$params,$ampersand).$anchor; } @@ -246,13 +245,10 @@ public function parseUrl($request) if($this->getUrlFormat()===self::PATH_FORMAT) { $pathInfo=$this->removeUrlSuffix($request->getPathInfo()); - foreach($this->_groups as $rules) + foreach($this->_rules as $rule) { - foreach($rules as $rule) - { - if(($r=$rule->parseUrl($pathInfo))!==false) - return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; - } + if(($r=$rule->parseUrl($pathInfo))!==false) + return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; } return $pathInfo; } @@ -370,7 +366,7 @@ public function setUrlFormat($value) * It mainly consists of two parts: route and pattern. The former classifies * the rule so that it only applies to specific controller-action route. * The latter performs the actual formatting and parsing role. The pattern - * may have a set of named parameters each of specific format. + * may have a set of named parameters. * * @author Qiang Xue * @version $Id$ @@ -383,6 +379,16 @@ class CUrlRule extends CComponent * @var string the controller/action pair */ public $route; + /** + * @var array the mapping from route param name to token name (e.g. _r1=><1>) + * @since 1.0.5 + */ + public $references=array(); + /** + * @var string the pattern used to match route + * @since 1.0.5 + */ + public $routePattern; /** * @var string regular expression used to parse a URL */ @@ -394,15 +400,11 @@ class CUrlRule extends CComponent /** * @var array list of parameters (name=>regular expression) */ - public $params; + public $params=array(); /** * @var boolean whether the URL allows additional parameters at the end of the path info. */ public $append; - /** - * @var string a token identifies the rule to a certain degree - */ - public $signature; /** * @var boolean whether the rule is case sensitive. Defaults to true. * @since 1.0.1 @@ -417,22 +419,31 @@ class CUrlRule extends CComponent public function __construct($route,$pattern) { $this->route=$route; + $tr2['/']=$tr['/']='\\/'; + + if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) + { + foreach($matches2[1] as $name) + $this->references[$name]="<$name>"; + } + if(preg_match_all('/<(\w+):?(.*?)?>/',$pattern,$matches)) - $this->params=array_combine($matches[1],$matches[2]); - else - $this->params=array(); + { + $tokens=array_combine($matches[1],$matches[2]); + foreach($tokens as $name=>$value) + { + $tr["<$name>"]="(?P<$name>".($value!==''?$value:'[^\/]+').')'; + if(isset($this->references[$name])) + $tr2["<$name>"]=$tr["<$name>"]; + else + $this->params[$name]=$value; + } + } $p=rtrim($pattern,'*'); $this->append=$p!==$pattern; $p=trim($p,'/'); $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); - if(($pos=strpos($p,'<'))!==false) - $this->signature=substr($p,0,$pos); - else - $this->signature=$p; - $tr['/']='\\/'; - foreach($this->params as $key=>$value) - $tr["<$key>"]="(?P<$key>".($value!==''?$value:'[^\/]+').')'; $this->pattern='/^'.strtr($this->template,$tr).'\/'; if($this->append) $this->pattern.='/u'; @@ -440,20 +451,40 @@ public function __construct($route,$pattern) $this->pattern.='$/u'; if(!$this->caseSensitive) $this->pattern.='i'; + + if($this->references!==array()) + { + $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; + if(!$this->caseSensitive) + $this->routePattern.='i'; + } + if(YII_DEBUG && @preg_match($this->pattern,'test')===false) throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', array('{route}'=>$route,'{pattern}'=>$pattern))); } /** + * @param string the route * @param array list of parameters * @param string URL suffix * @param string the token separating name-value pairs in the URL. * @return string the constructed URL */ - public function createUrl($params,$suffix,$ampersand) + public function createUrl($route,$params,$suffix,$ampersand) { $tr=array(); + if($route!==$this->route) + { + if($this->routePattern!==null && preg_match($this->routePattern,$route,$matches)) + { + foreach($this->references as $key=>$name) + $tr[$name]=$matches[$key]; + } + else + return false; + } + foreach($this->params as $key=>$value) { if(isset($params[$key])) @@ -464,6 +495,7 @@ public function createUrl($params,$suffix,$ampersand) else return false; } + $url=strtr($this->template,$tr); if(empty($params)) return $url!=='' ? $url.$suffix : $url; @@ -485,21 +517,23 @@ public function createUrl($params,$suffix,$ampersand) */ public function parseUrl($pathInfo) { - $func=$this->caseSensitive?'strncmp':'strncasecmp'; - if($func($pathInfo,$this->signature,strlen($this->signature))) - return false; - $pathInfo.='/'; if(preg_match($this->pattern,$pathInfo,$matches)) { + $tr=array(); foreach($matches as $key=>$value) { - if(is_string($key)) + if(isset($this->references[$key])) + $tr[$this->references[$key]]=urldecode($value); + else if(isset($this->params[$key])) $_GET[$key]=urldecode($value); } if($pathInfo!==$matches[0]) // there're additional GET params CUrlManager::parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/')); - return $this->route; + if($this->routePattern!==null) + return strtr($this->route,$tr); + else + return $this->route; } else return false; diff --git a/framework/web/widgets/COutputCache.php b/framework/web/widgets/COutputCache.php index 0a98b6e453..df93b2b3a4 100644 --- a/framework/web/widgets/COutputCache.php +++ b/framework/web/widgets/COutputCache.php @@ -149,8 +149,6 @@ public function filter($filterChain) */ public function init() { - $this->getController()->usePageCaching=$this->getIsFilter(); - if($this->getIsContentCached()) $this->replayActions(); else if($this->_cache!==null) @@ -170,7 +168,12 @@ public function init() public function run() { if($this->getIsContentCached()) - echo $this->getController()->processDynamicOutput($this->_content); + { + if($this->getController()->isCachingStackEmpty()) + echo $this->getController()->processDynamicOutput($this->_content); + else + echo $this->_content; + } else if($this->_cache!==null) { $this->_content=ob_get_clean(); @@ -179,7 +182,11 @@ public function run() if(is_array($this->dependency)) $this->dependency=Yii::createComponent($this->dependency); $this->_cache->set($this->getCacheKey(),$data,$this->duration>0 ? $this->duration : 0,$this->dependency); - echo $this->getController()->processDynamicOutput($this->_content); + + if($this->getController()->isCachingStackEmpty()) + echo $this->getController()->processDynamicOutput($this->_content); + else + echo $this->_content; } } @@ -306,7 +313,6 @@ protected function replayActions() { if(empty($this->_actions)) return; - $controller=$this->getController(); $cs=Yii::app()->getClientScript(); foreach($this->_actions as $action) diff --git a/framework/yiilite.php b/framework/yiilite.php index 95a663af6e..87415fdd2b 100644 --- a/framework/yiilite.php +++ b/framework/yiilite.php @@ -174,7 +174,11 @@ public static function autoload($className) else if(isset(self::$_classes[$className])) include(self::$_classes[$className]); else - include($className.'.php'); + { + @include($className.'.php'); + return class_exists($className,false); + } + return true; } public static function trace($msg,$category='application') { @@ -242,6 +246,7 @@ public static function t($category,$message,$params=array(),$source=null,$langua 'CApcCache' => '/caching/CApcCache.php', 'CCache' => '/caching/CCache.php', 'CDbCache' => '/caching/CDbCache.php', + 'CDummyCache' => '/caching/CDummyCache.php', 'CEAcceleratorCache' => '/caching/CEAcceleratorCache.php', 'CMemCache' => '/caching/CMemCache.php', 'CXCache' => '/caching/CXCache.php', @@ -753,6 +758,16 @@ public function setImport($aliases) foreach($aliases as $alias) Yii::import($alias); } + public function setAliases($mappings) + { + foreach($mappings as $name=>$alias) + { + if(($path=Yii::getPathOfAlias($alias))!==false) + Yii::setPathOfAlias($name,$path); + else + Yii::setPathOfAlias($name,$alias); + } + } public function getParentModule() { return $this->_parentModule; @@ -2125,6 +2140,7 @@ class CUrlManager extends CApplicationComponent const CACHE_KEY='CUrlManager.rules'; const GET_FORMAT='get'; const PATH_FORMAT='path'; + public $rules=array(); public $urlSuffix=''; public $showScriptName=true; public $appendParams=true; @@ -2133,7 +2149,6 @@ class CUrlManager extends CApplicationComponent public $cacheID='cache'; private $_urlFormat=self::GET_FORMAT; private $_rules=array(); - private $_groups=array(); private $_baseUrl; public function init() { @@ -2142,32 +2157,21 @@ public function init() } protected function processRules() { - if(empty($this->_rules) || $this->getUrlFormat()===self::GET_FORMAT) + if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) return; if($this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null) { - $hash=md5(serialize($this->_rules)); + $hash=md5(serialize($this->rules)); if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) { - $this->_groups=$data[0]; + $this->_rules=$data[0]; return; } } - foreach($this->_rules as $pattern=>$route) - $this->_groups[$route][]=new CUrlRule($route,$pattern); + foreach($this->rules as $pattern=>$route) + $this->_rules[]=new CUrlRule($route,$pattern); if(isset($cache)) - $cache->set(self::CACHE_KEY,array($this->_groups,$hash)); - } - public function getRules() - { - return $this->_rules; - } - public function setRules($value) - { - if($this->_rules===array()) - $this->_rules=$value; - else - $this->_rules=array_merge($this->_rules,$value); + $cache->set(self::CACHE_KEY,array($this->_rules,$hash)); } public function createUrl($route,$params=array(),$ampersand='&') { @@ -2179,13 +2183,10 @@ public function createUrl($route,$params=array(),$ampersand='&') } else $anchor=''; - if(isset($this->_groups[$route])) + foreach($this->_rules as $rule) { - foreach($this->_groups[$route] as $rule) - { - if(($url=$rule->createUrl($params,$this->urlSuffix,$ampersand))!==false) - return $this->getBaseUrl().'/'.$url.$anchor; - } + if(($url=$rule->createUrl($route,$params,$this->urlSuffix,$ampersand))!==false) + return $this->getBaseUrl().'/'.$url.$anchor; } return $this->createUrlDefault($route,$params,$ampersand).$anchor; } @@ -2226,13 +2227,10 @@ public function parseUrl($request) if($this->getUrlFormat()===self::PATH_FORMAT) { $pathInfo=$this->removeUrlSuffix($request->getPathInfo()); - foreach($this->_groups as $rules) + foreach($this->_rules as $rule) { - foreach($rules as $rule) - { - if(($r=$rule->parseUrl($pathInfo))!==false) - return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; - } + if(($r=$rule->parseUrl($pathInfo))!==false) + return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; } return $pathInfo; } @@ -2310,30 +2308,38 @@ public function setUrlFormat($value) class CUrlRule extends CComponent { public $route; + public $references=array(); + public $routePattern; public $pattern; public $template; - public $params; + public $params=array(); public $append; - public $signature; public $caseSensitive=true; public function __construct($route,$pattern) { $this->route=$route; + $tr2['/']=$tr['/']='\\/'; + if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) + { + foreach($matches2[1] as $name) + $this->references[$name]="<$name>"; + } if(preg_match_all('/<(\w+):?(.*?)?>/',$pattern,$matches)) - $this->params=array_combine($matches[1],$matches[2]); - else - $this->params=array(); + { + $tokens=array_combine($matches[1],$matches[2]); + foreach($tokens as $name=>$value) + { + $tr["<$name>"]="(?P<$name>".($value!==''?$value:'[^\/]+').')'; + if(isset($this->references[$name])) + $tr2["<$name>"]=$tr["<$name>"]; + else + $this->params[$name]=$value; + } + } $p=rtrim($pattern,'*'); $this->append=$p!==$pattern; $p=trim($p,'/'); $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); - if(($pos=strpos($p,'<'))!==false) - $this->signature=substr($p,0,$pos); - else - $this->signature=$p; - $tr['/']='\\/'; - foreach($this->params as $key=>$value) - $tr["<$key>"]="(?P<$key>".($value!==''?$value:'[^\/]+').')'; $this->pattern='/^'.strtr($this->template,$tr).'\/'; if($this->append) $this->pattern.='/u'; @@ -2341,13 +2347,29 @@ public function __construct($route,$pattern) $this->pattern.='$/u'; if(!$this->caseSensitive) $this->pattern.='i'; - if(@preg_match($this->pattern,'test')===false) + if($this->references!==array()) + { + $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; + if(!$this->caseSensitive) + $this->routePattern.='i'; + } + if(YII_DEBUG && @preg_match($this->pattern,'test')===false) throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', array('{route}'=>$route,'{pattern}'=>$pattern))); } - public function createUrl($params,$suffix,$ampersand) + public function createUrl($route,$params,$suffix,$ampersand) { $tr=array(); + if($route!==$this->route) + { + if($this->routePattern!==null && preg_match($this->routePattern,$route,$matches)) + { + foreach($this->references as $key=>$name) + $tr[$name]=$matches[$key]; + } + else + return false; + } foreach($this->params as $key=>$value) { if(isset($params[$key])) @@ -2373,20 +2395,23 @@ public function createUrl($params,$suffix,$ampersand) } public function parseUrl($pathInfo) { - $func=$this->caseSensitive?'strncmp':'strncasecmp'; - if($func($pathInfo,$this->signature,strlen($this->signature))) - return false; $pathInfo.='/'; if(preg_match($this->pattern,$pathInfo,$matches)) { + $tr=array(); foreach($matches as $key=>$value) { - if(is_string($key)) + if(isset($this->references[$key])) + $tr[$this->references[$key]]=urldecode($value); + else if(isset($this->params[$key])) $_GET[$key]=urldecode($value); } if($pathInfo!==$matches[0]) // there're additional GET params CUrlManager::parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/')); - return $this->route; + if($this->routePattern!==null) + return strtr($this->route,$tr); + else + return $this->route; } else return false; @@ -2573,11 +2598,10 @@ public function processOutput($output) // if using page caching, we should delay dynamic output replacement if(!$this->usePageCaching && $this->_dynamicOutput) $output=$this->processDynamicOutput($output); - if($this->_pageStates!==null || isset($_POST[self::STATE_INPUT_NAME])) - { - $states=$this->savePageStates(); - $output=str_replace(CHtml::pageStateField(''),CHtml::pageStateField($states),$output); - } + if($this->_pageStates===null) + $this->_pageStates=$this->loadPageStates(); + if(!empty($this->_pageStates)) + $this->savePageStates($this->_pageStates,$output); return $output; } public function processDynamicOutput($output) @@ -2871,13 +2895,13 @@ public function paginate($itemCount,$pageSize=null,$pageVar=null) public function getPageState($name,$defaultValue=null) { if($this->_pageStates===null) - $this->loadPageStates(); + $this->_pageStates=$this->loadPageStates(); return isset($this->_pageStates[$name])?$this->_pageStates[$name]:$defaultValue; } public function setPageState($name,$value,$defaultValue=null) { if($this->_pageStates===null) - $this->loadPageStates(); + $this->_pageStates=$this->loadPageStates(); if($value===$defaultValue) unset($this->_pageStates[$name]); else @@ -2898,27 +2922,18 @@ protected function loadPageStates() if(extension_loaded('zlib')) $data=@gzuncompress($data); if(($data=Yii::app()->getSecurityManager()->validateData($data))!==false) - { - $this->_pageStates=unserialize($data); - return; - } + return unserialize($data); } } - $this->_pageStates=array(); + return array(); } - protected function savePageStates() + protected function savePageStates($states,&$output) { - if($this->_pageStates===null) - $this->loadPageStates(); - if(empty($this->_pageStates)) - return ''; - else - { - $data=Yii::app()->getSecurityManager()->hashData(serialize($this->_pageStates)); - if(extension_loaded('zlib')) - $data=gzcompress($data); - return base64_encode($data); - } + $data=Yii::app()->getSecurityManager()->hashData(serialize($states)); + if(extension_loaded('zlib')) + $data=gzcompress($data); + $value=base64_encode($data); + $output=str_replace(CHtml::pageStateField(''),CHtml::pageStateField($value),$output); } } abstract class CAction extends CComponent implements IAction @@ -3461,9 +3476,7 @@ public static function encodeArray($data) } public static function tag($tag,$htmlOptions=array(),$content=false,$closeTag=true) { - $html='<' . $tag; - foreach($htmlOptions as $name=>$value) - $html .= ' ' . $name . '="' . self::encode($value) . '"'; + $html='<' . $tag . self::renderAttributes($htmlOptions); if($content===false) return $closeTag ? $html.' />' : $html.'>'; else @@ -3471,10 +3484,7 @@ public static function tag($tag,$htmlOptions=array(),$content=false,$closeTag=tr } public static function openTag($tag,$htmlOptions=array()) { - $html='<' . $tag; - foreach($htmlOptions as $name=>$value) - $html .= ' ' . $name . '="' . self::encode($value) . '"'; - return $html . '>'; + return '<' . $tag . self::renderAttributes($htmlOptions) . '>'; } public static function closeTag($tag) { @@ -4016,24 +4026,36 @@ public static function listData($models,$valueField,$textField,$groupField='') { foreach($models as $model) { - if(is_object($model)) - $listData[$model->$valueField]=$model->$textField; - else - $listData[$model[$valueField]]=$model[$textField]; + $value=self::value($model,$valueField); + $text=self::value($model,$textField); + $listData[$value]=$text; } } else { foreach($models as $model) { - if(is_object($model)) - $listData[$model->$groupField][$model->$valueField]=$model->$textField; - else - $listData[$model[$groupField]][$model[$valueField]]=$model[$textField]; + $group=self::value($model,$groupField); + $value=self::value($model,$valueField); + $text=self::value($model,$textField); + $listData[$group][$value]=$text; } } return $listData; } + public static function value($model,$attribute,$defaultValue=null) + { + foreach(explode('.',$attribute) as $name) + { + if(is_object($model)) + $model=$model->$name; + else if(is_array($model) && isset($model[$name])) + $model=$model[$name]; + else + return $defaultValue; + } + return $model; + } public static function getIdByName($name) { return str_replace(array('[]', '][', '[', ']'), array('', '_', '_', ''), $name); @@ -4060,15 +4082,16 @@ protected static function activeInputField($type,$model,$attribute,$htmlOptions) } public static function listOptions($selection,$listData,&$htmlOptions) { + $raw=isset($htmlOptions['encode']) && !$htmlOptions['encode']; $content=''; if(isset($htmlOptions['prompt'])) { - $content.='\n"; + $content.='\n"; unset($htmlOptions['prompt']); } if(isset($htmlOptions['empty'])) { - $content.='\n"; + $content.='\n"; unset($htmlOptions['empty']); } if(isset($htmlOptions['options'])) @@ -4082,19 +4105,19 @@ public static function listOptions($selection,$listData,&$htmlOptions) { if(is_array($value)) { - $content.='\n"; + $content.='\n"; $dummy=array(); $content.=self::listOptions($selection,$value,$dummy); $content.=''."\n"; } else { - $attributes=array('value'=>(string)$key); + $attributes=array('value'=>(string)$key, 'encode'=>!$raw); if(!is_array($selection) && !strcmp($key,$selection) || is_array($selection) && in_array($key,$selection)) $attributes['selected']='selected'; if(isset($options[$key])) $attributes=array_merge($attributes,$options[$key]); - $content.=CHtml::tag('option',$attributes,self::encode((string)$value))."\n"; + $content.=self::tag('option',$attributes,$raw?(string)$value : self::encode((string)$value))."\n"; } } return $content; @@ -4171,6 +4194,25 @@ protected static function addErrorCss(&$htmlOptions) else $htmlOptions['class']=self::$errorCss; } + protected static function renderAttributes($htmlOptions) + { + if($htmlOptions===array()) + return ''; + $html=''; + $raw=isset($htmlOptions['encode']) && !$htmlOptions['encode']; + unset($htmlOptions['encode']); + if($raw) + { + foreach($htmlOptions as $name=>$value) + $html .= ' ' . $name . '="' . $value . '"'; + } + else + { + foreach($htmlOptions as $name=>$value) + $html .= ' ' . $name . '="' . self::encode($value) . '"'; + } + return $html; + } } class CWidget extends CBaseController { @@ -5090,6 +5132,19 @@ public function addError($attribute,$error) { $this->_errors[$attribute][]=$error; } + public function addErrors($errors) + { + foreach($errors as $attribute=>$error) + { + if(is_array($error)) + { + foreach($error as $e) + $this->_errors[$attribute][]=$e; + } + else + $this->_errors[$attribute][]=$error; + } + } public function clearErrors($attribute=null) { if($attribute===null) @@ -5239,11 +5294,7 @@ public function __get($name) } public function __set($name,$value) { - if(isset($this->getMetaData()->columns[$name])) - $this->_attributes[$name]=$value; - else if(isset($this->getMetaData()->relations[$name])) - $this->_related[$name]=$value; - else + if($this->setAttribute($name,$value)===false) parent::__set($name,$value); } public function __isset($name) @@ -5342,12 +5393,19 @@ public function hasRelated($name) { return isset($this->_related[$name]) || array_key_exists($name,$this->_related); } - public function getDbCriteria() + public function getDbCriteria($createIfNull=true) { if($this->_c===null) - $this->_c=new CDbCriteria; + { + if(($c=$this->defaultScope())!==array() || $createIfNull) + $this->_c=new CDbCriteria($c); + } return $this->_c; } + public function defaultScope() + { + return array(); + } public static function model($className=__CLASS__) { if(isset(self::$_models[$className])) @@ -5434,11 +5492,6 @@ public function getAttribute($name) return $this->$name; else if(isset($this->_attributes[$name])) return $this->_attributes[$name]; - else if(isset($this->getMetaData()->columns[$name])) - return null; - else - throw new CDbException(Yii::t('yii','{class} does not have attribute "{name}".', - array('{class}'=>get_class($this), '{name}'=>$name))); } public function setAttribute($name,$value) { @@ -5447,8 +5500,8 @@ public function setAttribute($name,$value) else if(isset($this->getMetaData()->columns[$name])) $this->_attributes[$name]=$value; else - throw new CDbException(Yii::t('yii','{class} does not have attribute "{name}".', - array('{class}'=>get_class($this), '{name}'=>$name))); + return false; + return true; } public function addRelatedRecord($name,$record,$multiple) { @@ -5460,7 +5513,8 @@ public function addRelatedRecord($name,$record,$multiple) $this->_related[$name][]=$record; } else if(!isset($this->_related[$name])) - $this->_related[$name]=$record; } + $this->_related[$name]=$record; + } public function getAttributes($names=true) { $attributes=$this->_attributes; @@ -5676,10 +5730,10 @@ public function getPrimaryKey() } private function query($criteria,$all=false) { - if($this->_c!==null) + if(($c=$this->getDbCriteria(false))!==null) { - $this->_c->mergeWith($criteria); - $criteria=$this->_c; + $c->mergeWith($criteria); + $criteria=$c; $this->_c=null; } $command=$this->getCommandBuilder()->createFindCommand($this->getTableSchema(),$criteria); @@ -5732,10 +5786,10 @@ public function count($condition='',$params=array()) { $builder=$this->getCommandBuilder(); $criteria=$builder->createCriteria($condition,$params); - if($this->_c!==null) + if(($c=$this->getDbCriteria(false))!==null) { - $this->_c->mergeWith($criteria); - $criteria=$this->_c; + $c->mergeWith($criteria); + $criteria=$c; $this->_c=null; } return $builder->createCountCommand($this->getTableSchema(),$criteria)->queryScalar(); @@ -5751,10 +5805,10 @@ public function exists($condition,$params=array()) $table=$this->getTableSchema(); $criteria->select=reset($table->columns)->rawName; $criteria->limit=1; - if($this->_c!==null) + if(($c=$this->getDbCriteria(false))!==null) { - $this->_c->mergeWith($criteria); - $criteria=$this->_c; + $c->mergeWith($criteria); + $criteria=$c; $this->_c=null; } return $builder->createFindCommand($table,$criteria)->queryRow()!==false; @@ -5766,7 +5820,7 @@ public function with() $with=func_get_args(); if(is_array($with[0])) // the parameter is given as an array $with=$with[0]; - $finder=new CActiveFinder($this,$with,$this->_c); + $finder=new CActiveFinder($this,$with,$this->getDbCriteria(false)); $this->_c=null; return $finder; } @@ -6046,6 +6100,7 @@ class CDbConnection extends CApplicationComponent public $autoConnect=true; public $charset; public $emulatePrepare=false; + public $enableParamLogging=false; private $_attributes=array(); private $_active=false; private $_pdo; @@ -6495,6 +6550,7 @@ class CDbCommand extends CComponent private $_connection; private $_text=''; private $_statement=null; + private $_params; public function __construct(CDbConnection $connection,$text) { $this->_connection=$connection; @@ -6529,9 +6585,11 @@ public function prepare() try { $this->_statement=$this->getConnection()->getPdoInstance()->prepare($this->getText()); + $this->_params=array(); } catch(Exception $e) { + Yii::log('Error in preparing SQL: '.$this->getText(),CLogger::LEVEL_ERROR,'system.db.CDbCommand'); throw new CDbException(Yii::t('yii','CDbCommand failed to prepare the SQL statement: {error}', array('{error}'=>$e->getMessage()))); } @@ -6550,6 +6608,8 @@ public function bindParam($name, &$value, $dataType=null, $length=null) $this->_statement->bindParam($name,$value,$dataType); else $this->_statement->bindParam($name,$value,$dataType,$length); + if($this->_connection->enableParamLogging) + $this->_params[]=$name.'=['.gettype($value).']'; } public function bindValue($name, $value, $dataType=null) { @@ -6558,9 +6618,12 @@ public function bindValue($name, $value, $dataType=null) $this->_statement->bindValue($name,$value,$this->_connection->getPdoType(gettype($value))); else $this->_statement->bindValue($name,$value,$dataType); + if($this->_connection->enableParamLogging) + $this->_params[]=$name.'='.var_export($value,true); } public function execute() { + $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; try { if($this->_statement instanceof PDOStatement) @@ -6573,6 +6636,7 @@ public function execute() } catch(Exception $e) { + Yii::log('Error in executing SQL: '.$this->getText().$params,CLogger::LEVEL_ERROR,'system.db.CDbCommand'); throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}', array('{error}'=>$e->getMessage()))); } @@ -6603,6 +6667,7 @@ public function queryColumn() } private function queryInternal($method,$mode) { + $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; try { if($this->_statement instanceof PDOStatement) @@ -6617,7 +6682,7 @@ private function queryInternal($method,$mode) } catch(Exception $e) { - Yii::log('Error in executing SQL: '.$this->getText(),CLogger::LEVEL_ERROR,'system.db.CDbCommand'); + Yii::log('Error in querying SQL: '.$this->getText().$params,CLogger::LEVEL_ERROR,'system.db.CDbCommand'); throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}', array('{error}'=>$e->getMessage()))); } diff --git a/tests/unit/framework/db/ar/CActiveRecordTest.php b/tests/unit/framework/db/ar/CActiveRecordTest.php index f3fdacecd3..6f48baf9ff 100644 --- a/tests/unit/framework/db/ar/CActiveRecordTest.php +++ b/tests/unit/framework/db/ar/CActiveRecordTest.php @@ -736,4 +736,14 @@ public function testScopeWithRelations() $this->assertEquals(2,$user->posts[0]->id); $this->assertEquals(3,$user->posts[1]->id); } + + public function testDuplicateLazyLoadingBug() + { + $user=User::model()->with(array( + 'posts'=>array('condition'=>'??.id=-1') + ))->findByPk(1); + // with the bug, an eager loading for 'posts' would be trigger in the following + // and result with non-empty posts + $this->assertTrue($user->posts===array()); + } } diff --git a/tests/unit/framework/db/schema/CMysqlTest.php b/tests/unit/framework/db/schema/CMysqlTest.php index 117b19144d..a19b04a38e 100644 --- a/tests/unit/framework/db/schema/CMysqlTest.php +++ b/tests/unit/framework/db/schema/CMysqlTest.php @@ -142,7 +142,7 @@ public function testCommandBuilder() $table=$schema->getTable('posts'); $c=$builder->createInsertCommand($table,array('title'=>'test post','create_time'=>'2000-01-01','author_id'=>1,'content'=>'test content')); - $this->assertEquals('INSERT INTO `posts` (`title`, `create_time`, `author_id`, `content`) VALUES (:title, :create_time, :author_id, :content)',$c->text); + $this->assertEquals('INSERT INTO `posts` (`title`, `create_time`, `author_id`, `content`) VALUES (:_p0, :_p1, :_p2, :_p3)',$c->text); $c->execute(); $this->assertEquals(6,$builder->getLastInsertId($table)); @@ -238,7 +238,7 @@ public function testCommandBuilder() // createColumnCriteria $c=$builder->createColumnCriteria($table,array('id'=>1,'author_id'=>2),'title=``'); - $this->assertEquals('`posts`.`id`=:id AND `posts`.`author_id`=:author_id AND (title=``)',$c->condition); + $this->assertEquals('`posts`.`id`=:_p0 AND `posts`.`author_id`=:_p1 AND (title=``)',$c->condition); $c=$builder->createPkCriteria($table2,array()); $this->assertEquals('0=1',$c->condition); diff --git a/tests/unit/framework/db/schema/CPostgresTest.php b/tests/unit/framework/db/schema/CPostgresTest.php index cf43bc10c7..6d1fd465d3 100644 --- a/tests/unit/framework/db/schema/CPostgresTest.php +++ b/tests/unit/framework/db/schema/CPostgresTest.php @@ -137,7 +137,7 @@ public function testCommandBuilder() $table=$schema->getTable('test.posts'); $c=$builder->createInsertCommand($table,array('title'=>'test post','create_time'=>'2004-10-19 10:23:54','author_id'=>1,'content'=>'test content')); - $this->assertEquals('INSERT INTO "test"."posts" ("title", "create_time", "author_id", "content") VALUES (:title, :create_time, :author_id, :content)',$c->text); + $this->assertEquals('INSERT INTO "test"."posts" ("title", "create_time", "author_id", "content") VALUES (:_p0, :_p1, :_p2, :_p3)',$c->text); $c->execute(); $this->assertEquals(6,$builder->getLastInsertId($table)); @@ -230,6 +230,6 @@ public function testCommandBuilder() // createColumnCriteria $c=$builder->createColumnCriteria($table,array('id'=>1,'author_id'=>2),'title=\'\''); - $this->assertEquals('"test"."posts"."id"=:id AND "test"."posts"."author_id"=:author_id AND (title=\'\')',$c->condition); + $this->assertEquals('"test"."posts"."id"=:_p0 AND "test"."posts"."author_id"=:_p1 AND (title=\'\')',$c->condition); } } \ No newline at end of file diff --git a/tests/unit/framework/db/schema/CSqliteTest.php b/tests/unit/framework/db/schema/CSqliteTest.php index be28f77ab9..6b26ff9fa5 100644 --- a/tests/unit/framework/db/schema/CSqliteTest.php +++ b/tests/unit/framework/db/schema/CSqliteTest.php @@ -123,7 +123,7 @@ public function testCommandBuilder() $table=$schema->getTable('posts'); $c=$builder->createInsertCommand($table,array('title'=>'test post','create_time'=>time(),'author_id'=>1,'content'=>'test content')); - $this->assertEquals('INSERT INTO \'posts\' ("title", "create_time", "author_id", "content") VALUES (:title, :create_time, :author_id, :content)',$c->text); + $this->assertEquals('INSERT INTO \'posts\' ("title", "create_time", "author_id", "content") VALUES (:_p0, :_p1, :_p2, :_p3)',$c->text); $c->execute(); $this->assertEquals(6,$builder->getLastInsertId($table)); @@ -219,7 +219,7 @@ public function testCommandBuilder() // createColumnCriteria $c=$builder->createColumnCriteria($table,array('id'=>1,'author_id'=>2),'title=\'\''); - $this->assertEquals('\'posts\'."id"=:id AND \'posts\'."author_id"=:author_id AND (title=\'\')',$c->condition); + $this->assertEquals('\'posts\'."id"=:_p0 AND \'posts\'."author_id"=:_p1 AND (title=\'\')',$c->condition); $c=$builder->createPkCriteria($table2,array()); $this->assertEquals('0=1',$c->condition); diff --git a/tests/unit/framework/web/CUrlManagerTest.php b/tests/unit/framework/web/CUrlManagerTest.php index eadf7dc667..b890a293e3 100644 --- a/tests/unit/framework/web/CUrlManagerTest.php +++ b/tests/unit/framework/web/CUrlManagerTest.php @@ -41,6 +41,9 @@ public function testParseUrlWithPathFormat() 'register/*'=>'user', 'home/*'=>'', 'ad/*'=>'admin/index/list', + '//'=>'/', + '/'=>'/view', + 's/*'=>'/list', ); $entries=array( array( @@ -58,13 +61,6 @@ public function testParseUrlWithPathFormat() 'route'=>'article/read', 'params'=>array('year'=>'2000','title'=>'title goes here'), ), - /* - array( - 'pathInfo'=>'a/edit/title/title goes here', - 'route'=>'article/edit', - 'params'=>array('_a'=>'edit','title'=>'title goes here'), - ), - */ array( 'pathInfo'=>'article/2000/title goes here/name/value', 'route'=>'article/read', @@ -115,6 +111,31 @@ public function testParseUrlWithPathFormat() 'route'=>'admin/name/value', 'params'=>array(), ), + array( + 'pathInfo'=>'posts', + 'route'=>'post/list', + 'params'=>array(), + ), + array( + 'pathInfo'=>'posts/page/3', + 'route'=>'post/list', + 'params'=>array('page'=>3), + ), + array( + 'pathInfo'=>'post/3', + 'route'=>'post/view', + 'params'=>array('id'=>3), + ), + array( + 'pathInfo'=>'post/3/delete', + 'route'=>'post/delete', + 'params'=>array('id'=>3), + ), + array( + 'pathInfo'=>'post/3/delete/a', + 'route'=>'post/3/delete/a', + 'params'=>array(), + ), ); $config=array( 'basePath'=>dirname(__FILE__), @@ -157,6 +178,9 @@ public function testcreateUrlWithPathFormat() 'a/<_a>/*'=>'article', 'register/*'=>'user', 'home/*'=>'', + '//'=>'/', + '/'=>'/view', + 's/*'=>'/list', ); $config=array( 'basePath'=>dirname(__FILE__), @@ -168,6 +192,38 @@ public function testcreateUrlWithPathFormat() ); $app=new TestApplication($config); $entries=array( + array( + 'scriptUrl'=>'/apps/index.php', + 'url'=>'/apps/index.php/post/123?name1=value1', + 'url2'=>'/apps/post/123?name1=value1', + 'url3'=>'/apps/post/123.html?name1=value1', + 'route'=>'post/view', + 'params'=>array( + 'id'=>'123', + 'name1'=>'value1', + ), + ), + array( + 'scriptUrl'=>'/apps/index.php', + 'url'=>'/apps/index.php/post/123/update?name1=value1', + 'url2'=>'/apps/post/123/update?name1=value1', + 'url3'=>'/apps/post/123/update.html?name1=value1', + 'route'=>'post/update', + 'params'=>array( + 'id'=>'123', + 'name1'=>'value1', + ), + ), + array( + 'scriptUrl'=>'/apps/index.php', + 'url'=>'/apps/index.php/posts/page/123', + 'url2'=>'/apps/posts/page/123', + 'url3'=>'/apps/posts/page/123.html', + 'route'=>'post/list', + 'params'=>array( + 'page'=>'123', + ), + ), array( 'scriptUrl'=>'/apps/index.php', 'url'=>'/apps/index.php/article/123?name1=value1',