yii2tech / ar-variation
通过相关模型为 Yii2 提供对 ActiveRecord 变化的支持
Requires
- yiisoft/yii2: ~2.0.14
Requires (Dev)
- phpunit/phpunit: 4.8.27|^5.0|^6.0
README
为 Yii 2 的 ActiveRecord 变化扩展
此扩展通过相关模型提供对 ActiveRecord 变化的支持。特别是它允许为 ActiveRecord 实现国际化功能。
有关许可信息,请参阅 LICENSE 文件。
安装
安装此扩展的最佳方式是通过 composer。
运行以下命令
php composer.phar require --prefer-dist yii2tech/ar-variation
或将其添加到 composer.json 的 require 部分。
"yii2tech/ar-variation": "*"
用法
此扩展通过相关模型提供对 ActiveRecord 变化的支持。变化意味着某些特定实体具有属性(字段),其值应根据实际选择选项而变化。在数据库结构中,变化作为与连接实体的一对多关系实现,并在连接实体中添加额外列。
这种情况的最常见例子是国际化功能:假设我们有一个项目,其标题和描述应在几种不同的语言中提供。在关系型数据库中,将有两个不同的表:一个用于项目,另一个用于项目翻译,其中包含项目 ID 和语言 ID,以及实际的标题和描述。此解决方案的 DDL 如下所示
CREATE TABLE `Language` ( `id` varchar(5) NOT NULL, `name` varchar(64) NOT NULL, `locale` varchar(5) NOT NULL, PRIMARY KEY (`id`) ) ENGINE InnoDB; CREATE TABLE `Item` ( `id` integer NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL, `price` integer, PRIMARY KEY (`id`) ) ENGINE InnoDB; CREATE TABLE `ItemTranslation` ( `itemId` integer NOT NULL, `languageId` varchar(5) NOT NULL, `title` varchar(64) NOT NULL, `description` TEXT, PRIMARY KEY (`itemId`, `languageId`) FOREIGN KEY (`itemId`) REFERENCES `Item` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`languageId`) REFERENCES `Language` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ) ENGINE InnoDB;
通常,在大多数情况下,无需“项目”知道其所有翻译 - 仅获取一个即可,该翻译用作 Web 应用程序界面语言。
此扩展为 Yii2 中此类解决方案的支持提供了 [[\yii2tech\ar\variation\VariationBehavior]] ActiveRecord 行为。您需要创建“语言”、“项目”和“项目翻译”的 ActiveRecord 类,并以以下方式附加 [[\yii2tech\ar\variation\VariationBehavior]]
class Item extends ActiveRecord { public function behaviors() { return [ 'translations' => [ 'class' => VariationBehavior::className(), 'variationsRelation' => 'translations', 'defaultVariationRelation' => 'defaultTranslation', 'variationOptionReferenceAttribute' => 'languageId', 'optionModelClass' => Language::className(), 'defaultVariationOptionReference' => function() {return Yii::$app->language;}, 'variationAttributeDefaultValueMap' => [ 'title' => 'name' ], ], ]; } public static function tableName() { return 'Item'; } /** * @return \yii\db\ActiveQuery */ public function getTranslations() { return $this->hasMany(ItemTranslation::className(), ['itemId' => 'id']); } /** * @return \yii\db\ActiveQuery */ public function getDefaultTranslation() { return $this->hasDefaultVariationRelation(); // convert "has many translations" into "has one defaultTranslation" } }
请注意,行为通过主 ActiveRecord 在“has many”关系中声明与变化 ActiveRecord 一起工作。在上面的示例中,它将是“translations”关系。您还必须声明默认变化关系为“has one”,这可以通过 [[\yii2tech\ar\variation\VariationBehavior::hasDefaultVariationRelation()]] 方法轻松完成。此类关系继承源关系的所有信息,并在变化选项引用上应用额外条件,该引用由 [[\yii2tech\ar\variation\VariationBehavior::defaultVariationOptionReference]] 确定。此引用应提供与变化实体 [[\yii2tech\ar\variation\VariationBehavior::variationOptionReferenceAttribute]] 的 [[\yii2tech\ar\variation\VariationBehavior::variationOptionReferenceAttribute]] 属性匹配的默认值。
访问变化属性
拥有 defaultVariationRelation
对于使用变化属性很重要。应用 [[\yii2tech\ar\variation\VariationBehavior]] 允许访问变化字段,就像它们是主要字段一样
$item = Item::findOne(1); echo $item->title; // equal to `$item->defaultTranslation->title` echo $item->description; // equal to `$item->defaultTranslation->description`
如果主实体对于特定选项没有变异,您可以使用 [[\yii2tech\ar\variation\VariationBehavior::$variationAttributeDefaultValueMap]] 为变异字段提供默认值,就像在上面的示例中对'title'所做的那样。
$item = new Item(); // of course there is no translation for the new item $item->name = 'new item'; echo $item->title; // outputs 'new item'
查询变异
正如之前所说,[[\yii2tech\ar\variation\VariationBehavior]] 通过关系工作。因此,为了使变异属性功能生效,它将执行一个额外的查询以检索默认变异模型,这可能在对多个模型进行操作时产生性能影响。为了减少查询次数,您可以在默认变异关系上使用 with()
。
$items = Item::find()->with('defaultTranslation')->all(); // only 2 queries will be performed foreach ($items as $item) { echo $item->title . '<br>'; }
您还可以在 with()
中使用主变异关系。在这种情况下,默认变异将从其中获取,而无需额外的查询。
$items = Item::find()->with('translations')->all(); // only 2 queries will be performed foreach ($items as $item) { echo $item->title . '<br>'; // no extra query var_dump($item->defaultTranslation); // no extra query, `defaultTranslation` is populated from `translations` }
如果您使用的是关系数据库,您还可以使用 [[\yii\db\ActiveQuery::joinWith()]]。
$items = Item::find()->joinWith('defaultTranslation')->all();
您可以将 'with' 应用于变异关系作为主 ActiveRecord 查询的默认作用域。
class Item extends ActiveRecord { // ... public static function find() { return parent::find()->with('defaultTranslation'); } }
访问特定变异
您始终可以通过 getDefaultVariationModel()
方法访问默认变异模型。
$item = Item::findOne(1); $variationModel = $item->getDefaultVariationModel(); // get default variation instance echo $item->defaultVariationModel->title; // default variation is also available as virtual property
然而,在某些情况下,需要访问特定变异而不是默认变异。这可以通过 getVariationModel()
方法完成。
$item = Item::findOne(1); $frenchTranslation = $item->getVariationModel('fr'); $russianTranslation = $item->getVariationModel('ru');
注意:方法
getVariationModel()
将完全加载 [[\yii2tech\ar\variation\VariationBehavior::variationsRelation]] 关系,这可能降低性能。如果可能,您应该始终优先使用 [[getDefaultVariationModel()]] 方法。您还可以使用额外的条件筛选结果以节省性能的预加载variationsRelation
。
创建变异设置网页界面
使用 [[\yii2tech\ar\variation\VariationBehavior]] 可以简化变异的管理和创建它们的设置网页界面。
变异管理的网页控制器可能看起来如下所示
use yii\base\Model; use yii\web\Controller; use Yii; class ItemController extends Controller { public function actionCreate() { $model = new Item(); $post = Yii::$app->request->post(); if ($model->load($post) && Model::loadMultiple($model->getVariationModels(), $post) && $model->save()) { return $this->redirect(['index']); } return $this->render('create', [ 'model' => $model, ]); } }
注意,变异模型应手动用请求数据填充,但它们将自动进行验证和保存 - 您不需要手动进行此操作。只有在变异模型在所有者验证之前或保存触发之前被检索的情况下,才会进行自动处理。因此,它不会影响纯所有者验证或保存。
表单视图文件可以是以下内容
<?php use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $model Item */ ?> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'name'); ?> <?= $form->field($model, 'price'); ?> <?php foreach ($model->getVariationModels() as $index => $variationModel): ?> <?= $form->field($variationModel, "[{$index}]title")->label($variationModel->getAttributeLabel('title') . ' (' . $variationModel->languageId . ')'); ?> <?= $form->field($variationModel, "[{$index}]description")->label($variationModel->getAttributeLabel('description') . ' (' . $variationModel->languageId . ')'); ?> <?php endforeach; ?> <div class="form-group"> <?= Html::submitButton('Save', ['class' => 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?>
保存默认变异
无需一次性处理所有可能的变异 - 您可以仅操作单个变异模型,验证并保存它。例如:您可以提供一个网页界面,其中用户可以仅设置当前语言的翻译。这样做的话,最好设置 [[\yii2tech\ar\variation\VariationBehavior::$variationAttributeDefaultValueMap]] 值,允许对变异属性进行魔法访问。在被检索的默认变异模型将随着主模型一起进行验证和保存。
$item = Item::findOne($id); $item->title = ''; // setup of `$item->defaultTranslation->title` var_dump($item->validate()); // outputs: `false` $item->title = 'new title'; $item->save(); // invokes `$item->defaultTranslation->save()`
如果属性在 [[\yii2tech\ar\variation\VariationBehavior::$variationAttributeDefaultValueMap]] 中提及,则也可以将其设置为可设置,即使默认变异模型不存在:在这种情况下,它将自动创建。例如
$item = new Item(); $item->name = 'new name'; $item->title = 'translation title'; // setup of `$item->defaultTranslation` attribute, creating default variation model $item->description = 'translation description'; $item->save(); // saving both main model and default variation model
将主模型中的变异属性标记为 'safe',您可以通过简单的方式创建一个网页界面来设置它们。模型代码应如下所示
class Item extends ActiveRecord { public function behaviors() { return [ 'translations' => [ 'class' => VariationBehavior::className(), // ... 'variationAttributeDefaultValueMap' => [ 'title' => 'name', 'description' => null, ], ], ]; } public function rules() { return [ // ... [['title', 'description'], 'safe'] // allow 'title' and 'description' to be populated via main model ]; } // ... }
在视图中,您可以直接使用主模型中的变异属性
<?php use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $model Item */ ?> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'name'); ?> <?= $form->field($model, 'price'); ?> <?= $form->field($model, "title"); ?> <?= $form->field($model, "description")->textarea(); ?> <div class="form-group"> <?= Html::submitButton('Save', ['class' => 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?>
然后控制器代码将非常简单
use yii\web\Controller; use Yii; class ItemController extends Controller { public function actionCreate() { $model = new Item(); if ($model->load(Yii::$app->request->post()) && $model->save()) { // variation attributes are populated automatically // and variation model saved return $this->redirect(['index']); } return $this->render('create', [ 'model' => $model, ]); } }
额外的变异条件
存在一些情况,当变体选项或变体实体有额外的过滤条件或属性时。例如:假设我们有一个包含开发者及其支付率的数据库,支付率因特定的工作类型而异。工作类型按类别分组,如'前端'、'后端'、'数据库'等。支付率应分别设置正常工作时间和加班的支付率。此类用例的DDL可以是以下内容
CREATE TABLE `Developer` ( `id` integer NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL, PRIMARY KEY (`id`) ) ENGINE InnoDB; CREATE TABLE `WorkTypeGroup` ( `id` integer NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL, PRIMARY KEY (`id`) ) ENGINE InnoDB; CREATE TABLE `WorkType` ( `id` integer NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL, `groupId` integer NOT NULL, PRIMARY KEY (`id`) FOREIGN KEY (`groupId`) REFERENCES `WorkTypeGroup` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ) ENGINE InnoDB; CREATE TABLE `DeveloperPaymentRate` ( `developerId` integer NOT NULL, `workTypeId` varchar(5) NOT NULL, `paymentRate` integer NOT NULL, `isOvertime` integer(1) NOT NULL, PRIMARY KEY (`developerId`, `workTypeId`) FOREIGN KEY (`developerId`) REFERENCES `Developer` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`workTypeId`) REFERENCES `WorkType` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ) ENGINE InnoDB;
在这种情况下,您可能希望分别设置'前端'和'后端'(使用不同的Web界面或其他方式)。您可以使用[[\yii2tech\ar\variation\VariationBehavior::optionQueryFilter]]为'option' Active Record查询应用额外的过滤条件
class Developer extends ActiveRecord { public function behaviors() { return [ 'frontEndPaymentRates' => [ 'class' => VariationBehavior::className(), 'variationsRelation' => 'paymentRates', 'variationOptionReferenceAttribute' => 'workTypeId', 'optionModelClass' => WorkType::className(), 'optionQueryFilter' => [ 'groupId' => WorkType::GROUP_FRONT_END // add 'where' condition to the `WorkType` query ], ], 'backEndPaymentRates' => [ 'class' => VariationBehavior::className(), 'variationsRelation' => 'paymentRates', 'variationOptionReferenceAttribute' => 'workTypeId', 'optionModelClass' => WorkType::className(), // you can use a PHP callable as filter as well: 'optionQueryFilter' => function ($query) { $query->andWhere(['groupId' => WorkType::GROUP_BACK_END]); } ], ]; } // ... }
在这种情况下,您必须从行为实例访问getVariationModels()
,而不是直接从所有者访问
$developer = new Developer(); $developer->getBehavior('frontEndPaymentRates')->getVariationModels(); // get 'front-end' payment rates $developer->getBehavior('backEndPaymentRates')->getVariationModels(); // get 'back-end' payment rates
您也可以使用'加班'条件来分离变体:在不同流程中设置正常时间和加班的支付率。为此,您必须为'正常时间'和'加班'支付率声明2个单独的关系
class Developer extends ActiveRecord { public function behaviors() { return [ 'regularPaymentRates' => [ 'class' => VariationBehavior::className(), 'variationsRelation' => 'regularPaymentRates', 'variationOptionReferenceAttribute' => 'workTypeId', 'optionModelClass' => WorkType::className(), ], 'overtimePaymentRates' => [ 'class' => VariationBehavior::className(), 'variationsRelation' => 'overtimePaymentRates', 'variationOptionReferenceAttribute' => 'workTypeId', 'optionModelClass' => WorkType::className(), ], ]; } public function getPaymentRates() { return $this->hasMany(PaymentRates::className(), ['developerId' => 'id']); // basic 'payment rates' relation } public function getRegularPaymentRates() { return $this->getPaymentRates()->andWhere(['isOvertime' => false]); // regular payment rates } public function getOvertimePaymentRates() { return $this->getPaymentRates()->andWhere(['isOvertime' => true]); // overtime payment rates } // ... }
在这种情况下,变体将仅加载特定类型的支付率,并通过相应的isOvertime
标志属性值保存。然而,只有对于'hash'查询条件,自动检测额外的变体模型属性才会工作。如果您有复杂的变体选项过滤逻辑,您需要手动设置[[\yii2tech\ar\variation\VariationBehavior::variationModelDefaultAttributes]]。
在上面的示例中,您可能不希望将空变体数据保存到数据库中:如果特定开发者没有特定的'前端'技能,如'AngularJS',则没有相应的支付率,因此没有必要为它保存一个空的'PaymentRate'记录。您可以使用[[\yii2tech\ar\variation\VariationBehavior::variationSaveFilter]]来确定是否应保存变体记录。例如
class Developer extends ActiveRecord { public function behaviors() { return [ 'paymentRates' => [ 'class' => VariationBehavior::className(), 'variationsRelation' => 'regularPaymentRates', 'variationOptionReferenceAttribute' => 'workTypeId', 'optionModelClass' => WorkType::className(), 'variationSaveFilter' => function ($model) { return !empty($model->paymentRate); }, ], ]; } // ... }