mercredi 24 janvier 2018

Storing repeating field values from customizable form

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?

Aucun commentaire:

Enregistrer un commentaire