# Childentries

> 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. Th…

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/technical/property-types/childentries. Last updated 2026-08-12.

---
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`:

```php
$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 &lt;label&gt;."* 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()` 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
