Barry Barry - 7 months ago 22
PHP Question

Scaffold ListBox multiple select in ModelAdmin Filter for DataObject with Enum

Currently the automatic scaffolding for search fields where there is an enum produces a drop down only allowing one selection to be made. I'm interested in using existing filters to change this to allow multiple selections.

Given the following dataobject...

class MyDataObject extends DataObject {
static $db = array(
'Name' => "Varchar(255)",
'MyEnum' => "Enum('Option1,Option2,Option3','Option1')"
);
}


...and the following ModelAdmin...

class MyModelAdmin extends ModelAdmin {
static $mangaged_models = array(
'MyDataObject',
);
static $url_segment = 'mymodeladmin';
static $menu_title = 'MyModelAdmin';
static $menu_priority = 9;
}


...I'm looking for a module or a simple Filter of some kind to scaffold the Enum into a multiple select listbox

the multiple select listbox is defined as...


  • Allows multiple selection

  • After typing some characters suggestions are offered



And I'm asking for a generic solution - I can build a search context for each model admin but this is very frustrating.
Something like the following using either an existing filter (ExactMatchMultiFilter looks perfect but doesn't seem to actually work) or if there is one in a module or someone can suggest how to modify an existing filter for this that would be great.

class MyDataObject extends DataObject {
static $db = array(
'Name' => "Varchar(255)",
'MyEnum' => "Enum('Option1,Option2,Option3','Option1')"
);
public static $searchable_fields = array (
'MyEnum' => array('filter' => 'ExactMatchMultiFilter')
);
}


Any help is much appreciated.

Answer

From your question, it seems like you were intending that the filter you pass to the searchable field would change the scaffolding. I've done a bit of digging and that doesn't seem to be the case. However, if you used the field option instead, you can likely achieve what you want.

You do specifically mention ListboxField and while it does support multiple, it isn't enabled by the default constructor on the field which is how it would be instantiated.

What you want could be accomplished out-of-the-box a bit more by the CheckboxSetField. (I will admit, the UI is a bit average when used in ModelAdmin)

The resulting code could look something like this:

class MyDataObject extends DataObject {
    static $db = array(
        'Name'      => "Varchar(255)",
        'MyEnum'    => "Enum('Option1,Option2,Option3','Option1')"
    );
    public static $searchable_fields = array (
        'MyEnum'    => array('field' => 'CheckboxSetField')
    );
}

Unfortunately it isn't that easy, you will notice just by doing that it will come up saying "No options available" instead of a list of checkboxes. This is due to SilverStripe acting differently when we provide the field option that I mentioned earlier.

The workaround for such isn't great but is arguably still generic. I made an extension class of ModelAdmin, it looks for the CheckboxSetField in the search form and sets the Enum values for it.

class MyModelAdminExtension extends Extension {
    public function updateSearchForm($form) {

        $modelClass = $form->getController()->modelClass;

        foreach ($form->Fields() as $field) {
            if ($field->class == 'CheckboxSetField') {
                //We need to remove the "q[]" around the field name set by ModelAdmin
                $fieldName = substr($field->getName(), 2, -1);
                $dbObj = singleton($modelClass)->dbObject($fieldName);
                if ($dbObj->class == 'Enum') {
                    $enumValues = $dbObj->enumValues();
                    $field->setSource($enumValues);
                }
            }
        }
    }
}

That is a relatively safe ModelAdmin extension as it specifically looks for the combination of an Enum mapped to a CheckboxSetField which can only happen when you manually specify it.

Having gone this far, we actually could look back at the ListboxField, overcome the multiple option being disabled and populate it with values (as it would suffer the same problem mentioned above). This solution will be a little less generic as we will force all ListboxField's that were mapped from an Enum to be multiples but if we want a nicer solution, this is how we can get it.

class MyModelAdminExtension extends Extension {
    public function updateSearchForm($form) {

        $modelClass = $form->getController()->modelClass;

        foreach ($form->Fields() as $field) {
            if ($field->class == 'ListboxField') {
                //We need to remove the "q[]" around the field name set by ModelAdmin
                $fieldName = substr($field->getName(), 2, -1);
                $dbObj = singleton($modelClass)->dbObject($fieldName);
                if ($dbObj->class == 'Enum') {
                    $field->setMultiple(true);

                    $enumValues = $dbObj->enumValues();
                    $field->setSource($enumValues);
                }
            }
        }
    }
}

And for our model...

class MyDataObject extends DataObject {

    private static $db = array(
        'Name' => "Varchar(255)",
        'MyEnum' => "Enum('Option1,Option2,Option3','Option1')"
    );

    public static $searchable_fields = array (
        'MyEnum' => array('field' => 'ListboxField')
    );
}

You now have what you wanted - a multi-select ListBoxField with Enum values.

You might be asking now, why did I cover CheckboxSetField? Well, I think it is important to look at all possible solutions. I came to the solution I provided through trying the CheckboxSetField and it really was only a last minute thing where I realised with some minor modifications, I could get it working for the ListboxField.


As you raised, there is an issue for the above code handling an Enum across a HasOne relationship. This is due to the ModelAdmin extension taking the field name and treating it exclusively like a database field (via dbObject) on the model class of the form. Instead, we can detect a relationship from the dual underscores on the field name, replacing it with dot-syntax and treating that instead like a relationship (via relObject).

Our updated updateSearchForm function would look like this:

public function updateSearchForm($form) {

    $modelClass = $form->getController()->modelClass;

    foreach ($form->Fields() as $field) {
        if ($field->class == 'ListboxField') {
            //We need to remove the "q[]" around the field name set by Model Admin
            $fieldName = substr($field->getName(), 2, -1);
            $dbObj = null;

            //Check if the field name represents a value across a relationship
            if (strpos($fieldName, '__') !== false) {
                //To use "relObject", we need dot-syntax
                $fieldName = str_replace('__', '.', $fieldName);
                $dbObj = singleton($modelClass)->relObject($fieldName);
            }
            else {
                $dbObj = singleton($modelClass)->dbObject($fieldName);
            }

            if ($dbObj != null && $dbObj->class == 'Enum') {
                $field->setMultiple(true);

                $enumValues = $dbObj->enumValues();
                $field->setSource($enumValues);
            }
        }
    }
}
Comments