【问题标题】:Getting server error when trying to use GridField to create relationship between DataObjects尝试使用 GridField 创建 DataObjects 之间的关系时出现服务器错误
【发布时间】:2014-03-16 01:23:40
【问题描述】:

我正在通过创建一个小型网站来学习 SilverStripe,让用户管理他们的香水(即香水/古龙水)。用户添加成分(用于他们拥有的香精中),然后添加他们的香精,此时他们选择要添加的香精中的成分。

我创建了成分和香精类,它们都扩展了 DataObject。我还创建了 IngredientsPage 页面,允许用户添加/编辑/删除成分(由名称和描述组成)并列出到目前为止添加的所有成分,并且该页面功能齐全。我现在正在尝试创建 FragrancesPage 页面,该页面将允许用户添加/编辑/删除香水(由名称、描述和成分组成)并列出迄今为止添加的所有香水,但我遇到了一些麻烦。

我所知道的在香味和成分之间建立关系的唯一方法(一种香味有多种成分,一种成分属于多种香味)是使用 GridField(如果有更好的方法,请告诉我!),因为这是 SilverStripe 教程让您做的事情(尽管在教程中它是针对 CMS 而不是前端的)。但是,一旦我尝试将 GridField 添加到组合中,我就会进入一个错误页面,上面写着“服务器错误:抱歉,处理您的请求时出现问题。”。

我的代码如下。

成分.php:

<?php

class Ingredient extends DataObject {
    private static $db = array(
        'Name' => 'Text',
        'Description' => 'Text'
    );

    private static $belongs_many_many = array(
        'Fragrances' => 'Fragrance'
    );
}

?>

Fragrance.php:

<?php

class Fragrance extends DataObject {
    private static $db = array(
        'Name' => 'Text',
        'Description' => 'Text'
    );

    private static $many_many = array(
        'Ingredients' => 'Ingredient'
    );
}

?>

FragrancesPage.php:

<?php

class FragrancesPage extends Page {
    private static $icon = 'cms/images/treeicons/reports-file.png';
    private static $description = 'Fragrances page';
}

class FragrancesPage_Controller extends Page_Controller {
    private static $allowed_actions = array('FragranceAddForm');

    function FragranceAddForm() {
        $config = GridFieldConfig_RelationEditor::create();
        $config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
            'Name' => 'Name',
            'Ingredient.Name' => 'Ingredient'
        ));

        $fragrances_field = new GridField(
            'Ingredients',
            'Ingredient',
            $this->Ingredients(),
            $config
        );

        $fields = new FieldList(
            new TextField('Name', 'Fragrance Name'),
            new TextareaField('Description', 'Fragrance Description'),
            $fragrances_field
        );

        $actions = new FieldList(
            new FormAction('doFragranceAdd', 'Add Fragrance')
        );

        $validator = new RequiredFields('Name', 'Description');

        return new Form($this, 'FragranceAddForm', $fields, $actions, $validator);
    }

    public function doFragranceAdd($data, $form) {
        $submission = new Fragrance();
        $form->saveInto($submission);
        $submission->write();

        return $this->redirectBack();
    }

    public function FragranceList() {
        $submissions = Fragrance::get()->sort('Name');

        return $submissions;
    }
}

?>

如果我从 FragrancesPage.php 中删除与 GridField 相关的所有内容,则该页面可以正常工作。我似乎无法让 GridField 工作,并且不知道有任何其他方法可以在前端创建 Fragrances 和 Ingredients 之间的关系。如果 IngredientsPage.php 的代码也有帮助,请告诉我,我会添加它。

