This bugtracker is archived (announcement). New tickets are created on See all framework issues, cms issues, and search the module listings for more specific bugtrackers.
Last modified 8 years ago Last modified on 07/07/08 16:47:27

ModelAdmin? Development Plan


  • Ingo Schommer (ingo at silverstripe dot com)
  • Sam Minnee (sam at silverstripe dot com)



In summary, ModelAdmin is an improved version of GenericDataAdmin. It is a complete rewrite that isn't backward-compatible, however the existing GenericDataAdmin? will still be bundled with SilverStripe? so that existing applications still work. ModelAdmin? itself is fairly simple, but relies on a number of new technologies:

  • Field scaffolding: Each database field and relation has
  • SearchContext?: Search forms, the queries for thier their results, can be

For usage documentation, see

Rationale for Model Admin

This is some of the thinking that led to the decision to build ModelAdmin?:

Current Shortcomings of GenericDataAdmin?

  • No tight connection from management interface to datamodel, mostly custom controller logic
  • Heavy customization in search logic
  • Manual form building (e.g. instanciation of complextablefields for relation editing)
  • Base layer not customizeable enough, mostly function overrides without re-usage
  • No support for management of multiple datatypes and hierarchies

Core Assumptions

  • Datamodels are too custom to have a common base, so we don't provide one. The module will provide best practices and copypaste examples.
  • Rich data layer that has enough information about itself and object relations to produce a useable interface "out of the box"
  • GenericDataAdmin? provides one interface for multiple DataObject? types, which should be related to each other via has_one/has_many/many_many. This has several implications on the UI:
    • Multiple "create <dataobject>" buttons at the top (or dropdown selection)
    • Verbose URLs (admin/generic/show/Member/1 instead of admin/mycustompath/show/1)
    • One search interface for each DataObject? type, and no "out-of-the-box" searching across different types (e.g. you either search for a Contact's Name or an Organisation's ZIP, but can't search for "Show me all contacts named XYZ in ZIP area 99999")
    • SearchContexts? are customizeable to allow for querying across different types
  • Its impossible to retain backwards compatibility to the current GDA implementation
  • Q: How extensible do we want to make the search->result "widget"? It would be useful for TableListField? searching, but might turn out to be too complicated to be useful

Features: Datamodel

Searchable Fields

  • Implement DataObject::$searchable_fields and DataObject::getCMSSearchFields()
    • (Sam) Instead of getCMSSearchFields(), we have defaultSearchContext(), that returns a SearchContext? object - a combination of a set of search fields an instructions on how to use them. This being the case, we may as well drop DataObject::$searchable_fields altogether, and define our search fields simply by implementing defaultSearchContext(). Because it's a PHP function, it's much more flexible for defining search information. Here's a simple example, but it could be much more complex.
      function defaultSearchContext() {
        return new SearchContext(
  • Scaffold searchform based on these fields
  • Optional: Default mapping between DBField types and SearchFilter?
  • Optional: also evaluated in our fulltext site search
  • Optional: allow for "dot notation" in searchable fields


  • Main goal: Abstract search<->result behaviour away from the view, should be pluggable to TableListField?, Report Views and GDA results
  • SearchContext? acts on one or more DataObject? types and auto-generates a SQLQuery with the correct joins and fields
  • Query should be customizeable either by adding custom search-filters, or modifying the query-object before getting results
  • $searchable_fields have a default mapping to a SearchFilter? based on their DBField type (e.g. "Date" class would map to a "DateSearchFilter?" and render with the "DateField?" FormField? subclass)
  • Responsible for processing client input through $_REQUEST (or api call?)
    • (Sam) Don't access $_REQUEST directly; $searchParams should be an argument to getResultsQuery().
  • Responsible for paging, as SearchContext? handles the query object
  • Have most of the sql coming from the SQLQuery object, which is the starting point for any DB abstraction layer
  • We don't allow for multiple "views" of searchable fields by default, e.g. an extended searchform for GDA, and a shortened form for tablelistfield - but we'll make it easy to customize the default output.
  • Q: Does SearchContext? return a query object or a set of data to the view?
    • (Sam) Both: let's have two access methods - getResultsQuery() AND getResults(). The latter would obviously call the former. The key to flexibility is being able to access things in multiple ways. :-)


Formfield Scaffolding

  • Default implementation of DataObject::getCMSFields()
  • Separate Tab for each has_many/many_many object relation, containing a ComplexTableField? subclass
  • Optional: Hook a refactored RequiredFields? validator to a model knowing its own $required_fields
  • Q: Do we explicitly support validation? some validation routines are flaky when used in the backend
    • (Sam) Validation in the admin should work. We may need to wait until after the JS upgrade to do this.
  • Q: Conceptually it would be nice to have relations represented as an object as well (see Django), so we can treat all "editable" properties of an object equally

Features: GenericDataAdmin?

  • Port useful features down from CRMAdmin and deprecate it
  • Leave access control for search/display/saving for later
  • Use TableListField? as result display (versatile formatting options, integrated CSV export)
  • GDA needs to be able to construct a default edit link for all objects it manages (currently its a single GenericDataAdmin?->getLink() method)
  • Q: We manage more than one type on GDA, should result display be customizeable on DataObject? rather than GDA implementation?
  • Q: Should actions for a form like "save", "delete", "publish" be handled by DataObject? or GDA?

Features: UI

  • Editor for many_many extrafields (build on Romains work?)
  • Better editor for has_many/many_many relations (autocomplete instead of big list with checkboxes) - possibly as a CTF module

CRM Development Plan

Features: CommunicationForm?

Code Mockups

DataObject? $searchable_fields

class DataObject extends ViewableData {
	function getCMSSearchFields() {
		$fields = new FieldSet();
		foreach(self::$searchable_fields as $fieldName) {
			// would generate automatically for Organisation subclass:
			//new DateRangeSearchFilter('Created'),
			//new TextSearchFilter('Title'),
			//new MultiselectSearchField('Type'),

		return $fields;

class Organisation extends DataObject {
	static $db = array(
		'Title' => 'Varchar',
		'Street' => 'Text',
		'Type' => 'Enum("a,b,c")',
		'HasAffiliates' => 'Boolean',
	static $searchable_fields = array(
	function getCMSSearchFields() {
		$fields = parent::getCMSSearchFields();
		$fields->replaceByName('Type', new TextSearchFilter('Type'));

		return $fields;
	function getCMSSearchFilters() {

DBField extensions

class DBField extends Object {
	static $default_scaffold_formfield = "TextField";
	static $default_search_formfield = "TextField";
	static $default_search_filter = "TextSearchFilter";


class SearchFilter extends Object {
  function __construct($name) {
    $this->name = $name;

  function updateQuery($resultSet, $tableName, SQLQuery &$query) {
    $SQL_val = Convert::raw2sql($resultSet[$this->name]);
    $query->addFilter("`$tableName`.`$this->name` = '$SQL_val'");


// TODO is this really a controller?
class SearchContext extends Controller {
   function __construct($controller, $name, $fields, $filters) {


   function getQuery($searchFilters) {
     $q = new SQLQuery("*","Member");
     return $q;
   function getResults($searchFilters, $start, $limit) {
     $q = $this->getQuery($searchFilters);
     $q->limit = $start ? "$start, $limit" : $limit;
     $output = new DataObjectSet();
     foreach($q->execute() as $row) {
       $className = $row['RecordClassName'];
        $output->push(new $className($row));
     // do the setting of start/limit on the dataobjectset
     return $output;

   function processFilters($searchFilters, SQLQuery &$query) {
     foreach($this->filters as $filter) {
       $filter->updateQuery($searchFilters, $tableName, $query);