RecordBrowser lets you deploy highly-customizable sets of records quickly. It automatically provides browse, search, view, and edit interfaces, favorites and recent records, full edit history, and a range of field types. It also simplifies working with the Watchdog module.
You may also be interested in Utils/RBO, an object-oriented wrapper around this same engine.
Company Namecompany_nameTo create a custom recordset, we recommend giving it its own module — it isn't strictly required, but this page assumes that setup.
Once your module exists, define the recordset's installation in your ModuleInstall.php's install() method.
First, define the recordset's fields. Pass them as an array to install_new_recordset() in Utils_RecordBrowserCommon. Each element is a field definition — an array of property_name => value pairs. Two properties are required: name and type. Which other properties apply depends on the type; see the field type tables below.
Example:
$field1 = array('name' => 'Field 1', 'type' => 'text');
$field2 = array('name' => 'Field 2', 'type' => 'date');
$fields_definition = array($field1, $field2);
Utils_RecordBrowserCommon::install_new_recordset('my_recordset', $fields_definition);
Or more compactly:
Utils_RecordBrowserCommon::install_new_recordset('my_recordset',
array(
array('name' => 'Field 1', 'type' => 'text'),
array('name' => 'Field 2', 'type' => 'date')
));
Once the recordset exists, you can configure it further:
Quickjump — enables GenericBrowser's quickjump feature for a given field:
Utils_RecordBrowserCommon::set_quickjump($recordset_name, $field);
Favorites — enables or disables favorites for this recordset:
Utils_RecordBrowserCommon::set_favorites($recordset_name, $bool);
Recent — enables or disables the "recent" list. When enabling, $amount should be the number of entries to keep:
Utils_RecordBrowserCommon::set_recent($recordset_name, $amount);
Caption — sets the ModuleIndicator caption shown across browse/view/edit/history windows. This string goes through translation automatically — don't wrap it in a Lang call yourself:
Utils_RecordBrowserCommon::set_caption($recordset_name, $caption);
Access callback — determines record-level access per user group. See Access management below.
Utils_RecordBrowserCommon::set_access_callback($recordset_name, $callback);
Processing callback — registers pre/post-processing on view/edit/delete/restore. See Pre/post-processing below.
Utils_RecordBrowserCommon::register_processing_callback($recordset_name, $callback);
Watchdog — enables Watchdog integration for this recordset. See Watchdog integration below.
Utils_RecordBrowserCommon::enable_watchdog($recordset_name, $callback);
true, RecordBrowser raises a QuickForm error if the user submits the form with this field empty. Doesn't apply to checkbox, hidden, or calculated types. A super administrator can change this later. Be careful combining this with JavaScript that conditionally hides the field — a required-but-hidden field can leave the user unable to submit the form at all.true, the field appears by default as a column in table (browse) view. Doesn't apply to hidden type. (Expect this to change with user-defined views.)true by default. Controls whether a super administrator can change the field's name, type, and other properties through the UI. Non-extra fields are treated as core to the system and can't be edited that way.true, enables filtering by this field while browsing.QFfield_callback, since values set here are available inside that callback. See the field type tables below for details on each type's param.<select>/autocomplete. If set, RecordBrowser does no additional processing for that field — you're responsible for setting defaults and the required rule yourself. Never required.calculated type.| Name | QF Type | DB Type | Description | Param |
|---|---|---|---|---|
| text | <input type="text"> |
C(param) |
Holds a string value. HTML entities are escaped. The field's length must be defined. | Length of the field. |
| long text | <textarea> |
X |
Holds a note (long string value). HTML entities are escaped, with BBCode support. Because attachments are added to nearly every recordset, this field is capped at 400 characters (not counting BBCode tags). | none |
| integer | <input type="text"> |
I4 |
Holds an integer value. | none |
| float | <input type="text"> |
F |
Holds a float value. | none |
| checkbox | <input type="checkbox"> |
I1 |
Holds a boolean value: true for checked, '' (empty string — the same empty value every type uses) for unchecked. |
none |
| calculated | none | depends on param | Holds no value by default — it's read-only, meant to show the user derived information. display_callback is required. If needed, the data type can be set via param (usually modified from a processing callback). |
DB data type, or omit for no DB representation. Use Utils_RecordBrowserCommon::actual_db_type($type, $param) to get the correct type. |
| date | <input type="text"> |
D |
A date field with a pop-up calendar button. | none |
| timestamp | <input type="text"> for the date, <select> for the hour |
T |
A date-and-time field with a pop-up calendar. Time is chosen via HTML <select> elements. Timezone conversion is automatic for both display and update. |
none |
| currency | <input type="text"> for the value, <select> for the currency |
C(128) |
Holds a value and a currency, chosen via an HTML <select>. Stored as float_value__currency_id. See Utils_CurrencyField for details. |
none |
| select | <select><option> |
I |
Holds the key of a selected value. Links to another recordset — record IDs are always used as keys. Param is essential here. | See Select/multiselect parameter below. |
| multiselect | <select multiple="1"><option> |
X |
Holds keys of selected values. Links to another recordset or a CommonData set. Param is essential here. This is the only field type that returns and accepts an array value, rather than a single value. | See Select/multiselect parameter below. |
| commondata | <select><option> |
X |
Holds the key of a selected value. Links to another recordset, with record IDs always used as keys. Param is essential here. Supports ChainedSelect. | Must be an array. order_by_key (true) orders by key. Followed by an optional list of elements for the ChainedSelect chain. The last element must be the name of the CommonData table. |
| page_split | none | none | A delimiter that splits the form into tabs — fields after a page split appear under a new tab. | Number of columns for the tab's layout. Default is 2. |
RecordBrowser also supports custom data types. A custom type is really just a pre-processing function: it accepts a field definition and returns a modified one, adding a QFfield_callback, adjusting param, and mapping the type back to one RecordBrowser understands.
The CRM/Contacts module defines two such types, crm_contact and crm_company. Both can be either a select or multi-select field for choosing contacts. param should be an array with keys field_type ('select' or 'multiselect') and crits (a callback to a static method on the module's Common class).
crm_contact supports two more features. First, param can include a format key holding a callback to a static method, used to format the value in both edit and view mode. The two most common are:
'format' => array('CRM_ContactsCommon', 'contact_format_default')
// John Doe [Some Company]
and
'format' => array('CRM_ContactsCommon', 'contact_format_no_company')
// John Doe
Second, you can replace the multiselect element with an auto-multiselect (an autocomplete/multiselect combo). This is rougher around the edges: to enable it, your crits callback must return true when its first argument is false, and return the actual crits when the first argument is true. This interface is expected to change.
Select and multiselect param values are more involved. To link to another recordset, param has three parts, separated by ;. The 2nd and 3rd parts are optional.
Part 1:
RecordSet name::Fields list with | as delimiter
Which recordset this field refers to. List multiple fields separated by spaces, in the order they should be used; these fields also determine which columns are searched when a user searches by this select/multiselect field.
Part 2:
ModuleCommon::crits_method
A callback that returns crits to limit which records can be selected. If omitted, all records are available.
Part 3:
ModuleCommon::advanced_properties_method
A callback that returns an array of additional properties for record selection. Recognized (all optional) keys are order, cols, and format_callback. This third part is optional — most fields won't need it.
Example param string for a select field linking to the premium_projects recordset:
'param' => 'premium_projects::Project Name;'.
'Premium_Projects_TicketsCommon::projects_crits;'.
'Premium_Projects_TicketsCommon::projects_advanced'
Usually you define all of a recordset's fields in install_new_recordset(). Sometimes, though, you need to add a field to a recordset that's installed in a different module. Rather than editing that module's code directly, use new_record_field() from RBCommon.
Its first argument is the recordset name, its second a field definition — in almost the same format used by install_new_recordset() (which takes an array of field definitions, while new_record_field() takes just one).
The difference is that new_record_field() also accepts a position key. Give it an integer to place the field at that exact position, pushing later fields (and page splits) forward without breaking the layout. Give it a string — the name of an existing column — to place the new field right after that column.
Note: access management is due for a major overhaul. The input/output structure will change to make achieving specific field behaviors simpler.
RecordBrowser lets you set different access levels for every action performed on a record. You can restrict which groups may browse a recordset, limit which records a user sees, and control who can edit which record.
All of this goes through the access callback, defined in your module's installation procedure:
Utils_RecordBrowserCommon::set_access_callback($recordset_name, $callback);
RecordBrowser calls this callback whenever it needs to check access, passing up to 3 arguments. The first is always the action being performed — add, browse, edit, delete, view, or fields. For the first 4, return true or false to grant or deny access. view expects an array of crits used to filter which records are shown. For edit and delete, the record itself is passed as the 2nd argument.
The fields action is more involved, and is due to be deprecated soon, so it isn't covered here.
RecordBrowser lets you run your own logic around every action it performs — modifying fields, or updating another recordset — through a processing callback. Register one in your module's install() method:
Utils_RecordBrowserCommon::register_processing_callback($recordset_name, $callback);
Each recordset can have any number of processing callbacks; all of them run, in order, on every action. A processing callback can do both pre- and post-processing. Each call passes your method 3 arguments:
adding), or a pair of records (clone)The action determines both what you receive as the first argument and what a valid return value looks like. Be careful: if the action expects an updated record back and you return nothing (array(), false, null), RecordBrowser treats that as a valid update — which empties every field in the record. The table below covers every action:
| Action | Data (1st param) | Expected result | Notes |
|---|---|---|---|
| adding | defaults | new defaults | Lets you influence defaults for a newly-created record, or run JavaScript. |
| editing | record | updated record | Lets you influence the record right before the edit form is displayed. Changing values here doesn't affect the record until the user saves. |
| view | record | updated record | Lets you influence the record right before it's displayed. Changing values here doesn't affect the record. Only fires when a user views a record — not in browse mode. |
| add | record | updated record | Fires each time a new record is about to be created, including direct new_record() calls. At this point the record has no ID yet — use added if you need it. |
| added | record | none | Fires each time a new record is created, including direct new_record() calls — after the record exists, so it has an ID. To modify the record here, call update_record() explicitly. |
| edit | record | updated record | Fires each time a record is edited, including direct update_record() calls. |
| clone | array('original' => $old, 'clone' => $new) |
none | Fires after a record is cloned. |
| delete | record | none | Fires on delete, after the record is marked as deleted. |
| restore | record | none | Fires on restore, after the record is marked as deleted. |
RecordBrowser makes it easy to hook into the Watchdog module. First, enable it by supplying a callback that will process Watchdog requests, in your module's install() method:
Utils_RecordBrowserCommon::enable_watchdog($recordset_name, $callback);
This callback is usually fairly involved, since it needs to supply everything Watchdog needs. RecordBrowser's built-in helper handles most of it:
public static function watchdog_label($rid = null, $events = array(), $details = true) {
return Utils_RecordBrowserCommon::watchdog_label(
'task',
Base_LangCommon::ts('CRM_Tasks', 'Tasks'),
$rid,
$events,
'title',
$details
);
}
Arguments:
$details, passed through from the 3rd argumentThe 5th argument can also be a callback: it receives the record as its first argument and should return the label string to use. Example:
public static function watchdog_label($rid = null, $events = array(), $details = true) {
return Utils_RecordBrowserCommon::watchdog_label(
'premium_tickets',
Base_LangCommon::ts('Premium_Projects_Tickets', 'Tickets'),
$rid,
$events,
array('Premium_Projects_TicketsCommon', 'watchdog_label_format'),
$details
);
}
public static function watchdog_label_format($r) {
return $r['ticket_id'] . ': ' . $r['title'];
}
RecordBrowser exposes a handful of methods for creating and retrieving records without touching its underlying table structure directly.
new_record()
The most basic method:
Utils_RecordBrowserCommon::new_record($record_set, $values);
Creates a new record. The add and added processing actions still apply. Only fields defined through install_new_recordset() or new_record_field() are accepted — you can't set the ID or other record metadata directly. Returns the new record's ID.
update_record()
Utils_RecordBrowserCommon::update_record($record_set, $id, $values, $full_update = false, $date = null, $dont_notify = false);
The first three arguments are the recordset name, the record's ID, and an associative array of values to update. $full_update controls whether unspecified fields keep their old value (false) or get cleared (true). $date lets you override the "edited on" date. $dont_notify, when true, skips sending an event to the Watchdog module — useful for minor updates.
get_record()
Utils_RecordBrowserCommon::get_record($record_set, $id, $htmlspecialchars = true);
Retrieves a single record. Returns an array on success, null otherwise. The first argument is the recordset name, the second the record's ID. The optional third argument, when false, skips htmlspecialchars() escaping of the returned values. Note that this method doesn't check whether the record is active (i.e. not deleted) — it does, however, include an active key indicating its state.
get_records()
Utils_RecordBrowserCommon::get_records($record_set, $crits = array(), $cols = array(), $order = array(),
$limit = array(), $admin = false);
The most involved of the four. $record_set tells RecordBrowser which recordset to query; by default, only active (non-deleted) records are returned.
$crits is the filter: an array where each key is a column key (optionally with modifiers) and each value is what the record must contain in that field. If a value is itself an array, it's treated as alternatives — the record matches if it has any of those values in that field.
Each key/value pair is one criterion, and by default criteria are joined with AND — a record must satisfy all of them. For example, in CRM/Contacts:
$crits = array('first_name' => array('John', 'Tom'), 'last_name' => 'Doe');
This selects every record where first_name is 'John' OR 'Tom', AND last_name is 'Doe'. John Doe matches; Jeremy Doe doesn't.
In that example, key order doesn't matter — but with modifiers, it can. Modifiers go at the start of the key string:
| Modifier | Description |
|---|---|
: |
Filter by an internal field: id, Fav, Recent, Created_on, Created_by, or Edited_on (case-sensitive). Fav and Recent are booleans for whether the current user has the record saved as a favorite / in their recents. |
! |
Negation — inverts the condition. With an array value, matches records where the field is none of the listed values. |
" |
Disables automatic quote-escaping for this value when building the SQL query. Useful for LIKE queries built with DB::Concat(). Caution: misuse of this modifier can open the door to SQL injection. |
<, >, <=, >= |
Comparison operators, valid for string, integer, date, and timestamp fields. array('<start_date' => '2009-01-01') means start_date must be earlier than 2009-01-01. |
~ |
Valid for string fields — replaces = with a case-insensitive LIKE. Typically combined with ", e.g. array('"~last_name' => DB::Concat(DB::qstr('%'), DB::qstr('foo'), DB::qstr('%'))), meaning last_name must contain "foo" (Foobar, Barfoobar, BarFOO). The " modifier is needed here because escaping the result of DB::Concat() would break the query. |
( |
Opens an OR group — see below. |
\| |
Adds another element to the current OR group, starting one if none is open. See below. |
The ( and | modifiers are the least intuitive of the set, and order matters once they're involved. Say you want everyone whose first name is John or whose last name is Doe:
array('(last_name' => 'Doe', '|first_name' => 'John')
or equivalently:
array('(first_name' => 'John', '|last_name' => 'Doe')
Both give the same result. In each, ( opens the OR group and | extends it — every element in the chain satisfies the whole group if it matches. To add a third condition (say, anyone from New York), add another | element:
array('(first_name' => 'John', '|last_name' => 'Doe', '|city' => 'New York')
Why distinguish ( from | at all? Because sometimes you need (cond_1 OR cond_2) AND (cond_3 OR cond_4). Use ( to open a new group for cond_1 and cond_3 — RecordBrowser will recognize that cond_3 starts a second OR group.
Two things to keep in mind:
(, even if you don't think anything else will be ANDed with it — you may not know about other criteria it'll end up combined with (crits from a view access callback, for instance).Any element without a | modifier breaks the current OR chain. So:
array('(first_name' => 'a', 'city' => 'b', '|last_name' => 'c')
doesn't contain a real OR chain at all — city breaks it, leaving two unrelated one-element groups. Swapping city and last_name would fix that.
These three sections are stubs with no content in the source page — flagged during the August 2026 documentation review.