【问题讨论】:

    标签: silverstripe


    【解决方案1】:

    我的猜测是您关闭了错误报告,这就是为什么您只看到这样一个含义较少的错误消息。
    你应该在php.ini.htaccess_ss_environment.php 中打开display_errorserror_reporting(重要提示:在_config.php 中设置它不起作用,因为它会被错误处理程序覆盖)

    我在您的代码中看到的问题是尝试在FragrancesPage 上使用$this-&gt;Ingredients(),但据我所知,只有Fragrances 类有一个方法Ingredients(该方法是“神奇地”为many_many 关系)。
    另外,我认为你的setDisplayFields()

    所以基本上你需要使用$fragrance-&gt;Ingredients() 而不是$this-&gt;Ingredients()
    但这导致我们遇到下一个问题:您还没有香水。
    不幸的是,此时,GridField 仅在您已有对象时才有效。这意味着您必须将其拆分为两种形式或使用另一种形式。


    选项 1: 使用 CheckboxSetField 管理 many_many 关系。

    这将不允许动态创建成分,它只会为您提供可以勾选以链接项目的复选框。

    public function FragranceAddForm() {
        $fragrances_field = new CheckboxSetField('Ingredients', 'Ingredient', Ingredient::get()->map());
    
        $fields = new FieldList(
            new TextField('Name', 'Fragrance Name'),
            new TextareaField('Description', 'Fragrance Description'),
            $fragrances_field
        );
    
        $actions = new FieldList(
            new FormAction('doFragranceAdd', 'Add Fragrance')
        );
    
        $validator = new RequiredFields('Name', 'Description');
    
        return new Form($this, __FUNCTION__, $fields, $actions, $validator);
    }
    
    public function doFragranceAdd($data, $form) {
        $submission = new Fragrance();
        $form->saveInto($submission);
        $submission->write();
    
        return $this->redirectBack();
    }
    

    选项 2: 以第二种形式使用 GridField

    这将允许动态创建成分,但工作量更大。你可能会在使用 GridField 时遇到一些麻烦,因为它还没有在前端进行全面测试。

    (最近有一个问题,我写了一点关于前端的 GridField 问题https://stackoverflow.com/a/22059197/1119263
    我想这个地方和任何地方一样好,可以最终为前端 GridFields 编写教程/工作示例。
    我冒昧地重构了您的代码以包含编辑功能,在编辑页面上使用 GridField 比在单独的表单上要好得多。

    如前所述,GridField 在前端工作得不是很好,有一个模块可以减轻痛苦,但它的边缘仍然很粗糙,需要你做一些造型来让它看起来很漂亮。 在 PackagistGitHub 上找到模块 (你需要

    class Ingredient extends DataObject {
        private static $db = array(
            'Name' => 'Text',
            'Description' => 'Text'
        );
    
        private static $belongs_many_many = array(
            'Fragrances' => 'Fragrance'
        );
    
        public function getCMSFields() {
            // fields used by the GridField, don't let the CMSFields mislead you
            return new FieldList(
                TextField::create('Name', 'Name'),
                TextAreaField::create('Description', 'Description')
            );
        }
    }
    
    class Fragrance extends DataObject {
        private static $db = array(
            'Name' => 'Text',
            'Description' => 'Text'
        );
    
        private static $many_many = array(
            'Ingredients' => 'Ingredient'
        );
    }
    
    /**
     * Form in a separate class, so we can reuse it.
     * @param Controller $controller
     * @param string $name
     * @param Null|Fragrance $fragrance Either null to create a new one, or pass an existing to edit it and add Ingredients
     * @return Form
     */
    class FragranceForm extends Form {
        public function __construct($controller, $name, $fragrance = null) {
            if ($fragrance && $fragrance->isInDB()) {
                // we can only use a GridField if the object exists and has already been saved
    
                // gridfield needs jQuery
                Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.min.js');
                // ensure we don't have 2 versions of jQuery
                Requirements::block(THIRDPARTY_DIR . '/jquery/jquery.js');
                $config = FrontEndGridFieldConfig_RelationEditor::create();
                $config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
                    'Name' => 'Name',
                    'Description' => 'Description',
                ));
                $ingredientField = new FrontEndGridField(
                    'Ingredients',
                    'Ingredient',
                    $fragrance->Ingredients(),
                    $config
                );
            } else {
                $ingredientField = new LiteralField('Ingredients', '<p>Ingredients can be added after saving</p>');
            }
            $fields = new FieldList(
                new HiddenField('ID', ''),
                new TextField('Name', 'Fragrance Name'),
                new TextareaField('Description', 'Fragrance Description'),
                $ingredientField
            );
    
            $actions = new FieldList(
                new FormAction('doFragranceSave', 'Save Fragrance')
            );
    
            $validator = new RequiredFields('Name', 'Description');
    
            // populate the fields (ID, Name and Description) with the values from $fragrance. This does not effect the GridField
            if ($fragrance && $fragrance->exists()) {
                $fields->fieldByName('ID')->setValue($fragrance->ID);
                $fields->fieldByName('Name')->setValue($fragrance->Name);
                $fields->fieldByName('Description')->setValue($fragrance->Description);
                // there is actually a method for that, but we can't use it here,
                // because fields are not set yet. we could do it after __construct, but then we would
                // overwrite things set by the error handler, so lets just do it by hand
                // $this->loadDataFrom($fragrance);
            }
            parent::__construct($controller, $name, $fields, $actions, $validator);
        }
    
        public function doFragranceSave($data, $form) {
            if (isset($data['ID']) && $data['ID']) {
                $id = (int)$data['ID'];
                $fragrance = Fragrance::get()->byID($id);
            }
            if (!isset($fragrance) || !$fragrance || !$fragrance->exists()) {
                // if the ID was invalid or we don't have one, create a new Fragrance
                $fragrance = new Fragrance();
            }
            $form->saveInto($fragrance);
            $fragrance->write();
    
            // redirect to the edit page.
            $controller = $this->getController();
            $editLink = $controller->EditLink($fragrance->ID);
            return $controller->redirect($editLink);
        }
    }
    
    class FragrancesPage extends Page {
    
    }
    
    class FragrancesPage_Controller extends Page_Controller {
        private static $allowed_actions = array(
            'edit',
            'AddForm',
            'EditForm',
        );
    
        /**
         * the default action
         * @return ViewableData_Customised
         */
        public function index() {
            // $this->customise() lets you overwrite variables that you can use in the template later.
            return $this->customise(array(
                // set the AddForm to $Form instead of $AddForm, this way you can use $Form in template and can reuse the template
                'Form' => $this->AddForm(),
            ));
        }
    
        /**
         * edit action to edit an existing Fragrance
         * links will look like this /FragrancesPage/edit/$ID
         *
         * @param SS_HTTPRequest $request
         * @return SS_HTTPResponse|ViewableData_Customised
         */
        public function edit(SS_HTTPRequest $request) {
            $id = (int)$request->param('ID');
            $fragrance = Fragrance::get()->byID($id);
            if (!$fragrance || !$fragrance->exists()) {
                // fragrance not found? display a 404 error page
                return ErrorPage::response_for(404);
            }
            // now that we have a $fragrance, overwrite EditForm with a EditForm that contains the $fragrance
            $form = $this->EditForm($fragrance);
            $return = $this->customise(array(
                // also overwrite Title and Content, to display info about what the user can do here
                // if you don't overwrite that, it will display the Title and Content of the page
                'Title' => 'Edit: ' . $fragrance->Name,
                'Content' => '<p>you are editing an existing fragrance</p>',
                // set the Form to $Form instead of $EditForm, this way you can use $Form in template and can reuse the template
                'Form' => $form,
            ));
            // per default SilverStripe will try to use the following templates: FragrancesPage_edit.ss > FragrancesPage.ss > Page.ss
            // if you want to use a custom template here, you can specify that with ->renderWith()
            // but you probably won't need that anyway
            // $return = $return->renderWith(array('MyCustomTemplateName', 'Page'));
            return $return;
        }
    
        public function AddForm() {
            return new FragranceForm($this, __FUNCTION__);
        }
    
        public function EditForm($fragranceOrRequest = null) {
            // unfortunately, GridField / FormFields in general are a bit clumsy and do forget what item they where
            // suppose to edit, so we have to check what $fragranceOrRequest is and set/get the fragrance to/from session
            if ($fragranceOrRequest && is_a($fragranceOrRequest, 'Fragrance')) {
                $fragrance = $fragranceOrRequest;
                Session::set('FragrancesPage.CurrentFragrance', $fragrance->ID);
            } else {
                $fragrance = Fragrance::get()->byID(Session::get('FragrancesPage.CurrentFragrance'));
            }
            if (!$fragrance || !$fragrance->exists()) {
                // that's bad, some error has occurred, lets display an ugly 404 page
                return $this->httpError(404);
            }
            return new FragranceForm($this, __FUNCTION__, $fragrance);
        }
    
        public function EditLink($ID) {
            return $this->Link("edit/$ID");
        }
    }
    

    作为仍在学习 SilverStripe 的人,我知道有很多东西需要吸收,如果您有任何问题,请随时发表评论或在 IRC 上戳我

    【讨论】:

    • 感谢您的深入回答!我会试一试,看看效果如何。
    • 好的,所以第一个选项有同样的问题,SilverStripe 只会显示一个错误页面。然而,第二种选择似乎几乎奏效了。没有错误页面,但在为新香水命名/描述并保存后,我最终得到:i.imgur.com/l6apbZo.png。 GridField 的样式不是问题(易于修复),问题是当输入我创建的成分时,什么也没有发生——没有与我迄今为止输入的成分匹配的下拉列表,所以我无法将成分与香水联系起来。有什么想法吗?
    • 哦,另外,单击左侧的“添加成分”按钮会将我带到 [站点根目录]/fragrances/EditForm/field/Ingredients/item/new,这是一个空白页面。我不确定这是否是有效的,但我已经有一个用于添加有效成分的表格,所以如果需要我可以隐藏这个按钮。
    • 因为选项 2 是一个非常广泛的例子,所以我对它进行了测试。这一切都对我有用。您基本上应该能够复制粘贴代码并且它应该可以工作。空白页听起来像是隐藏的错误,你开启报错和显示错误了吗?
    • 另外,我在选项 1 中发现了我的错误。使用 $this-&gt;Ingredients 而不是 Ingredient::get()。修好了。
    猜你喜欢
    • 2021-01-19
    • 2021-08-28
    • 2015-01-15
    • 2013-03-04
    • 1970-01-01
    • 1970-01-01
    • 2017-09-05
    • 1970-01-01
    • 2022-09-29
    相关资源
    最近更新 更多