Skip to main content
Version: 4.0 preview

Childentries

Version
4.0 preview
Updated
View markdown

Embeds another model's list view inside this record's edit form, so the child records can be listed, added, edited and deleted without leaving the parent. The question form's answers list, the product form's detail panes, the configuration screen's currencies and tax classes are all this.

Storage kind: derived · Column: none

It stores nothing. What it contributes is a rendered child list plus two behaviours the parent needs: child records are copied when the parent is copied, and they are carried when the parent is exported.

Settings

SettingMeaning
viewClassRequired. The child's list view class, e.g. 'ConfigboxViewAdminanswers'
viewPathOptional explicit path to that view, for a view outside the usual lookup
viewFiltersWhich child records to show — array of array('filterName' => …, 'filterValueKey' => …)
foreignKeyFieldThe child property holding the foreign key back to this record
parentKeyFieldThe parent property holding the key that fills it — normally 'id'
labelUsed in the "save the record first" message
hideAdminLabelUsually true — the embedded list brings its own heading
copyWithParentfalse keeps the child records out of a copy of the parent — see below
deleteWithParentfalse keeps them out of a cascading product delete — see below
exportWithParentfalse keeps them out of a transfer package — see below

A parent/child pair, from adminquestions:

$propDefs['answers'] = array(
'name' => 'answers',
'label' => KText::_('Answers'),
'type' => 'childentries',
'viewClass' => 'ConfigboxViewAdminanswers',
'viewFilters' => array(
array('filterName' => 'answers.question_id', 'filterValueKey' => 'id'),
),
'foreignKeyField' => 'question_id',
'parentKeyField' => 'id',
'positionForm' => 27500,
'appliesWhen' => array(
'question_type' => array('checkbox', 'radiobuttons', 'dropdown', 'images'),
),
);

filterName is a filter the child model's getRecords() understands — conventionally <child model name>.<column>. filterValueKey names the property on the parent record whose value is passed. foreignKeyField and parentKeyField are what pre-fill the parent reference when the operator clicks "add" in the embedded list, so a new child arrives already pointing at the right parent.

The two shapes

A true parent/child list — the case above. viewFilters narrows the child list to this parent's records, and parentKeyField/foreignKeyField wire up the add form.

A plain embedded listviewFilters empty, foreignKeyField and parentKeyField empty strings. The configuration screen uses this to put the currencies, tax classes, salutations and customer-group lists on tabs of one form. There is no parent relationship at all; the property is simply a way to place a whole list view in a form.

An unsaved parent shows nothing

When parentKeyField is declared and the parent record has no value for it — a record being created — the template renders "Save the record to add <label>." instead of the list. It has to: a child needs a parent id to point at, and there is not one yet. Expect it, and do not read it as a broken view.

Copying recurses

copy() loads the child records through viewFilters (substituting the old parent id), sets foreignKeyField on each to the new parent id, and calls the child model's copy(). That is the recursion path that makes copying a product bring its pages, and a product list bring its entries — and it means a child that refuses to copy aborts the whole copy, exception and reason intact, rather than leaving a half-copied tree.

A cycle in the parent/child graph would recurse forever. Do not point a childentries at a view whose model can reach back to this one.

The plain embedded list is not copied at all, and needs no flag to say so. With no viewFilters and no foreignKeyField it names no parent relation, so it has no children to copy — the same fact that keeps the deletion walk out of it. This is a guard, not a nicety: the filter loop would build an empty filter array, and an empty filter array is not "no records" but no restriction, so getRecords(array()) returns every record of the child model. A product carrying such a list once duplicated every salutation on the site, twice, on one copy.

Three things can happen to a parent — copyWithParent, deleteWithParent, exportWithParent

A childentries is walked by three operations, and each asks the property whether these particular children should come along:

SettingThe walkDefault
copyWithParentcopy(), plus KenedoModel::copyRulesAndCalculations()true
deleteWithParentthe product cascade — ConfigboxProductDeleteHelper, behind the admin's confirmation dialog and the cbx_delete_product_deep tooltrue
exportWithParentgetPropertyChildTransferItems() — what a transfer package carriestrue

All three default to true, which is what a real parent/child list wants: say nothing and the children travel, exactly as this type has always behaved. Set one to false where those records should stay behind — a log, a usage history, an archive, rows another product could adopt.

They are three decisions, not one, and the asymmetries are real. An archive that must not be duplicated by a copy may still have to go when its product does. A log worth deleting with its parent is rarely worth shipping to another install inside a package. So they are separate settings rather than one "these are not really mine" switch.

For the cascade specifically: a child list on a product — or on anything under one — is in the deletion plan without anybody adding it to a list, because the cascade reads the graph off the models. A level found that way is still named in the confirmation dialog, using the property's own label, so an operator is never asked to confirm a delete wider than they were shown.

@see ../com_configbox_api_contract.md — counts and deleted are open maps for this reason

None of them is how you exempt a plain embedded list

That shape — no viewFilters, no foreignKeyField — is refused by all three walks as a matter of fact, not preference, and needs no setting. Its "children" are every record of the child model, so copying it duplicates the table, deleting it empties the table, and exporting it packages the table. One predicate answers that question for all three, so the three cannot drift apart.

Renamed from ignoreOnCopy / ignoreOnDelete, and inverted. The old names said "ignore" without saying by whom, and read backwards from the default. Both were a day old and unreleased with no setters anywhere, so there is no compatibility shim — an old name is simply not read. If you have one in a customization, rename it and invert the value.

Export carries the children

getPropertyChildTransferItems() runs the same filter, then calls getExportData() on each child, so an exported parent record carries its children as nested items. This is the only property type that contributes child records to a transfer rather than values or files.

It honours exportWithParent, and it refuses a property that names no parent relation — the same two questions copy() and the cascade ask. That second one used to be missing here, which meant a package exported from a parent carrying a plain embedded list contained every record of the child model, and an import would have written them into the target install.

Notes

  • Derived: getSelectsForGetRecord() and getDataKeysForBaseTable() both return empty arrays, and getDataFromRequest() does nothing. The property never reads or writes a value.
  • Because it is derived, it appears in no generated column description and no schema property.
  • Deleting a single record does not go through this property — it has no delete() override, so KenedoModel::delete() leaves the children to the child table's foreign key (ON DELETE CASCADE) or the parent model's afterDelete(). It is the product cascade that walks this property, and only that; the two are different operations and only one of them is a deep delete.
  • getChildRelation() is what the cascade asks. Overriding it in a custom property type is how a type other than childentries can put a level in the cascade.
  • appliesWhen works normally, and is worth using: the answers list only makes sense for question types that have answers.

@see multiselect.md — when the relationship is a set of references rather than child records @see ../com_configbox_kenedo_view.md @see ../com_configbox_mvc_tasks.md — copy() and the recursive deep-clone