dfware/ar-variation

为Yii2中的ActiveRecord变异提供支持,通过相关模型实现

资助包维护!
klimov-paul
Patreon

安装: 49

依赖: 0

建议: 0

安全: 0

星星: 0

关注者: 0

分支: 10

开放问题: 0

类型:yii2-extension

1.0.0 2023-09-27 17:51 UTC

This package is auto-updated.

Last update: 2024-09-27 20:25:43 UTC


README

安装此扩展的最佳方式是通过 composer

运行以下命令之一

composer require dfware/ar-variation

"dfware/ar-variation": "*"

将其添加到你的composer.json文件中的require部分。

用法

此扩展通过相关模型为ActiveRecord变异提供支持。变异意味着某些特定实体具有属性(字段),其值应根据实际选定的选项而变化。在数据库结构中,变异通过在连接实体中添加额外列的多个对多个关系实现。

此类情况最常见的例子是i18n功能:想象我们有一个项目,其标题和描述应在多种不同的语言中提供。在关系数据库中,将为此情况创建两个不同的表:一个用于项目,另一个用于项目翻译,其中包含项目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行为。您必须为'Language'、'Item'和'ItemTranslation'创建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到变异ActiveRecord的'has many'关系工作的。在上面的例子中,它将是关系'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;

在这种情况下,您可能希望分别设置“前端”和“后端”(使用不同的网页界面或其他方法)。您可以使用[[\yii2tech\ar\variation\VariationBehavior::optionQueryFilter]]为“选项”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

您还可以使用“加班”条件来分别分离变体:在不同的过程中设置正常时间和加班时间的支付率。为此,您需要为“正常时间”和“加班”支付率声明两个分离的关系

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标志属性值一起保存。然而,只有对于“散列”查询条件,才会自动检测额外的变体模型属性。如果您有复杂的变体选项过滤逻辑,您需要手动设置[[\yii2tech\ar\variation\VariationBehavior::variationModelDefaultAttributes]]。

在上面的示例中,您可能不想在数据库中保存空变体数据:如果特定的开发者没有特定的“前端”技能,如“AngularJS”,则没有相应的支付率,因此没有必要为它保存一个空的“支付率”记录。您可以使用[[\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);
                },
            ],
        ];
    }

    // ...
}