Childentries
- Version
- 4.0 preview
- Updated
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
| Setting | Meaning |
|---|---|
viewClass | Required. The child's list view class, e.g. 'ConfigboxViewAdminanswers' |
viewPath | Optional explicit path to that view, for a view outside the usual lookup |
viewFilters | Which child records to show — array of array('filterName' => …, 'filterValueKey' => …) |
foreignKeyField | The child property holding the foreign key back to this record |
parentKeyField | The parent property holding the key that fills it — normally 'id' |
label | Used in the "save the record first" message |
hideAdminLabel | Usually true — the embedded list brings its own heading |
copyWithParent | false keeps the child records out of a copy of the parent — see below |
deleteWithParent | false keeps them out of a cascading product delete — see below |
exportWithParent | false 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 list — viewFilters 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:
| Setting | The walk | Default |
|---|---|---|
copyWithParent | copy(), plus KenedoModel::copyRulesAndCalculations() | true |
deleteWithParent | the product cascade — ConfigboxProductDeleteHelper, behind the admin's confirmation dialog and the cbx_delete_product_deep tool | true |
exportWithParent | getPropertyChildTransferItems() — what a transfer package carries | true |
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()andgetDataKeysForBaseTable()both return empty arrays, andgetDataFromRequest()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, soKenedoModel::delete()leaves the children to the child table's foreign key (ON DELETE CASCADE) or the parent model'safterDelete(). 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 thanchildentriescan put a level in the cascade.appliesWhenworks 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