+
$generator) {
@@ -24,7 +24,7 @@ $activeGenerator = Yii::$app->controller->generator;
?>
-
diff --git a/extensions/yii/jui/CHANGELOG.md b/extensions/yii/jui/CHANGELOG.md
index 6f3cfb9..b31c34e 100644
--- a/extensions/yii/jui/CHANGELOG.md
+++ b/extensions/yii/jui/CHANGELOG.md
@@ -4,7 +4,7 @@ Yii Framework 2 jui extension Change Log
2.0.0 beta under development
----------------------------
-- Bug #1550: Ensure active id to options when using models (tonydspaniard)
+- Bug #1550: fixed the issue that JUI input widgets did not property input IDs.
2.0.0 alpha, December 1, 2013
-----------------------------
diff --git a/extensions/yii/jui/DatePicker.php b/extensions/yii/jui/DatePicker.php
index 06ca356..bab2abe 100644
--- a/extensions/yii/jui/DatePicker.php
+++ b/extensions/yii/jui/DatePicker.php
@@ -54,14 +54,30 @@ class DatePicker extends InputWidget
* @var boolean If true, shows the widget as an inline calendar and the input as a hidden field.
*/
public $inline = false;
+ /**
+ * @var array the HTML attributes for the container tag. This is only used when [[inline]] is true.
+ */
+ public $containerOptions = [];
/**
+ * @inheritdoc
+ */
+ public function init()
+ {
+ parent::init();
+ if ($this->inline && !isset($this->containerOptions['id'])) {
+ $this->containerOptions['id'] = $this->options['id'] . '-container';
+ }
+ }
+
+ /**
* Renders the widget.
*/
public function run()
{
echo $this->renderWidget() . "\n";
+ $containerID = $this->inline ? $this->containerOptions['id'] : $this->options['id'];
if ($this->language !== false) {
$view = $this->getView();
DatePickerRegionalAsset::register($view);
@@ -71,10 +87,10 @@ class DatePicker extends InputWidget
$options = $this->clientOptions;
$this->clientOptions = false; // the datepicker js widget is already registered
- $this->registerWidget('datepicker', DatePickerAsset::className());
+ $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID);
$this->clientOptions = $options;
} else {
- $this->registerWidget('datepicker', DatePickerAsset::className());
+ $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID);
}
}
@@ -101,8 +117,7 @@ class DatePicker extends InputWidget
$this->clientOptions['defaultDate'] = $this->value;
}
$this->clientOptions['altField'] = '#' . $this->options['id'];
- $this->options['id'] .= '-container';
- $contents[] = Html::tag('div', null, $this->options);
+ $contents[] = Html::tag('div', null, $this->containerOptions);
}
return implode("\n", $contents);
diff --git a/extensions/yii/jui/InputWidget.php b/extensions/yii/jui/InputWidget.php
index 68334c7..7aae366 100644
--- a/extensions/yii/jui/InputWidget.php
+++ b/extensions/yii/jui/InputWidget.php
@@ -45,7 +45,10 @@ class InputWidget extends Widget
public function init()
{
if (!$this->hasModel() && $this->name === null) {
- throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified.");
+ throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified.");
+ }
+ if ($this->hasModel() && !isset($this->options['id'])) {
+ $this->options['id'] = Html::getInputId($this->model, $this->attribute);
}
if($this->hasModel() && !array_key_exists('id', $this->options)) {
$this->options['id'] = Html::getInputId($this->model, $this->attribute);
diff --git a/extensions/yii/jui/SliderInput.php b/extensions/yii/jui/SliderInput.php
index 8ded4e8..8a43eb1 100644
--- a/extensions/yii/jui/SliderInput.php
+++ b/extensions/yii/jui/SliderInput.php
@@ -50,30 +50,42 @@ class SliderInput extends InputWidget
'start' => 'slidestart',
'stop' => 'slidestop',
];
+ /**
+ * @var array the HTML attributes for the container tag.
+ */
+ public $containerOptions = [];
+
+ /**
+ * @inheritdoc
+ */
+ public function init()
+ {
+ parent::init();
+ if (!isset($this->containerOptions['id'])) {
+ $this->containerOptions['id'] = $this->options['id'] . '-container';
+ }
+ }
/**
* Executes the widget.
*/
public function run()
{
- echo Html::tag('div', '', $this->options);
+ echo Html::tag('div', '', $this->containerOptions);
- $inputId = $this->id.'-input';
- $inputOptions = $this->options;
- $inputOptions['id'] = $inputId;
if ($this->hasModel()) {
- echo Html::activeHiddenInput($this->model, $this->attribute, $inputOptions);
+ echo Html::activeHiddenInput($this->model, $this->attribute, $this->options);
} else {
- echo Html::hiddenInput($this->name, $this->value, $inputOptions);
+ echo Html::hiddenInput($this->name, $this->value, $this->options);
}
if (!isset($this->clientEvents['slide'])) {
$this->clientEvents['slide'] = 'function(event, ui) {
- $("#'.$inputId.'").val(ui.value);
+ $("#' . $this->options['id'] . '").val(ui.value);
}';
}
- $this->registerWidget('slider', SliderAsset::className());
- $this->getView()->registerJs('$("#'.$inputId.'").val($("#'.$this->id.'").slider("value"));');
+ $this->registerWidget('slider', SliderAsset::className(), $this->containerOptions['id']);
+ $this->getView()->registerJs('$("#' . $this->options['id'] . '").val($("#' . $this->id . '").slider("value"));');
}
}
diff --git a/extensions/yii/jui/Widget.php b/extensions/yii/jui/Widget.php
index 90bad68..8881a77 100644
--- a/extensions/yii/jui/Widget.php
+++ b/extensions/yii/jui/Widget.php
@@ -76,11 +76,11 @@ class Widget extends \yii\base\Widget
/**
* Registers a specific jQuery UI widget options
* @param string $name the name of the jQuery UI widget
+ * @param string $id the ID of the widget
*/
- protected function registerClientOptions($name)
+ protected function registerClientOptions($name, $id)
{
if ($this->clientOptions !== false) {
- $id = $this->options['id'];
$options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions);
$js = "jQuery('#$id').$name($options);";
$this->getView()->registerJs($js);
@@ -90,11 +90,11 @@ class Widget extends \yii\base\Widget
/**
* Registers a specific jQuery UI widget events
* @param string $name the name of the jQuery UI widget
+ * @param string $id the ID of the widget
*/
- protected function registerClientEvents($name)
+ protected function registerClientEvents($name, $id)
{
if (!empty($this->clientEvents)) {
- $id = $this->options['id'];
$js = [];
foreach ($this->clientEvents as $event => $handler) {
if (isset($this->clientEventMap[$event])) {
@@ -112,11 +112,15 @@ class Widget extends \yii\base\Widget
* Registers a specific jQuery UI widget asset bundle, initializes it with client options and registers related events
* @param string $name the name of the jQuery UI widget
* @param string $assetBundle the asset bundle for the widget
+ * @param string $id the ID of the widget. If null, it will use the `id` value of [[options]].
*/
- protected function registerWidget($name, $assetBundle)
+ protected function registerWidget($name, $assetBundle, $id = null)
{
+ if ($id === null) {
+ $id = $this->options['id'];
+ }
$this->registerAssets($assetBundle);
- $this->registerClientOptions($name);
- $this->registerClientEvents($name);
+ $this->registerClientOptions($name, $id);
+ $this->registerClientEvents($name, $id);
}
}
diff --git a/extensions/yii/sphinx/ActiveRecord.php b/extensions/yii/sphinx/ActiveRecord.php
index e7bda34..0f9a48e 100644
--- a/extensions/yii/sphinx/ActiveRecord.php
+++ b/extensions/yii/sphinx/ActiveRecord.php
@@ -29,7 +29,7 @@ use yii\helpers\StringHelper;
* @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is
* returned if the primary key is composite. A string is returned otherwise (null will be returned if the key
* value is null). This property is read-only.
- * @property array $populatedRelations An array of relation data indexed by relation names. This property is
+ * @property array $relatedRecords An array of the populated related records indexed by relation names. This property is
* read-only.
* @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if
* the primary key is composite. A string is returned otherwise (null will be returned if the key value is null).
@@ -668,4 +668,4 @@ abstract class ActiveRecord extends BaseActiveRecord
$transactions = $this->transactions();
return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
}
-}
\ No newline at end of file
+}
diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md
index 073a3ce..b6c56c6 100644
--- a/framework/CHANGELOG.md
+++ b/framework/CHANGELOG.md
@@ -9,21 +9,35 @@ Yii Framework 2 Change Log
- Bug #1500: Log messages exported to files are not separated by newlines (omnilight, qiangxue)
- Bug #1509: The SQL for creating Postgres RBAC tables is incorrect (qiangxue)
- Bug #1545: It was not possible to execute db Query twice, params where missing (cebe)
+- Bug #1550: fixed the issue that JUI input widgets did not property input IDs.
+- Bug #1582: Error messages shown via client-side validation should not be double encoded (qiangxue)
+- Bug #1591: StringValidator is accessing undefined property (qiangxue)
+- Bug #1597: Added `enableAutoLogin` to basic and advanced application templates so "remember me" now works properly (samdark)
- Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark)
- Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark)
- Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe)
- Bug: Fixed issue with tabular input on ActiveField::radio() and ActiveField::checkbox() (jom)
+- Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue)
- Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe)
- Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue)
- Enh #1437: Added ListView::viewParams (qiangxue)
- Enh #1469: ActiveRecord::find() now works with default conditions (default scope) applied by createQuery (cebe)
+- Enh #1499: Added `ActionColumn::controller` property to support customizing the controller for handling GridView actions (qiangxue)
- Enh #1523: Query conditions now allow to use the NOT operator (cebe)
- Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code)
+- Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark)
+- Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue)
+- Enh #1581: Added `ActiveQuery::joinWith()` and `ActiveQuery::innerJoinWith()` to support joining with relations (qiangxue)
+- Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight)
- Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
- Enh: Support for file aliases in console command 'message' (omnilight)
-- Enh: Sort and Paginiation can now create absolute URLs (cebe)
-- Chg: Renamed yii\jui\Widget::clientEventsMap to clientEventMap (qiangxue)
+- Enh: Sort and Pagination can now create absolute URLs (cebe)
+- Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue)
+- Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue)
+- Chg: Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue)
+- Chg: Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator` (qiangxue)
+- Chg: Added `yii\widgets\InputWidget::options` (qiangxue)
- New #1438: [MongoDB integration](https://github.com/yiisoft/yii2-mongodb) ActiveRecord and Query (klimov-paul)
- New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo)
diff --git a/framework/yii/assets/yii.activeForm.js b/framework/yii/assets/yii.activeForm.js
index c1d5bf5..e898efc 100644
--- a/framework/yii/assets/yii.activeForm.js
+++ b/framework/yii/assets/yii.activeForm.js
@@ -348,7 +348,7 @@
$container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass)
.addClass(data.settings.errorCssClass);
} else {
- $error.html('');
+ $error.text('');
$container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ')
.addClass(data.settings.successCssClass);
}
@@ -365,15 +365,15 @@
var updateSummary = function ($form, messages) {
var data = $form.data('yiiActiveForm'),
$summary = $form.find(data.settings.errorSummary),
- content = '';
+ $ul = $summary.find('ul');
if ($summary.length && messages) {
$.each(data.attributes, function () {
if ($.isArray(messages[this.name]) && messages[this.name].length) {
- content += '
' + messages[this.name][0] + '';
+ $ul.append($('
').text(messages[this.name][0]));
}
});
- $summary.toggle(content !== '').find('ul').html(content);
+ $summary.toggle($ul.find('li').length > 0);
}
};
diff --git a/framework/yii/captcha/Captcha.php b/framework/yii/captcha/Captcha.php
index 76090a2..18b8765 100644
--- a/framework/yii/captcha/Captcha.php
+++ b/framework/yii/captcha/Captcha.php
@@ -39,10 +39,6 @@ class Captcha extends InputWidget
*/
public $captchaAction = 'site/captcha';
/**
- * @var array HTML attributes to be applied to the text input field.
- */
- public $options = [];
- /**
* @var array HTML attributes to be applied to the CAPTCHA image tag.
*/
public $imageOptions = [];
@@ -62,9 +58,6 @@ class Captcha extends InputWidget
$this->checkRequirements();
- if (!isset($this->options['id'])) {
- $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId();
- }
if (!isset($this->imageOptions['id'])) {
$this->imageOptions['id'] = $this->options['id'] . '-image';
}
diff --git a/framework/yii/captcha/CaptchaValidator.php b/framework/yii/captcha/CaptchaValidator.php
index 83996d5..57665ec 100644
--- a/framework/yii/captcha/CaptchaValidator.php
+++ b/framework/yii/captcha/CaptchaValidator.php
@@ -93,9 +93,9 @@ class CaptchaValidator extends Validator
'hash' => $hash,
'hashKey' => 'yiiCaptcha/' . $this->captchaAction,
'caseSensitive' => $this->caseSensitive,
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute),
- ])),
+ ]),
];
if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1;
diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php
index e93e9be..26b0c6e 100644
--- a/framework/yii/db/ActiveQuery.php
+++ b/framework/yii/db/ActiveQuery.php
@@ -68,6 +68,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$rows = $command->queryAll();
if (!empty($rows)) {
$models = $this->createModels($rows);
+ if (!empty($this->join) && $this->indexBy === null) {
+ $models = $this->removeDuplicatedModels($models);
+ }
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
@@ -78,6 +81,47 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
/**
+ * Removes duplicated models by checking their primary key values.
+ * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
+ * @param array $models the models to be checked
+ * @return array the distinctive models
+ */
+ private function removeDuplicatedModels($models)
+ {
+ $hash = [];
+ /** @var ActiveRecord $class */
+ $class = $this->modelClass;
+ $pks = $class::primaryKey();
+
+ if (count($pks) > 1) {
+ foreach ($models as $i => $model) {
+ $key = [];
+ foreach ($pks as $pk) {
+ $key[] = $model[$pk];
+ }
+ $key = serialize($key);
+ if (isset($hash[$key])) {
+ unset($models[$i]);
+ } else {
+ $hash[$key] = true;
+ }
+ }
+ } else {
+ $pk = reset($pks);
+ foreach ($models as $i => $model) {
+ $key = $model[$pk];
+ if (isset($hash[$key])) {
+ unset($models[$i]);
+ } else {
+ $hash[$key] = true;
+ }
+ }
+ }
+
+ return array_values($models);
+ }
+
+ /**
* Executes query and returns a single row of result.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
@@ -143,4 +187,223 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
return $db->createCommand($sql, $params);
}
+
+ /**
+ * Joins with the specified relations.
+ *
+ * This method allows you to reuse existing relation definitions to perform JOIN queries.
+ * Based on the definition of the specified relation(s), the method will append one or multiple
+ * JOIN statements to the current query.
+ *
+ * If the `$eagerLoading` parameter is true, the method will also eager loading the specified relations,
+ * which is equivalent to calling [[with()]] using the specified relations.
+ *
+ * Note that because a JOIN query will be performed, you are responsible to disambiguate column names.
+ *
+ * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement
+ * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations.
+ *
+ * @param array $with the relations to be joined. Each array element represents a single relation.
+ * The array keys are relation names, and the array values are the corresponding anonymous functions that
+ * can be used to modify the relation queries on-the-fly. If a relation query does not need modification,
+ * you may use the relation name as the array value. Sub-relations can also be specified (see [[with()]]).
+ * For example,
+ *
+ * ```php
+ * // find all orders that contain books, and eager loading "books"
+ * Order::find()->joinWith('books', true, 'INNER JOIN')->all();
+ * // find all orders, eager loading "books", and sort the orders and books by the book names.
+ * Order::find()->joinWith([
+ * 'books' => function ($query) {
+ * $query->orderBy('tbl_item.name');
+ * }
+ * ])->all();
+ * ```
+ *
+ * @param boolean|array $eagerLoading whether to eager load the relations specified in `$with`.
+ * When this is a boolean, it applies to all relations specified in `$with`. Use an array
+ * to explicitly list which relations in `$with` need to be eagerly loaded.
+ * @param string|array $joinType the join type of the relations specified in `$with`.
+ * When this is a string, it applies to all relations specified in `$with`. Use an array
+ * in the format of `relationName => joinType` to specify different join types for different relations.
+ * @return static the query object itself
+ */
+ public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')
+ {
+ $with = (array)$with;
+ $this->joinWithRelations(new $this->modelClass, $with, $joinType);
+
+ if (is_array($eagerLoading)) {
+ foreach ($with as $name => $callback) {
+ if (is_integer($name)) {
+ if (!in_array($callback, $eagerLoading, true)) {
+ unset($with[$name]);
+ }
+ } elseif (!in_array($name, $eagerLoading, true)) {
+ unset($with[$name]);
+ }
+ }
+ $this->with($with);
+ } elseif ($eagerLoading) {
+ $this->with($with);
+ }
+ return $this;
+ }
+
+ /**
+ * Inner joins with the specified relations.
+ * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN".
+ * Please refer to [[joinWith()]] for detailed usage of this method.
+ * @param array $with the relations to be joined with
+ * @param boolean|array $eagerLoading whether to eager loading the relations
+ * @return static the query object itself
+ * @see joinWith()
+ */
+ public function innerJoinWith($with, $eagerLoading = true)
+ {
+ return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
+ }
+
+ /**
+ * Modifies the current query by adding join fragments based on the given relations.
+ * @param ActiveRecord $model the primary model
+ * @param array $with the relations to be joined
+ * @param string|array $joinType the join type
+ */
+ private function joinWithRelations($model, $with, $joinType)
+ {
+ $relations = [];
+
+ foreach ($with as $name => $callback) {
+ if (is_integer($name)) {
+ $name = $callback;
+ $callback = null;
+ }
+
+ $primaryModel = $model;
+ $parent = $this;
+ $prefix = '';
+ while (($pos = strpos($name, '.')) !== false) {
+ $childName = substr($name, $pos + 1);
+ $name = substr($name, 0, $pos);
+ $fullName = $prefix === '' ? $name : "$prefix.$name";
+ if (!isset($relations[$fullName])) {
+ $relations[$fullName] = $relation = $primaryModel->getRelation($name);
+ $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
+ } else {
+ $relation = $relations[$fullName];
+ }
+ $primaryModel = new $relation->modelClass;
+ $parent = $relation;
+ $prefix = $fullName;
+ $name = $childName;
+ }
+
+ $fullName = $prefix === '' ? $name : "$prefix.$name";
+ if (!isset($relations[$fullName])) {
+ $relations[$fullName] = $relation = $primaryModel->getRelation($name);
+ if ($callback !== null) {
+ call_user_func($callback, $relation);
+ }
+ $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
+ }
+ }
+ }
+
+ /**
+ * Returns the join type based on the given join type parameter and the relation name.
+ * @param string|array $joinType the given join type(s)
+ * @param string $name relation name
+ * @return string the real join type
+ */
+ private function getJoinType($joinType, $name)
+ {
+ if (is_array($joinType) && isset($joinType[$name])) {
+ return $joinType[$name];
+ } else {
+ return is_string($joinType) ? $joinType : 'INNER JOIN';
+ }
+ }
+
+ /**
+ * Returns the table name used by the specified active query.
+ * @param ActiveQuery $query
+ * @return string the table name
+ */
+ private function getQueryTableName($query)
+ {
+ if (empty($query->from)) {
+ /** @var ActiveRecord $modelClass */
+ $modelClass = $query->modelClass;
+ return $modelClass::tableName();
+ } else {
+ return reset($query->from);
+ }
+ }
+
+ /**
+ * Joins a parent query with a child query.
+ * The current query object will be modified accordingly.
+ * @param ActiveQuery $parent
+ * @param ActiveRelation $child
+ * @param string $joinType
+ */
+ private function joinWithRelation($parent, $child, $joinType)
+ {
+ $via = $child->via;
+ $child->via = null;
+ if ($via instanceof ActiveRelation) {
+ // via table
+ $this->joinWithRelation($parent, $via, $joinType);
+ $this->joinWithRelation($via, $child, $joinType);
+ return;
+ } elseif (is_array($via)) {
+ // via relation
+ $this->joinWithRelation($parent, $via[1], $joinType);
+ $this->joinWithRelation($via[1], $child, $joinType);
+ return;
+ }
+
+ $parentTable = $this->getQueryTableName($parent);
+ $childTable = $this->getQueryTableName($child);
+
+
+ if (!empty($child->link)) {
+ $on = [];
+ foreach ($child->link as $childColumn => $parentColumn) {
+ $on[] = '{{' . $parentTable . "}}.[[$parentColumn]] = {{" . $childTable . "}}.[[$childColumn]]";
+ }
+ $on = implode(' AND ', $on);
+ } else {
+ $on = '';
+ }
+ $this->join($joinType, $childTable, $on);
+
+
+ if (!empty($child->where)) {
+ $this->andWhere($child->where);
+ }
+ if (!empty($child->having)) {
+ $this->andHaving($child->having);
+ }
+ if (!empty($child->orderBy)) {
+ $this->addOrderBy($child->orderBy);
+ }
+ if (!empty($child->groupBy)) {
+ $this->addGroupBy($child->groupBy);
+ }
+ if (!empty($child->params)) {
+ $this->addParams($child->params);
+ }
+ if (!empty($child->join)) {
+ foreach ($child->join as $join) {
+ $this->join[] = $join;
+ }
+ }
+ if (!empty($child->union)) {
+ foreach ($child->union as $union) {
+ $this->union[] = $union;
+ }
+ }
+ }
}
diff --git a/framework/yii/db/ActiveRecordInterface.php b/framework/yii/db/ActiveRecordInterface.php
index 556384b..73db852 100644
--- a/framework/yii/db/ActiveRecordInterface.php
+++ b/framework/yii/db/ActiveRecordInterface.php
@@ -70,6 +70,23 @@ interface ActiveRecordInterface
public function getPrimaryKey($asArray = false);
/**
+ * Returns the old primary key value(s).
+ * This refers to the primary key value that is populated into the record
+ * after executing a find method (e.g. find(), findAll()).
+ * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
+ * @param boolean $asArray whether to return the primary key value as an array. If true,
+ * the return value will be an array with column name as key and column value as value.
+ * If this is false (default), a scalar value will be returned for non-composite primary key.
+ * @property mixed The old primary key value. An array (column name => column value) is
+ * returned if the primary key is composite. A string is returned otherwise (null will be
+ * returned if the key value is null).
+ * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
+ * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
+ * the key value is null).
+ */
+ public function getOldPrimaryKey($asArray = false);
+
+ /**
* Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose.
*
* This method is usually ment to be used like this:
@@ -290,4 +307,4 @@ interface ActiveRecordInterface
* If true, the model containing the foreign key will be deleted.
*/
public function unlink($name, $model, $delete = false);
-}
\ No newline at end of file
+}
diff --git a/framework/yii/db/ActiveRelationTrait.php b/framework/yii/db/ActiveRelationTrait.php
index c885006..dac3028 100644
--- a/framework/yii/db/ActiveRelationTrait.php
+++ b/framework/yii/db/ActiveRelationTrait.php
@@ -189,26 +189,6 @@ trait ActiveRelationTrait
}
/**
- * @param ActiveRecord|array $model
- * @param array $attributes
- * @return string
- */
- private function getModelKey($model, $attributes)
- {
- if (count($attributes) > 1) {
- $key = [];
- foreach ($attributes as $attribute) {
- $key[] = $model[$attribute];
- }
- return serialize($key);
- } else {
- $attribute = reset($attributes);
- $key = $model[$attribute];
- return is_scalar($key) ? $key : serialize($key);
- }
- }
-
- /**
* @param array $models
*/
private function filterByModels($models)
@@ -237,6 +217,26 @@ trait ActiveRelationTrait
}
/**
+ * @param ActiveRecord|array $model
+ * @param array $attributes
+ * @return string
+ */
+ private function getModelKey($model, $attributes)
+ {
+ if (count($attributes) > 1) {
+ $key = [];
+ foreach ($attributes as $attribute) {
+ $key[] = $model[$attribute];
+ }
+ return serialize($key);
+ } else {
+ $attribute = reset($attributes);
+ $key = $model[$attribute];
+ return is_scalar($key) ? $key : serialize($key);
+ }
+ }
+
+ /**
* @param array $primaryModels either array of AR instances or arrays
* @return array
*/
diff --git a/framework/yii/db/BaseActiveRecord.php b/framework/yii/db/BaseActiveRecord.php
index dae7134..e20501b 100644
--- a/framework/yii/db/BaseActiveRecord.php
+++ b/framework/yii/db/BaseActiveRecord.php
@@ -30,7 +30,7 @@ use yii\helpers\Inflector;
* @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is
* returned if the primary key is composite. A string is returned otherwise (null will be returned if the key
* value is null). This property is read-only.
- * @property array $populatedRelations An array of relation data indexed by relation names. This property is
+ * @property array $relatedRecords An array of the populated related records indexed by relation names. This property is
* read-only.
* @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if
* the primary key is composite. A string is returned otherwise (null will be returned if the key value is null).
@@ -232,6 +232,13 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
$value = parent::__get($name);
if ($value instanceof ActiveRelationInterface) {
+ if (method_exists($this, 'get' . $name)) {
+ $method = new \ReflectionMethod($this, 'get' . $name);
+ $realName = lcfirst(substr($method->getName(), 3));
+ if ($realName !== $name) {
+ throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
+ }
+ }
return $this->_related[$name] = $value->multiple ? $value->all() : $value->one();
} else {
return $value;
@@ -390,10 +397,10 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
/**
- * Returns all populated relations.
- * @return array an array of relation data indexed by relation names.
+ * Returns all populated related records.
+ * @return array an array of related records indexed by relation names.
*/
- public function getPopulatedRelations()
+ public function getRelatedRecords()
{
return $this->_related;
}
@@ -999,15 +1006,25 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
{
$getter = 'get' . $name;
try {
+ // the relation could be defined in a behavior
$relation = $this->$getter();
- if ($relation instanceof ActiveRelationInterface) {
- return $relation;
- } else {
- throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
- }
} catch (UnknownMethodException $e) {
throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
}
+ if (!$relation instanceof ActiveRelationInterface) {
+ throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
+ }
+
+ if (method_exists($this, $getter)) {
+ // relation name is case sensitive, trying to validate it when the relation is defined within this class
+ $method = new \ReflectionMethod($this, $getter);
+ $realName = lcfirst(substr($method->getName(), 3));
+ if ($realName !== $name) {
+ throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
+ }
+ }
+
+ return $relation;
}
/**
@@ -1217,11 +1234,10 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
public static function isPrimaryKey($keys)
{
$pks = static::primaryKey();
- foreach ($keys as $key) {
- if (!in_array($key, $pks, true)) {
- return false;
- }
+ if (count($keys) === count($pks)) {
+ return count(array_intersect($keys, $pks)) === count($pks);
+ } else {
+ return false;
}
- return count($keys) === count($pks);
}
}
diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php
index ee24c2f..2baa78c 100644
--- a/framework/yii/db/Query.php
+++ b/framework/yii/db/Query.php
@@ -148,7 +148,7 @@ class Query extends Component implements QueryInterface
* Executes the query and returns a single row of result.
* @param Connection $db the database connection used to generate the SQL statement.
* If this parameter is not given, the `db` application component will be used.
- * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query
+ * @return array|boolean the first row (in terms of an array) of the query result. Null is returned if the query
* results in nothing.
*/
public function one($db = null)
diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php
index 338a74b..77b9532 100644
--- a/framework/yii/db/mssql/QueryBuilder.php
+++ b/framework/yii/db/mssql/QueryBuilder.php
@@ -60,6 +60,47 @@ class QueryBuilder extends \yii\db\QueryBuilder
// }
/**
+ * Builds a SQL statement for renaming a DB table.
+ * @param string $table the table to be renamed. The name will be properly quoted by the method.
+ * @param string $newName the new table name. The name will be properly quoted by the method.
+ * @return string the SQL statement for renaming a DB table.
+ */
+ public function renameTable($table, $newName)
+ {
+ return "sp_rename '$table', '$newName'";
+ }
+
+ /**
+ * Builds a SQL statement for renaming a column.
+ * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
+ * @param string $name the old name of the column. The name will be properly quoted by the method.
+ * @param string $newName the new name of the column. The name will be properly quoted by the method.
+ * @return string the SQL statement for renaming a DB column.
+ */
+ public function renameColumn($table, $name, $newName)
+ {
+ return "sp_rename '$table.$name', '$newName', 'COLUMN'";
+ }
+
+ /**
+ * Builds a SQL statement for changing the definition of a column.
+ * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
+ * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
+ * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any)
+ * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
+ * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
+ * @return string the SQL statement for changing the definition of a column.
+ */
+ public function alterColumn($table, $column, $type)
+ {
+ $type=$this->getColumnType($type);
+ $sql='ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
+ . $this->db->quoteColumnName($column) . ' '
+ . $this->getColumnType($type);
+ return $sql;
+ }
+
+ /**
* Builds a SQL statement for enabling or disabling integrity check.
* @param boolean $check whether to turn on or off the integrity check.
* @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php
index f6c7298..eb7de37 100644
--- a/framework/yii/db/pgsql/Schema.php
+++ b/framework/yii/db/pgsql/Schema.php
@@ -299,7 +299,7 @@ SQL;
$table->columns[$column->name] = $column;
if ($column->isPrimaryKey === true) {
$table->primaryKey[] = $column->name;
- if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) {
+ if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) {
$table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue);
}
}
diff --git a/framework/yii/grid/ActionColumn.php b/framework/yii/grid/ActionColumn.php
index 707d411..b53b606 100644
--- a/framework/yii/grid/ActionColumn.php
+++ b/framework/yii/grid/ActionColumn.php
@@ -19,6 +19,13 @@ use yii\helpers\Html;
*/
class ActionColumn extends Column
{
+ /**
+ * @var string the ID of the controller that should handle the actions specified here.
+ * If not set, it will use the currently active controller. This property is mainly used by
+ * [[urlCreator]] to create URLs for different actions. The value of this property will be prefixed
+ * to each action name to form the route of the action.
+ */
+ public $controller;
public $template = '{view} {update} {delete}';
public $buttons = [];
public $urlCreator;
@@ -75,7 +82,8 @@ class ActionColumn extends Column
return call_user_func($this->urlCreator, $model, $key, $index, $action);
} else {
$params = is_array($key) ? $key : ['id' => $key];
- return Yii::$app->controller->createUrl($action, $params);
+ $route = $this->controller ? $this->controller . '/' . $action : $action;
+ return Yii::$app->controller->createUrl($route, $params);
}
}
diff --git a/framework/yii/helpers/BaseHtml.php b/framework/yii/helpers/BaseHtml.php
index 2cfcb15..49fe832 100644
--- a/framework/yii/helpers/BaseHtml.php
+++ b/framework/yii/helpers/BaseHtml.php
@@ -1281,7 +1281,8 @@ class BaseHtml
* @param array $options options (name => config) for the checkbox list. The following options are specially handled:
*
* - unselect: string, the value that should be submitted when none of the checkboxes is selected.
- * By setting this option, a hidden input will be generated.
+ * You may set this option to be null to prevent default value submission.
+ * If this option is not set, an empty string will be submitted.
* - separator: string, the HTML code that separates items.
* - item: callable, a callback that can be used to customize the generation of the HTML code
* corresponding to a single item in $items. The signature of this callback must be:
@@ -1300,7 +1301,7 @@ class BaseHtml
$name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute);
$selection = static::getAttributeValue($model, $attribute);
if (!array_key_exists('unselect', $options)) {
- $options['unselect'] = '0';
+ $options['unselect'] = '';
}
if (!array_key_exists('id', $options)) {
$options['id'] = static::getInputId($model, $attribute);
@@ -1321,7 +1322,8 @@ class BaseHtml
* @param array $options options (name => config) for the radio button list. The following options are specially handled:
*
* - unselect: string, the value that should be submitted when none of the radio buttons is selected.
- * By setting this option, a hidden input will be generated.
+ * You may set this option to be null to prevent default value submission.
+ * If this option is not set, an empty string will be submitted.
* - separator: string, the HTML code that separates items.
* - item: callable, a callback that can be used to customize the generation of the HTML code
* corresponding to a single item in $items. The signature of this callback must be:
@@ -1340,7 +1342,7 @@ class BaseHtml
$name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute);
$selection = static::getAttributeValue($model, $attribute);
if (!array_key_exists('unselect', $options)) {
- $options['unselect'] = '0';
+ $options['unselect'] = '';
}
if (!array_key_exists('id', $options)) {
$options['id'] = static::getInputId($model, $attribute);
diff --git a/framework/yii/validators/BooleanValidator.php b/framework/yii/validators/BooleanValidator.php
index 961ed14..8bca827 100644
--- a/framework/yii/validators/BooleanValidator.php
+++ b/framework/yii/validators/BooleanValidator.php
@@ -72,11 +72,11 @@ class BooleanValidator extends Validator
$options = [
'trueValue' => $this->trueValue,
'falseValue' => $this->falseValue,
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute),
'{true}' => $this->trueValue,
'{false}' => $this->falseValue,
- ])),
+ ]),
];
if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1;
diff --git a/framework/yii/validators/CompareValidator.php b/framework/yii/validators/CompareValidator.php
index 69bd6d5..cbd12d2 100644
--- a/framework/yii/validators/CompareValidator.php
+++ b/framework/yii/validators/CompareValidator.php
@@ -195,11 +195,11 @@ class CompareValidator extends Validator
$options['skipOnEmpty'] = 1;
}
- $options['message'] = Html::encode(strtr($this->message, [
+ $options['message'] = strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute),
'{compareAttribute}' => $compareValue,
'{compareValue}' => $compareValue,
- ]));
+ ]);
ValidationAsset::register($view);
return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');';
diff --git a/framework/yii/validators/EmailValidator.php b/framework/yii/validators/EmailValidator.php
index 24eeaec..e5d9b75 100644
--- a/framework/yii/validators/EmailValidator.php
+++ b/framework/yii/validators/EmailValidator.php
@@ -98,9 +98,9 @@ class EmailValidator extends Validator
'pattern' => new JsExpression($this->pattern),
'fullPattern' => new JsExpression($this->fullPattern),
'allowName' => $this->allowName,
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute),
- ])),
+ ]),
'enableIDN' => (boolean)$this->enableIDN,
];
if ($this->skipOnEmpty) {
diff --git a/framework/yii/validators/ExistValidator.php b/framework/yii/validators/ExistValidator.php
index 585b82f..7e783a8 100644
--- a/framework/yii/validators/ExistValidator.php
+++ b/framework/yii/validators/ExistValidator.php
@@ -13,29 +13,47 @@ use yii\base\InvalidConfigException;
/**
* ExistValidator validates that the attribute value exists in a table.
*
+ * ExistValidator checks if the value being validated can be found in the table column specified by
+ * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]].
+ *
* This validator is often used to verify that a foreign key contains a value
* that can be found in the foreign table.
*
+ * The followings are examples of validation rules using this validator:
+ *
+ * ```php
+ * // a1 needs to exist
+ * ['a1', 'exist']
+ * // a1 needs to exist, but its value will use a2 to check for the existence
+ * ['a1', 'exist', 'targetAttribute' => 'a2']
+ * // a1 and a2 need to exist together, and they both will receive error message
+ * ['a1, a2', 'exist', 'targetAttribute' => ['a1', 'a2']]
+ * // a1 and a2 need to exist together, only a1 will receive error message
+ * ['a1', 'exist', 'targetAttribute' => ['a1', 'a2']]
+ * // a1 needs to exist by checking the existence of both a2 and a3 (using a1 value)
+ * ['a1', 'exist', 'targetAttribute' => ['a2', 'a1' => 'a3']]
+ * ```
+ *
* @author Qiang Xue
* @since 2.0
*/
class ExistValidator extends Validator
{
/**
- * @var string the ActiveRecord class name or alias of the class
- * that should be used to look for the attribute value being validated.
- * Defaults to null, meaning using the ActiveRecord class of
- * the attribute being validated.
- * @see attributeName
+ * @var string the name of the ActiveRecord class that should be used to validate the existence
+ * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated.
+ * @see targetAttribute
*/
- public $className;
+ public $targetClass;
/**
- * @var string the yii\db\ActiveRecord class attribute name that should be
- * used to look for the attribute value being validated. Defaults to null,
- * meaning using the name of the attribute being validated.
- * @see className
+ * @var string|array the name of the ActiveRecord attribute that should be used to
+ * validate the existence of the current attribute value. If not set, it will use the name
+ * of the attribute currently being validated. You may use an array to validate the existence
+ * of multiple columns at the same time. The array values are the attributes that will be
+ * used to validate the existence, while the array keys are the attributes whose values are to be validated.
+ * If the key and the value are the same, you can just specify the value.
*/
- public $attributeName;
+ public $targetAttribute;
/**
@@ -54,19 +72,28 @@ class ExistValidator extends Validator
*/
public function validateAttribute($object, $attribute)
{
- $value = $object->$attribute;
+ /** @var \yii\db\ActiveRecordInterface $targetClass */
+ $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass;
+ $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
- if (is_array($value)) {
- $this->addError($object, $attribute, $this->message);
- return;
+ if (is_array($targetAttribute)) {
+ $params = [];
+ foreach ($targetAttribute as $k => $v) {
+ $params[$v] = is_integer($k) ? $object->$v : $object->$k;
+ }
+ } else {
+ $params = [$targetAttribute => $object->$attribute];
+ }
+
+ foreach ($params as $value) {
+ if (is_array($value)) {
+ $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.'));
+ return;
+ }
}
- /** @var \yii\db\ActiveRecord $className */
- $className = $this->className === null ? get_class($object) : $this->className;
- $attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
- $query = $className::find();
- $query->where([$attributeName => $value]);
- if (!$query->exists()) {
+ /** @var \yii\db\ActiveRecordInterface $className */
+ if (!$targetClass::find()->where($params)->exists()) {
$this->addError($object, $attribute, $this->message);
}
}
@@ -79,16 +106,17 @@ class ExistValidator extends Validator
if (is_array($value)) {
return [$this->message, []];
}
- if ($this->className === null) {
+ if ($this->targetClass === null) {
throw new InvalidConfigException('The "className" property must be set.');
}
- if ($this->attributeName === null) {
- throw new InvalidConfigException('The "attributeName" property must be set.');
+ if (!is_string($this->targetAttribute)) {
+ throw new InvalidConfigException('The "attributeName" property must be configured as a string.');
}
- /** @var \yii\db\ActiveRecord $className */
- $className = $this->className;
- $query = $className::find();
- $query->where([$this->attributeName => $value]);
+
+ /** @var \yii\db\ActiveRecordInterface $targetClass */
+ $targetClass = $this->targetClass;
+ $query = $targetClass::find();
+ $query->where([$this->targetAttribute => $value]);
return $query->exists() ? null : [$this->message, []];
}
}
diff --git a/framework/yii/validators/NumberValidator.php b/framework/yii/validators/NumberValidator.php
index 60e920a..1bb2360 100644
--- a/framework/yii/validators/NumberValidator.php
+++ b/framework/yii/validators/NumberValidator.php
@@ -124,24 +124,24 @@ class NumberValidator extends Validator
$options = [
'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern),
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $label,
- ])),
+ ]),
];
if ($this->min !== null) {
$options['min'] = $this->min;
- $options['tooSmall'] = Html::encode(strtr($this->tooSmall, [
+ $options['tooSmall'] = strtr($this->tooSmall, [
'{attribute}' => $label,
'{min}' => $this->min,
- ]));
+ ]);
}
if ($this->max !== null) {
$options['max'] = $this->max;
- $options['tooBig'] = Html::encode(strtr($this->tooBig, [
+ $options['tooBig'] = strtr($this->tooBig, [
'{attribute}' => $label,
'{max}' => $this->max,
- ]));
+ ]);
}
if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1;
diff --git a/framework/yii/validators/RangeValidator.php b/framework/yii/validators/RangeValidator.php
index cfd1f51..a4da139 100644
--- a/framework/yii/validators/RangeValidator.php
+++ b/framework/yii/validators/RangeValidator.php
@@ -73,9 +73,9 @@ class RangeValidator extends Validator
$options = [
'range' => $range,
'not' => $this->not,
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute),
- ])),
+ ]),
];
if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1;
diff --git a/framework/yii/validators/RegularExpressionValidator.php b/framework/yii/validators/RegularExpressionValidator.php
index 7b02381..28e9bdc 100644
--- a/framework/yii/validators/RegularExpressionValidator.php
+++ b/framework/yii/validators/RegularExpressionValidator.php
@@ -80,9 +80,9 @@ class RegularExpressionValidator extends Validator
$options = [
'pattern' => new JsExpression($pattern),
'not' => $this->not,
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute),
- ])),
+ ]),
];
if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1;
diff --git a/framework/yii/validators/RequiredValidator.php b/framework/yii/validators/RequiredValidator.php
index 43b40cf..f291f39 100644
--- a/framework/yii/validators/RequiredValidator.php
+++ b/framework/yii/validators/RequiredValidator.php
@@ -101,9 +101,9 @@ class RequiredValidator extends Validator
$options['strict'] = 1;
}
- $options['message'] = Html::encode(strtr($options['message'], [
+ $options['message'] = strtr($options['message'], [
'{attribute}' => $object->getAttributeLabel($attribute),
- ]));
+ ]);
ValidationAsset::register($view);
return 'yii.validation.required(value, messages, ' . json_encode($options) . ');';
diff --git a/framework/yii/validators/StringValidator.php b/framework/yii/validators/StringValidator.php
index a93fb72..279a189 100644
--- a/framework/yii/validators/StringValidator.php
+++ b/framework/yii/validators/StringValidator.php
@@ -151,31 +151,31 @@ class StringValidator extends Validator
$label = $object->getAttributeLabel($attribute);
$options = [
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $label,
- ])),
+ ]),
];
if ($this->min !== null) {
$options['min'] = $this->min;
- $options['tooShort'] = Html::encode(strtr($this->tooShort, [
+ $options['tooShort'] = strtr($this->tooShort, [
'{attribute}' => $label,
'{min}' => $this->min,
- ]));
+ ]);
}
if ($this->max !== null) {
$options['max'] = $this->max;
- $options['tooLong'] = Html::encode(strtr($this->tooLong, [
+ $options['tooLong'] = strtr($this->tooLong, [
'{attribute}' => $label,
'{max}' => $this->max,
- ]));
+ ]);
}
if ($this->length !== null) {
$options['is'] = $this->length;
- $options['notEqual'] = Html::encode(strtr($this->notEqual, [
+ $options['notEqual'] = strtr($this->notEqual, [
'{attribute}' => $label,
- '{length}' => $this->is,
- ]));
+ '{length}' => $this->length,
+ ]);
}
if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1;
diff --git a/framework/yii/validators/UniqueValidator.php b/framework/yii/validators/UniqueValidator.php
index d9cd587..1136f02 100644
--- a/framework/yii/validators/UniqueValidator.php
+++ b/framework/yii/validators/UniqueValidator.php
@@ -8,12 +8,28 @@
namespace yii\validators;
use Yii;
-use yii\base\InvalidConfigException;
-use yii\db\ActiveRecord;
use yii\db\ActiveRecordInterface;
/**
- * UniqueValidator validates that the attribute value is unique in the corresponding database table.
+ * UniqueValidator validates that the attribute value is unique in the specified database table.
+ *
+ * UniqueValidator checks if the value being validated is unique in the table column specified by
+ * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]].
+ *
+ * The followings are examples of validation rules using this validator:
+ *
+ * ```php
+ * // a1 needs to be unique
+ * ['a1', 'unique']
+ * // a1 needs to be unique, but column a2 will be used to check the uniqueness of the a1 value
+ * ['a1', 'unique', 'targetAttribute' => 'a2']
+ * // a1 and a2 need to unique together, and they both will receive error message
+ * ['a1, a2', 'unique', 'targetAttribute' => ['a1', 'a2']]
+ * // a1 and a2 need to unique together, only a1 will receive error message
+ * ['a1', 'unique', 'targetAttribute' => ['a1', 'a2']]
+ * // a1 needs to be unique by checking the uniqueness of both a2 and a3 (using a1 value)
+ * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']]
+ * ```
*
* @author Qiang Xue
* @since 2.0
@@ -21,18 +37,20 @@ use yii\db\ActiveRecordInterface;
class UniqueValidator extends Validator
{
/**
- * @var string the ActiveRecord class name or alias of the class
- * that should be used to look for the attribute value being validated.
- * Defaults to null, meaning using the ActiveRecord class of the attribute being validated.
- * @see attributeName
+ * @var string the name of the ActiveRecord class that should be used to validate the uniqueness
+ * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated.
+ * @see targetAttribute
*/
- public $className;
+ public $targetClass;
/**
- * @var string the ActiveRecord class attribute name that should be
- * used to look for the attribute value being validated. Defaults to null,
- * meaning using the name of the attribute being validated.
+ * @var string|array the name of the ActiveRecord attribute that should be used to
+ * validate the uniqueness of the current attribute value. If not set, it will use the name
+ * of the attribute currently being validated. You may use an array to validate the uniqueness
+ * of multiple columns at the same time. The array values are the attributes that will be
+ * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated.
+ * If the key and the value are the same, you can just specify the value.
*/
- public $attributeName;
+ public $targetAttribute;
/**
* @inheritdoc
@@ -50,36 +68,48 @@ class UniqueValidator extends Validator
*/
public function validateAttribute($object, $attribute)
{
- $value = $object->$attribute;
+ /** @var ActiveRecordInterface $targetClass */
+ $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass;
+ $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
- if (is_array($value)) {
- $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.'));
- return;
+ if (is_array($targetAttribute)) {
+ $params = [];
+ foreach ($targetAttribute as $k => $v) {
+ $params[$v] = is_integer($k) ? $object->$v : $object->$k;
+ }
+ } else {
+ $params = [$targetAttribute => $object->$attribute];
}
- /** @var \yii\db\ActiveRecord $className */
- $className = $this->className === null ? get_class($object) : $this->className;
- $attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
+ foreach ($params as $value) {
+ if (is_array($value)) {
+ $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.'));
+ return;
+ }
+ }
- $query = $className::find();
- $query->where([$attributeName => $value]);
+ $query = $targetClass::find();
+ $query->where($params);
if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) {
// if current $object isn't in the database yet then it's OK just to call exists()
$exists = $query->exists();
} else {
// if current $object is in the database already we can't use exists()
- $query->limit(2);
- $objects = $query->all();
-
+ /** @var ActiveRecordInterface[] $objects */
+ $objects = $query->limit(2)->all();
$n = count($objects);
if ($n === 1) {
- if (in_array($attributeName, $className::primaryKey())) {
+ $keys = array_keys($params);
+ $pks = $targetClass::primaryKey();
+ sort($keys);
+ sort($pks);
+ if ($keys === $pks) {
// primary key is modified and not unique
$exists = $object->getOldPrimaryKey() != $object->getPrimaryKey();
} else {
// non-primary key, need to exclude the current record based on PK
- $exists = array_shift($objects)->getPrimaryKey() != $object->getOldPrimaryKey();
+ $exists = $objects[0]->getPrimaryKey() != $object->getOldPrimaryKey();
}
} else {
$exists = $n > 1;
diff --git a/framework/yii/validators/UrlValidator.php b/framework/yii/validators/UrlValidator.php
index 4023e2a..4cb20f6 100644
--- a/framework/yii/validators/UrlValidator.php
+++ b/framework/yii/validators/UrlValidator.php
@@ -121,9 +121,9 @@ class UrlValidator extends Validator
$options = [
'pattern' => new JsExpression($pattern),
- 'message' => Html::encode(strtr($this->message, [
+ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute),
- ])),
+ ]),
'enableIDN' => (boolean)$this->enableIDN,
];
if ($this->skipOnEmpty) {
diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php
index 0df48bd..540140f 100644
--- a/framework/yii/web/Controller.php
+++ b/framework/yii/web/Controller.php
@@ -101,9 +101,9 @@ class Controller extends \yii\base\Controller
}
/**
- * Creates a URL using the given route and parameters.
+ * Normalizes route making it suitable for UrlManager. Absolute routes are staying as is
+ * while relative routes are converted to absolute routes.
*
- * This method enhances [[UrlManager::createUrl()]] by supporting relative routes.
* A relative route is a route without a leading slash, such as "view", "post/view".
*
* - If the route is an empty string, the current [[route]] will be used;
@@ -112,13 +112,10 @@ class Controller extends \yii\base\Controller
* - If the route has no leading slash, it is considered to be a route relative
* to the current module and will be prepended with the module's uniqueId.
*
- * After this route conversion, the method calls [[UrlManager::createUrl()]] to create a URL.
- *
* @param string $route the route. This can be either an absolute route or a relative route.
- * @param array $params the parameters (name-value pairs) to be included in the generated URL
- * @return string the created URL
+ * @return string normalized route suitable for UrlManager
*/
- public function createUrl($route, $params = [])
+ protected function getNormalizedRoute($route)
{
if (strpos($route, '/') === false) {
// empty or an action ID
@@ -127,10 +124,58 @@ class Controller extends \yii\base\Controller
// relative to module
$route = ltrim($this->module->getUniqueId() . '/' . $route, '/');
}
+ return $route;
+ }
+
+ /**
+ * Creates a relative URL using the given route and parameters.
+ *
+ * This method enhances [[UrlManager::createUrl()]] by supporting relative routes.
+ * A relative route is a route without a leading slash, such as "view", "post/view".
+ *
+ * - If the route is an empty string, the current [[route]] will be used;
+ * - If the route contains no slashes at all, it is considered to be an action ID
+ * of the current controller and will be prepended with [[uniqueId]];
+ * - If the route has no leading slash, it is considered to be a route relative
+ * to the current module and will be prepended with the module's uniqueId.
+ *
+ * After this route conversion, the method calls [[UrlManager::createUrl()]] to create a URL.
+ *
+ * @param string $route the route. This can be either an absolute route or a relative route.
+ * @param array $params the parameters (name-value pairs) to be included in the generated URL
+ * @return string the created relative URL
+ */
+ public function createUrl($route, $params = [])
+ {
+ $route = $this->getNormalizedRoute($route);
return Yii::$app->getUrlManager()->createUrl($route, $params);
}
/**
+ * Creates an absolute URL using the given route and parameters.
+ *
+ * This method enhances [[UrlManager::createAbsoluteUrl()]] by supporting relative routes.
+ * A relative route is a route without a leading slash, such as "view", "post/view".
+ *
+ * - If the route is an empty string, the current [[route]] will be used;
+ * - If the route contains no slashes at all, it is considered to be an action ID
+ * of the current controller and will be prepended with [[uniqueId]];
+ * - If the route has no leading slash, it is considered to be a route relative
+ * to the current module and will be prepended with the module's uniqueId.
+ *
+ * After this route conversion, the method calls [[UrlManager::createUrl()]] to create a URL.
+ *
+ * @param string $route the route. This can be either an absolute route or a relative route.
+ * @param array $params the parameters (name-value pairs) to be included in the generated URL
+ * @return string the created absolute URL
+ */
+ public function createAbsoluteUrl($route, $params = [])
+ {
+ $route = $this->getNormalizedRoute($route);
+ return Yii::$app->getUrlManager()->createAbsoluteUrl($route, $params);
+ }
+
+ /**
* Returns the canonical URL of the currently requested page.
* The canonical URL is constructed using [[route]] and [[actionParams]]. You may use the following code
* in the layout view to add a link tag about canonical URL:
diff --git a/framework/yii/widgets/ActiveField.php b/framework/yii/widgets/ActiveField.php
index 4228ea9..bd26237 100644
--- a/framework/yii/widgets/ActiveField.php
+++ b/framework/yii/widgets/ActiveField.php
@@ -112,8 +112,8 @@ class ActiveField extends Component
/**
* @var array different parts of the field (e.g. input, label). This will be used together with
* [[template]] to generate the final field HTML code. The keys are the token names in [[template]],
- * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}`,
- * `{error}`, and `{error}`. Note that you normally don't need to access this property directly as
+ * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}` and `{error}`.
+ * Note that you normally don't need to access this property directly as
* it is maintained by various methods of this class.
*/
public $parts = [];
diff --git a/framework/yii/widgets/InputWidget.php b/framework/yii/widgets/InputWidget.php
index e1981c9..0a4b5b7 100644
--- a/framework/yii/widgets/InputWidget.php
+++ b/framework/yii/widgets/InputWidget.php
@@ -11,6 +11,7 @@ use Yii;
use yii\base\Widget;
use yii\base\Model;
use yii\base\InvalidConfigException;
+use yii\helpers\Html;
/**
* InputWidget is the base class for widgets that collect user inputs.
@@ -40,6 +41,10 @@ class InputWidget extends Widget
* @var string the input value.
*/
public $value;
+ /**
+ * @var array the HTML attributes for the input tag.
+ */
+ public $options = [];
/**
@@ -49,7 +54,10 @@ class InputWidget extends Widget
public function init()
{
if (!$this->hasModel() && $this->name === null) {
- throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified.");
+ throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified.");
+ }
+ if (!isset($this->options['id'])) {
+ $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId();
}
parent::init();
}
diff --git a/framework/yii/widgets/MaskedInput.php b/framework/yii/widgets/MaskedInput.php
index fc21cef..7eb42a7 100644
--- a/framework/yii/widgets/MaskedInput.php
+++ b/framework/yii/widgets/MaskedInput.php
@@ -61,10 +61,6 @@ class MaskedInput extends InputWidget
* @var string a JavaScript function callback that will be invoked when user finishes the input.
*/
public $completed;
- /**
- * @var array the HTML attributes for the input tag.
- */
- public $options = [];
/**
@@ -77,10 +73,6 @@ class MaskedInput extends InputWidget
if (empty($this->mask)) {
throw new InvalidConfigException('The "mask" property must be set.');
}
-
- if (!isset($this->options['id'])) {
- $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId();
- }
}
/**
diff --git a/tests/unit/data/ar/Category.php b/tests/unit/data/ar/Category.php
new file mode 100644
index 0000000..cebacb0
--- /dev/null
+++ b/tests/unit/data/ar/Category.php
@@ -0,0 +1,27 @@
+hasMany(Item::className(), ['category_id' => 'id']);
+ }
+}
diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php
index e725be9..2d04f9e 100644
--- a/tests/unit/data/ar/Item.php
+++ b/tests/unit/data/ar/Item.php
@@ -15,4 +15,9 @@ class Item extends ActiveRecord
{
return 'tbl_item';
}
+
+ public function getCategory()
+ {
+ return $this->hasOne(Category::className(), ['id' => 'category_id']);
+ }
}
diff --git a/tests/unit/extensions/mongodb/ActiveRelationTest.php b/tests/unit/extensions/mongodb/ActiveRelationTest.php
index 8736d52..2baeab4 100644
--- a/tests/unit/extensions/mongodb/ActiveRelationTest.php
+++ b/tests/unit/extensions/mongodb/ActiveRelationTest.php
@@ -69,7 +69,7 @@ class ActiveRelationTest extends MongoDbTestCase
$this->assertTrue($order->isRelationPopulated('customer'));
$this->assertTrue($customer instanceof Customer);
$this->assertEquals((string)$customer->_id, (string)$order->customer_id);
- $this->assertEquals(1, count($order->populatedRelations));
+ $this->assertEquals(1, count($order->relatedRecords));
}
public function testFindEager()
@@ -83,4 +83,4 @@ class ActiveRelationTest extends MongoDbTestCase
$this->assertTrue($orders[1]->customer instanceof Customer);
$this->assertEquals((string)$orders[1]->customer->_id, (string)$orders[1]->customer_id);
}
-}
\ No newline at end of file
+}
diff --git a/tests/unit/extensions/sphinx/ActiveRelationTest.php b/tests/unit/extensions/sphinx/ActiveRelationTest.php
index cd58035..d85c6b9 100644
--- a/tests/unit/extensions/sphinx/ActiveRelationTest.php
+++ b/tests/unit/extensions/sphinx/ActiveRelationTest.php
@@ -29,7 +29,7 @@ class ActiveRelationTest extends SphinxTestCase
$index = $article->index;
$this->assertTrue($article->isRelationPopulated('index'));
$this->assertTrue($index instanceof ArticleIndex);
- $this->assertEquals(1, count($article->populatedRelations));
+ $this->assertEquals(1, count($article->relatedRecords));
$this->assertEquals($article->id, $index->id);
}
@@ -42,4 +42,4 @@ class ActiveRelationTest extends SphinxTestCase
$this->assertTrue($articles[0]->index instanceof ArticleIndex);
$this->assertTrue($articles[1]->index instanceof ArticleIndex);
}
-}
\ No newline at end of file
+}
diff --git a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php
index e30a0cf..1740c42 100644
--- a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php
+++ b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php
@@ -32,7 +32,7 @@ class ExternalActiveRelationTest extends SphinxTestCase
$source = $article->source;
$this->assertTrue($article->isRelationPopulated('source'));
$this->assertTrue($source instanceof ArticleDb);
- $this->assertEquals(1, count($article->populatedRelations));
+ $this->assertEquals(1, count($article->relatedRecords));
// has many :
/*$this->assertFalse($article->isRelationPopulated('tags'));
@@ -71,4 +71,4 @@ class ExternalActiveRelationTest extends SphinxTestCase
->all();
$this->assertEquals(2, count($articles));
}
-}
\ No newline at end of file
+}
diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php
index 50a5f81..95def9d 100644
--- a/tests/unit/framework/ar/ActiveRecordTestTrait.php
+++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php
@@ -392,14 +392,14 @@ trait ActiveRecordTestTrait
$orders = $customer->orders;
$this->assertTrue($customer->isRelationPopulated('orders'));
$this->assertEquals(2, count($orders));
- $this->assertEquals(1, count($customer->populatedRelations));
+ $this->assertEquals(1, count($customer->relatedRecords));
/** @var Customer $customer */
$customer = $this->callCustomerFind(2);
$this->assertFalse($customer->isRelationPopulated('orders'));
$orders = $customer->getOrders()->where(['id' => 3])->all();
$this->assertFalse($customer->isRelationPopulated('orders'));
- $this->assertEquals(0, count($customer->populatedRelations));
+ $this->assertEquals(0, count($customer->relatedRecords));
$this->assertEquals(1, count($orders));
$this->assertEquals(3, $orders[0]->id);
@@ -421,7 +421,7 @@ trait ActiveRecordTestTrait
$customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one();
$this->assertTrue($customer->isRelationPopulated('orders'));
$this->assertEquals(1, count($customer->orders));
- $this->assertEquals(1, count($customer->populatedRelations));
+ $this->assertEquals(1, count($customer->relatedRecords));
}
public function testFindLazyVia()
diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php
index 15462b5..40050e5 100644
--- a/tests/unit/framework/db/ActiveRecordTest.php
+++ b/tests/unit/framework/db/ActiveRecordTest.php
@@ -217,4 +217,64 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertTrue(OrderItem::isPrimaryKey(['order_id', 'item_id']));
$this->assertFalse(OrderItem::isPrimaryKey(['order_id', 'item_id', 'quantity']));
}
+
+ public function testJoinWith()
+ {
+ // left join and eager loading
+ $orders = Order::find()->joinWith('customer')->orderBy('tbl_customer.id DESC, tbl_order.id')->all();
+ $this->assertEquals(3, count($orders));
+ $this->assertEquals(2, $orders[0]->id);
+ $this->assertEquals(3, $orders[1]->id);
+ $this->assertEquals(1, $orders[2]->id);
+ $this->assertTrue($orders[0]->isRelationPopulated('customer'));
+ $this->assertTrue($orders[1]->isRelationPopulated('customer'));
+ $this->assertTrue($orders[2]->isRelationPopulated('customer'));
+
+ // inner join filtering and eager loading
+ $orders = Order::find()->innerJoinWith([
+ 'customer' => function ($query) {
+ $query->where('tbl_customer.id=2');
+ },
+ ])->orderBy('tbl_order.id')->all();
+ $this->assertEquals(2, count($orders));
+ $this->assertEquals(2, $orders[0]->id);
+ $this->assertEquals(3, $orders[1]->id);
+ $this->assertTrue($orders[0]->isRelationPopulated('customer'));
+ $this->assertTrue($orders[1]->isRelationPopulated('customer'));
+
+ // inner join filtering without eager loading
+ $orders = Order::find()->innerJoinWith([
+ 'customer' => function ($query) {
+ $query->where('tbl_customer.id=2');
+ },
+ ], false)->orderBy('tbl_order.id')->all();
+ $this->assertEquals(2, count($orders));
+ $this->assertEquals(2, $orders[0]->id);
+ $this->assertEquals(3, $orders[1]->id);
+ $this->assertFalse($orders[0]->isRelationPopulated('customer'));
+ $this->assertFalse($orders[1]->isRelationPopulated('customer'));
+
+ // join with via-relation
+ $orders = Order::find()->innerJoinWith('books')->orderBy('tbl_order.id')->all();
+ $this->assertEquals(2, count($orders));
+ $this->assertEquals(1, $orders[0]->id);
+ $this->assertEquals(3, $orders[1]->id);
+ $this->assertTrue($orders[0]->isRelationPopulated('books'));
+ $this->assertTrue($orders[1]->isRelationPopulated('books'));
+ $this->assertEquals(2, count($orders[0]->books));
+ $this->assertEquals(1, count($orders[1]->books));
+
+ // join with sub-relation
+ $orders = Order::find()->innerJoinWith([
+ 'items.category' => function ($q) {
+ $q->where('tbl_category.id = 2');
+ },
+ ])->orderBy('tbl_order.id')->all();
+ $this->assertEquals(1, count($orders));
+ $this->assertTrue($orders[0]->isRelationPopulated('items'));
+ $this->assertEquals(2, $orders[0]->id);
+ $this->assertEquals(3, count($orders[0]->items));
+ $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category'));
+ $this->assertEquals(2, $orders[0]->items[0]->category->id);
+ }
}
diff --git a/tests/unit/framework/validators/ExistValidatorTest.php b/tests/unit/framework/validators/ExistValidatorTest.php
index 45ff5d5..8f1a054 100644
--- a/tests/unit/framework/validators/ExistValidatorTest.php
+++ b/tests/unit/framework/validators/ExistValidatorTest.php
@@ -7,6 +7,8 @@ use Yii;
use yii\base\Exception;
use yii\validators\ExistValidator;
use yiiunit\data\ar\ActiveRecord;
+use yiiunit\data\ar\Order;
+use yiiunit\data\ar\OrderItem;
use yiiunit\data\validators\models\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel;
use yiiunit\framework\db\DatabaseTestCase;
@@ -34,18 +36,18 @@ class ExistValidatorTest extends DatabaseTestCase
}
// combine to save the time creating a new db-fixture set (likely ~5 sec)
try {
- $val = new ExistValidator(['className' => ValidatorTestMainModel::className()]);
+ $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className()]);
$val->validate('ref');
$this->fail('Exception should have been thrown at this time');
} catch (Exception $e) {
$this->assertInstanceOf('yii\base\InvalidConfigException', $e);
- $this->assertEquals('The "attributeName" property must be set.', $e->getMessage());
+ $this->assertEquals('The "attributeName" property must be configured as a string.', $e->getMessage());
}
}
public function testValidateValue()
{
- $val = new ExistValidator(['className' => ValidatorTestRefModel::className(), 'attributeName' => 'id']);
+ $val = new ExistValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'id']);
$this->assertTrue($val->validate(2));
$this->assertTrue($val->validate(5));
$this->assertFalse($val->validate(99));
@@ -55,22 +57,22 @@ class ExistValidatorTest extends DatabaseTestCase
public function testValidateAttribute()
{
// existing value on different table
- $val = new ExistValidator(['className' => ValidatorTestMainModel::className(), 'attributeName' => 'id']);
+ $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']);
$m = ValidatorTestRefModel::find(['id' => 1]);
$val->validateAttribute($m, 'ref');
$this->assertFalse($m->hasErrors());
// non-existing value on different table
- $val = new ExistValidator(['className' => ValidatorTestMainModel::className(), 'attributeName' => 'id']);
+ $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']);
$m = ValidatorTestRefModel::find(['id' => 6]);
$val->validateAttribute($m, 'ref');
$this->assertTrue($m->hasErrors('ref'));
// existing value on same table
- $val = new ExistValidator(['attributeName' => 'ref']);
+ $val = new ExistValidator(['targetAttribute' => 'ref']);
$m = ValidatorTestRefModel::find(['id' => 2]);
$val->validateAttribute($m, 'test_val');
$this->assertFalse($m->hasErrors());
// non-existing value on same table
- $val = new ExistValidator(['attributeName' => 'ref']);
+ $val = new ExistValidator(['targetAttribute' => 'ref']);
$m = ValidatorTestRefModel::find(['id' => 5]);
$val->validateAttribute($m, 'test_val_fail');
$this->assertTrue($m->hasErrors('test_val_fail'));
@@ -86,10 +88,50 @@ class ExistValidatorTest extends DatabaseTestCase
$val->validateAttribute($m, 'a_field');
$this->assertTrue($m->hasErrors('a_field'));
// check array
- $val = new ExistValidator(['attributeName' => 'ref']);
+ $val = new ExistValidator(['targetAttribute' => 'ref']);
$m = ValidatorTestRefModel::find(['id' => 2]);
$m->test_val = [1,2,3];
$val->validateAttribute($m, 'test_val');
$this->assertTrue($m->hasErrors('test_val'));
}
+
+ public function testValidateCompositeKeys()
+ {
+ $val = new ExistValidator([
+ 'targetClass' => OrderItem::className(),
+ 'targetAttribute' => ['order_id', 'item_id'],
+ ]);
+ // validate old record
+ $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]);
+ $val->validateAttribute($m, 'order_id');
+ $this->assertFalse($m->hasErrors('order_id'));
+
+ // validate new record
+ $m = new OrderItem(['order_id' => 1, 'item_id' => 2]);
+ $val->validateAttribute($m, 'order_id');
+ $this->assertFalse($m->hasErrors('order_id'));
+ $m = new OrderItem(['order_id' => 10, 'item_id' => 2]);
+ $val->validateAttribute($m, 'order_id');
+ $this->assertTrue($m->hasErrors('order_id'));
+
+ $val = new ExistValidator([
+ 'targetClass' => OrderItem::className(),
+ 'targetAttribute' => ['id' => 'order_id'],
+ ]);
+ // validate old record
+ $m = Order::find(1);
+ $val->validateAttribute($m, 'id');
+ $this->assertFalse($m->hasErrors('id'));
+ $m = Order::find(1);
+ $m->id = 10;
+ $val->validateAttribute($m, 'id');
+ $this->assertTrue($m->hasErrors('id'));
+
+ $m = new Order(['id' => 1]);
+ $val->validateAttribute($m, 'id');
+ $this->assertFalse($m->hasErrors('id'));
+ $m = new Order(['id' => 10]);
+ $val->validateAttribute($m, 'id');
+ $this->assertTrue($m->hasErrors('id'));
+ }
}
diff --git a/tests/unit/framework/validators/UniqueValidatorTest.php b/tests/unit/framework/validators/UniqueValidatorTest.php
index 707239c..4af3d29 100644
--- a/tests/unit/framework/validators/UniqueValidatorTest.php
+++ b/tests/unit/framework/validators/UniqueValidatorTest.php
@@ -6,6 +6,8 @@ namespace yiiunit\framework\validators;
use yii\validators\UniqueValidator;
use Yii;
use yiiunit\data\ar\ActiveRecord;
+use yiiunit\data\ar\Order;
+use yiiunit\data\ar\OrderItem;
use yiiunit\data\validators\models\FakedValidationModel;
use yiiunit\data\validators\models\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel;
@@ -58,7 +60,7 @@ class UniqueValidatorTest extends DatabaseTestCase
public function testValidateAttributeOfNonARModel()
{
- $val = new UniqueValidator(['className' => ValidatorTestRefModel::className(), 'attributeName' => 'ref']);
+ $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']);
$m = FakedValidationModel::createWithAttributes(['attr_1' => 5, 'attr_2' => 1313]);
$val->validateAttribute($m, 'attr_1');
$this->assertTrue($m->hasErrors('attr_1'));
@@ -68,7 +70,7 @@ class UniqueValidatorTest extends DatabaseTestCase
public function testValidateNonDatabaseAttribute()
{
- $val = new UniqueValidator(['className' => ValidatorTestRefModel::className(), 'attributeName' => 'ref']);
+ $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']);
$m = ValidatorTestMainModel::find(1);
$val->validateAttribute($m, 'testMainVal');
$this->assertFalse($m->hasErrors('testMainVal'));
@@ -85,4 +87,51 @@ class UniqueValidatorTest extends DatabaseTestCase
$m = new ValidatorTestMainModel();
$val->validateAttribute($m, 'testMainVal');
}
+
+ public function testValidateCompositeKeys()
+ {
+ $val = new UniqueValidator([
+ 'targetClass' => OrderItem::className(),
+ 'targetAttribute' => ['order_id', 'item_id'],
+ ]);
+ // validate old record
+ $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]);
+ $val->validateAttribute($m, 'order_id');
+ $this->assertFalse($m->hasErrors('order_id'));
+ $m->item_id = 1;
+ $val->validateAttribute($m, 'order_id');
+ $this->assertTrue($m->hasErrors('order_id'));
+
+ // validate new record
+ $m = new OrderItem(['order_id' => 1, 'item_id' => 2]);
+ $val->validateAttribute($m, 'order_id');
+ $this->assertTrue($m->hasErrors('order_id'));
+ $m = new OrderItem(['order_id' => 10, 'item_id' => 2]);
+ $val->validateAttribute($m, 'order_id');
+ $this->assertFalse($m->hasErrors('order_id'));
+
+ $val = new UniqueValidator([
+ 'targetClass' => OrderItem::className(),
+ 'targetAttribute' => ['id' => 'order_id'],
+ ]);
+ // validate old record
+ $m = Order::find(1);
+ $val->validateAttribute($m, 'id');
+ $this->assertTrue($m->hasErrors('id'));
+ $m = Order::find(1);
+ $m->id = 2;
+ $val->validateAttribute($m, 'id');
+ $this->assertTrue($m->hasErrors('id'));
+ $m = Order::find(1);
+ $m->id = 10;
+ $val->validateAttribute($m, 'id');
+ $this->assertFalse($m->hasErrors('id'));
+
+ $m = new Order(['id' => 1]);
+ $val->validateAttribute($m, 'id');
+ $this->assertTrue($m->hasErrors('id'));
+ $m = new Order(['id' => 10]);
+ $val->validateAttribute($m, 'id');
+ $this->assertFalse($m->hasErrors('id'));
+ }
}