I have a Laravel API + Vue FE app where a user can modify/create custom forms for different entities. The problem I'm facing is I don't know the best way to store and retrieve the "repeater" field type's children values. Ideally I would like to have the ability to have nested repeater fields.
A simplified form_fields
table schema:
form_fields
------------------
id
name
type # like text, checkbox, repeater, etc
form_id # the form that this field belongs to
parent_id # the id of the repeater parent
For the form_field_values
table, I'm thinking something like this:
form_field_values
------------------
id
form_field_id
value
parent_id # the parent if it's nested in a repeater
answerable_id # morph
answerable_type # morph
This object structure for the form is as follows
form = {
id: 1,
name: 'form name',
fields: [
{
id: 1,
name: 'text_field_name',
type: 'text',
value: 'Text value',
value_id: null,
},
{
id: 2,
name: 'repeating_type_field',
type: 'repeater',
value: null,
value_id: null,
// This stores the form field structure
// When a user adds a new repeating section,
// This just gets copied into the `collections` property
children: [
{
id: 3,
name: 'child_field_1',
type: 'text',
value: null,
value_id: null,
parent_id: 2
},
{
id: 4,
name: 'child_field_2',
type: 'repeater',
value: null,
value_id: null,
parent_id: 2,
children: [
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: null,
value_id: null,
parent_id: 4
}
],
// When creating values for an entity,
// `collections` is build on the front end only
// But when retrieving an entity's form values
// I can't figure out how to populate this property
collections: [
[
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: 'nested value',
value_id: null,
parent_id: 4
},
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: 'different nested value',
value_id: null,
parent_id: 4
}
]
]
}
],
collections: [
[
{
id: 3,
name: 'child_field_1',
type: 'text',
value: 'some value',
value_id: null,
parent_id: 2
},
{
id: 4,
name: 'child_field_2',
type: 'repeater',
value: null,
value_id: null,
parent_id: 2,
children: [
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: null,
value_id: null,
parent_id: 4
}
],
collections: [
[
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: 'nested value',
value_id: null,
parent_id: 4
},
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: 'different nested value',
value_id: null,
parent_id: 4
}
]
]
}
],
[
{
id: 3,
name: 'child_field_1',
type: 'text',
value: null,
value_id: null,
parent_id: 2
},
{
id: 4,
name: 'child_field_2',
type: 'repeater',
value: null,
value_id: null,
parent_id: 2,
children: [
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: 'some value',
value_id: null,
parent_id: 4
}
],
collections: [
[
{
id: 5,
name: 'nested_child_field_1',
type: 'text',
value: 'nested value',
value_id: null,
parent_id: 4
}
]
]
}
]
]
}
]
}
Creating the above json structure is easy on the frontend. However, I can't quite get the structure of collections right when retrieving an existing entity's values. I think it might be my form_field_values
table design, but I'm not sure.
Here's a general idea of the function that I have for building out the collections
property of a field.
$entity = Entity::find($id);
// Loads the morphable answers, and the nested answers from parent_id
$entity->load('answers', 'answers.children');
// I have it grouped by the form_field_id for easy retrieval
$answers = $entity->answers->groupBy('form_field_id');
$form->load('fields', 'fields.children');
// Create a fields collection keyed by id for easy retrieval
$fieldsByKey = $form->fields->keyBy('id');
// My thinking is that I'll store the built out
// collection into this variable
$collectionsByParentId = [];
$valueBuilder = function ($field) use ($entity, $answers, $fieldsByKey, &$collectionsByParentId, &$valueBuilder) {
// If this field has children it's a repeater type
// All the children values need to be populated
// Recurively call this function
// We need to set $collectionsByParentId, using this field (child's parent) id
// After the children have been processed it's just a matter of
// setting the collection from $collectionsByParentId
if ($field->children->isNotEmpty()) {
// Recursively run this function on its children
$field->children->each($valueBuilder);
// // Set the field's newly created collection in $collectionsByParentId
$field->collections = $collectionsByParentId[$field->id];
}
// If this field is a non repeater field
// and it is a child of a repeater
// add the values to $collectionsByParentId by its parent id
elseif ($field->parent_id && isset($answers[$field->parent_id])) {
// We need find this field's value info from $answers
// We know the number of collections based on the count of answers
// of this field's parent answers
// If the parent of this field also has a parent,
// We need to get the answers from the parent's answer's children
$isSubChild = !isset($fieldsByKey[$field->parent_id]);
$answerCollection = $isSubChild ? $answers[$field->parent_id] : $answers[$field->id];
$numOfCollections = count($answerCollection);
// Iterate the number of collections, correctly inserting the collection
// at the appropriate index n
for ($i = 0; $i < $numOfCollections; $i++) {
// There seems to be a reference problem,
// so replicate the field to prevent that
$clone = $field->replicate();
// If this field's parent doesn't have children answers
// it is not a child of a nested repeater
// Add the values from it's own offset the answers
if (!isset($answers[$field->parent_id][$i]) || !$answers[$field->parent_id][$i]->parent_id) {
$clone->value = $answers[$field->id][$i]->value;
$clone->value_id = $answers[$field->id][$i]->id;
// Add it to the parent's collection
$collectionsByParentId[$field->parent_id][$i][] = $clone;
}
// If the collection values have children,
// we need to add those children from its parent's child answers
else {
foreach ($answers[$field->parent_id][$i]->children as $j => $child) {
$clone->value = $child->value;
$clone->value_id = $child->id;
// Add it to the parent's collection
$collectionsByParentId[$field->parent_id][$i][$j] = $clone;
}
}
}
}
// If this field is a non repeater field
// and it isn't a child of a repeater
// simply add the answer value and value_id
elseif (isset($answers[$field->id])) {
$field->value = $answers[$field->id][0]->value;
$field->value_id = $answers[$field->id][0]->id;
}
};
// Run all the fields through the function
$form->fields->each($valueBuilder);
I know there's recursion involved due to the nested nature, I just can't quite get the logic straightened out.
Should I change the table structure or am I just not getting the function logic right?