在 SilverStripe 中链接/取消链接多对多记录后更新字段

问题描述 投票:0回答:3

我通过扩展

Customer
创建了
DataObject
Member
Customer
many_many
Package
具有
DataObject
数据关系。

Credits
通过 CMS 基于
Customer
表中的
DataObject
字段链接/取消链接时,我希望增加/减少
Package
Limit
中的
Package
字段。

客户

class Customer extends Member {

    private static $db = array(
        'Gender' => 'Varchar(2)',
        'DateOfBirth' => 'Date',
        'Featured' => 'Boolean',
        'Credits' => 'Int'
    );

    private static $many_many = array(
        'Packages' => 'Package'
    );

    public function getCMSFields() {

        $fields = new FieldList();

        $config = GridFieldConfig_RelationEditor::create();
        $config->removeComponentsByType('GridFieldAddNewButton');

        $packageField = new GridField(
            'Packages',
            'Package',
            $this->Packages(),
            $config
        );

        $fields->addFieldToTab('Root.Package', $packageField); 

        Session::set('SingleID', $this->ID);

        $this->extend('updateCMSFields', $fields);

        return $fields;
    }
}

包装

class Package extends DataObject {

    private static $db = array(
        'Title' => 'Varchar(255)',
        'Limit' => 'Int'
    );

    private static $belongs_many_many = array(
        'Customers' => 'Customer'
    );

}
php silverstripe
3个回答
6
投票

当您创建或删除多对多关系时,只会修改数据库中的一条记录 - 表中连接关系双方元素的记录。因此,关系所基于的对象都不会更新。这就是为什么像

onBeforeWrite
onAfterWrite
onBeforeDelete
onAfterDelete
这样的方法根本不会被调用,并且您无法使用它们来检测此类更改。

但是,Silverstripe 提供了类

ManyManyList
,它负责与多对多关系相关的所有操作。有两种您感兴趣的方法:添加和删除。您可以覆盖它们并采取内部行动来执行您需要的操作。无论对象类型是什么,这些方法显然都会在每个链接或取消链接操作上调用,因此您应该对您特别感兴趣的类进行一些过滤。

重写

ManyManyList
类的正确方法是使用Injector机制,以免修改framework或cms文件夹内的任何内容。下面的示例使用了 Silverstripe 中成员和组之间的关系,但您可以轻松地根据您的需要采用它(客户 -> 成员;套餐 -> 组)。

app.yml

Injector:
    ManyManyList:
        class: ManyManyListExtended

ManyManyListExtended.php

/**
 * When adding or removing elements on a many to many relationship
 * neither side of the relationship is updated (written or deleted).
 * SilverStripe does not provide any built-in actions to get information
 * that such event occurs. This is why this class is created.
 *
 * When it is uses together with SilverStripe Injector mechanism it can provide
 * additional actions to run on many-to-many relations (see: class ManyManyList).
 */
class ManyManyListExtended extends ManyManyList {

    /**
     * Overwritten method for adding new element to many-to-many relationship.
     *
     * This is called for all many-to-many relationships combinations.
     * 'joinTable' field is used to make actions on specific relation only.
     *
     * @param mixed $item
     * @param null $extraFields
     * @throws Exception
     */
    public function add($item, $extraFields = null) {
        parent::add($item, $extraFields);

        if ($this->isGroupMembershipChange()) {
            $memberID = $this->getMembershipID($item, 'MemberID');
            $groupID = $this->getMembershipID($item, 'GroupID');
            SS_Log::log("Member ($memberID) added to Group ($groupID)", SS_Log::INFO);
            // ... put some additional actions here
        }
    }

    /**
     * Overwritten method for removing item from many-to-many relationship.
     *
     * This is called for all many-to-many relationships combinations.
     * 'joinTable' field is used to make actions on specific relation only.
     *
     * @param DataObject $item
     */
    public function remove($item) {
        parent::remove($item);

        if ($this->isGroupMembershipChange()) {
            $memberID = $this->getMembershipID($item, 'MemberID');
            $groupID = $this->getMembershipID($item, 'GroupID');
            SS_Log::log("Member ($memberID) removed from Group ($groupID)", SS_Log::INFO);
            // ... put some additional actions here            
        }
    }

    /**
     * Check if relationship is of Group-Member type.
     *
     * @return bool
     */
    private function isGroupMembershipChange() {
        return $this->getJoinTable() === 'Group_Members';
    }

    /**
     * Get the actual ID for many-to-many relationship part - local or foreign key value.
     *
     * This works both ways: make action on a Member being element of a Group OR
     * make action on a Group being part of a Member.
     *
     * @param DataObject|int $item
     * @param string $keyName
     * @return bool|null
     */
    private function getMembershipID($item, $keyName) {
        if ($this->getLocalKey() === $keyName)
            return is_object($item) ? $item->ID : $item;
        if ($this->getForeignKey() === $keyName)
            return $this->getForeignID();
        return false;
    }
}

3dgoo 提供的解决方案也应该可以正常工作,但在我看来,该代码做了更多的“黑客攻击”,这就是为什么它的可维护性要差得多。它需要更多的修改(在两个类中),并且如果您想进行任何额外的链接/取消链接管理,例如添加自定义管理模块或某些表单,则需要成倍增加。


2
投票

问题是在多对多关系上添加或删除项目时,关系的任何一方都没有被写入。因此

onAfterWrite
onBeforeWrite
都不会在任何一个对象上调用。

我以前也遇到过这个问题。我使用的解决方案不是很好,但这是唯一对我有用的方法。

我们可以做的是,当调用

getCMSFields
时,将 Packages 的 ID 列表设置为会话变量。然后,当在网格字段上添加或删除项目时,我们刷新 CMS 面板以再次调用
getCMSFields
。然后我们检索之前的列表并将其与当前列表进行比较。如果列表不同,我们可以做一些事情。

客户

class Customer extends Member {

    // ...

    public function getCMSFields() {

        // Some JavaScript to reload the panel each time a package is added or removed
        Requirements::javascript('/mysite/javascript/cms-customer.js');

        // This is the code block that saves the package id list and checks if any changes have been made
        if ($this->ID) {
            if (Session::get($this->ID . 'CustomerPackages')) {
                $initialCustomerPackages = json_decode(Session::get($this->ID . 'CustomerPackages'), true);

                $currentCustomerPackages = $this->Packages()->getIDList();

                // Check if the package list has changed
                if($initialCustomerPackages != $currentCustomerPackages) {
                    // In here is where you put your code to do what you need
                }
            }

            Session::set($this->ID . 'CustomerPackages', json_encode($this->Packages()->getIDList()));
        }

        $fields = parent::getCMSFields();

        $config = GridFieldConfig_RelationEditor::create();
        $config->removeComponentsByType('GridFieldAddNewButton');

        $packageField = GridField::create(
            'Packages',
            'Package',
            $this->Packages(),
            $config
        );
        // This class needs to be added so our javascript gets called
        $packageField->addExtraClass('refresh-on-reload');

        $fields->addFieldToTab('Root.Package', $packageField); 

        Session::set('SingleID', $this->ID);

        $this->extend('updateCMSFields', $fields);

        return $fields;
    }
}

if ($this->ID) { ... }
代码块是我们所有会话代码发生的地方。另请注意,我们向网格字段添加了一个类,以便 JavaScript 刷新工作
$packageField->addExtraClass('refresh-on-reload');

如前所述,每次在列表中添加或删除包时,我们需要添加一些 JavaScript 来重新加载面板。

cms-customer.js

(function($) {
    $.entwine('ss', function($){
        $('.ss-gridfield.refresh-on-reload').entwine({
            reload: function(e) {
                this._super(e);
                $('.cms-content').addClass('loading');
                $('.cms-container').loadPanel(location.href, null, null, true);
            }
        });
    });
})(jQuery);

if($initialCustomerPackages != $currentCustomerPackages) { ... }
代码块内,您可以执行许多操作。

您可以使用

$this->Packages()
获取与该客户关联的所有当前包裹。

您可以调用

array_diff
array_merge
来获取已添加和删除的包:

$changedPackageIDs = array_merge(array_diff($initialCustomerPackages, $currentCustomerPackages), array_diff($currentCustomerPackages, $initialCustomerPackages));
$changedPackages = Package::get()->byIDs($changedPackageIDs);

上面的代码会将此功能添加到关系的

Customer
一侧。如果您还想管理关系
Package
一侧的多对多关系,则需要向
Package
getCMSFields
函数添加类似的代码。

希望有人能提出更好的解决方案。如果没有,我希望这对你有用。


0
投票

注意:实际上并没有检查模型是否有效,但通过目视检查这应该对您有帮助:

在您提供的链接上,您正在使用

$customer = Customer::get()->Filter...

返回一个对象的 DataList,而不是单个对象,除非您指定要从 DataList 中获取的对象是什么。

如果您正在过滤客户,那么您希望从数据列表中获取特定客户,例如本例中的第一个。

$customer = Customer::get()->filter(array('ID' => $this->CustomerID))->first();

但是您应该能够通过以下方式获取单个 DataObject:

$customer = $this->Customer();

当您将客户定义为“has_one”时。如果关系是 Has Many,则使用 () 将获得对象的 DataList。

专业提示:

您不需要在 SilverStripe 中编写我们自己的调试文件。它有自己的功能。例如,

Debug::log("yay");
将输出写入文件,
Debug::dump("yay")
将其直接转储出来。

提示您可以检查您正确访问的对象是什么。

Debug::dump(get_class($customer));
将仅输出对象的类。

© www.soinside.com 2019 - 2024. All rights reserved.