# ivyforms/0.8/backend/src/Services/Field/FieldService.php

The Innovative Form Builder – IvyForms, version 0.8. 383 lines.

- Page: https://pluginprobe.com/plugins/ivyforms/0.8/code/backend/src/Services/Field/FieldService.php
- Raw: https://pluginprobe.com/plugins/ivyforms/0.8/raw/backend/src/Services/Field/FieldService.php
- Modified: 2026-01-13T11:11:38+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ivyforms/0.8/code/backend/src/Services/Field/FieldService.php#L10-L20`.

```php
<?php

namespace IvyForms\Services\Field;

// phpcs:disable PSR1.Files.SideEffects
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

use IvyForms\Common\Exceptions\QueryExecutionException;
use IvyForms\Common\Exceptions\ValidationException;
use IvyForms\Entity\Field\Field;
use IvyForms\Common\Exceptions\InvalidArgumentException;
use IvyForms\Factory\Field\FieldFactory;
use IvyForms\Factory\FieldOptions\FieldOptionsFactory;
use IvyForms\Repository\Field\FieldRepositoryInterface;
use IvyForms\Repository\FieldOptions\FieldOptionsRepositoryInterface;
use IvyForms\Repository\EntryField\EntryFieldRepositoryInterface;
use IvyForms\Services\Translations\BackendStrings;
use IvyForms\Common\Helpers\FieldHelper;

/**
 * @SuppressWarnings("TooManyPublicMethods")
 */
class FieldService
{
    private FieldRepositoryInterface $fieldRepository;
    private FieldOptionsRepositoryInterface $fieldOptionsRepository;
    private EntryFieldRepositoryInterface $entryFieldRepository;

    // Constructor injection for repositories only via PHP-DI
    public function __construct(
        FieldRepositoryInterface $fieldRepository,
        FieldOptionsRepositoryInterface $fieldOptionsRepository,
        EntryFieldRepositoryInterface $entryFieldRepository
    ) {
        $this->fieldRepository = $fieldRepository;
        $this->fieldOptionsRepository = $fieldOptionsRepository;
        $this->entryFieldRepository = $entryFieldRepository;
    }

    /**
     * Create a new field.
     *
     * @param Field $fieldData
     * @return int Field ID
     * @throws InvalidArgumentException If the field data is invalid
     */
    public function addField(Field $fieldData): int
    {
        if (empty($fieldData->getFormId())) {
            throw new InvalidArgumentException(
                BackendStrings::getExceptionStrings()['form_id_required']
            );
        }
        return $this->fieldRepository->add($fieldData);
    }

    /**
     * Update an existing field.
     *
     * @param int $fieldId
     * @param Field $fieldData
     * @return bool
     * @throws InvalidArgumentException If the field data is invalid
     */
    public function updateField(int $fieldId, Field $fieldData): bool
    {
        if (empty($fieldData->getId())) {
            throw new InvalidArgumentException(
                BackendStrings::getExceptionStrings()['field_id_required']
            );
        }
        return $this->fieldRepository->update($fieldId, $fieldData);
    }

    /**
     * Delete a field by its ID (removes options first).
     *
     * @param int $fieldId
     * @return int
     * @throws QueryExecutionException
     */
    public function deleteField(int $fieldId): int
    {
        $this->fieldOptionsRepository->deleteByFieldIds([$fieldId]);
        return $this->fieldRepository->delete($fieldId);
    }

    /**
     * Batch delete fields by IDs.
     *
     * @param int[] $fieldIds
     * @return int Number of deleted rows
     * @throws QueryExecutionException
     */
    public function deleteFields(array $fieldIds): int
    {
        $ids = array_values(array_filter(array_map('intval', $fieldIds), static fn($val) => $val > 0));
        if (empty($ids)) {
            return 0;
        }
        $this->fieldOptionsRepository->deleteByFieldIds($ids);
        return $this->fieldRepository->deleteMany($ids);
    }

    /**
     * Get all fields by form ID.
     * @param int $id
     * @return Field[]|null
     */
    public function getAllFields(int $id): ?array
    {
        return $this->fieldRepository->getFieldsById($id);
    }

    /**
     * Get multiple fields by their IDs in a single query.
     * @param int[] $fieldIds
     * @return array<int, object> Indexed by field ID
     */
    public function getFieldsByIds(array $fieldIds): array
    {
        if (empty($fieldIds)) {
            return [];
        }
        return $this->fieldRepository->getFieldsByIds($fieldIds);
    }

    /**
     * Delete all fields for multiple form IDs.
     * @param int[] $formIds
     * @return int
     * @throws InvalidArgumentException|QueryExecutionException
     */
    public function deleteFieldsByFormIds(array $formIds): int
    {
        if (empty($formIds)) {
            throw new InvalidArgumentException(
                BackendStrings::getExceptionStrings()['no_form_ids_provided']
            );
        }
        $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', $formIds);
        return $this->fieldRepository->deleteManyByForeignKeyValues($formIds, 'formId');
    }

    /**
     * Delete fields by single form ID.
     * @param int $formId
     * @return int
     * @throws QueryExecutionException
     */
    public function deleteFieldsByFormId(int $formId): int
    {
        $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', [$formId]);
        return $this->fieldRepository->deleteOneByForeignKeyValue($formId, 'formId');
    }

    /**
     * Duplicate all fields from one form to another.
     * @param int $originalFormId
     * @param int $newFormId
     * @return void
     * @throws InvalidArgumentException|ValidationException
     */
    public function duplicateFields(int $originalFormId, int $newFormId): void
    {
        $fields = $this->getAllFields($originalFormId);
        foreach ($fields as $field) {
            $fieldData = $field->toArray();
            unset($fieldData['id']);
            $fieldData['formId'] = $newFormId;
            $this->addField(FieldFactory::create($fieldData));
        }
    }

    /**
     * Save fields with their options (radio/checkbox/select) for a form.
     * @param array<int, array<string,mixed>> $fields
     * @param int $formId
     * @return void
     * @throws ValidationException|InvalidArgumentException
     */
    public function saveFieldsWithOptions(array $fields, int $formId): void
    {
        $parentMap = [];
        foreach ($fields as $fieldData) {
            $fieldData['formId'] = $fieldData['formId'] ?? $formId;
            // Resolve parentId using fieldIndex if available
            FieldHelper::resolveParentId($fieldData, $parentMap);
            $field = FieldFactory::create($fieldData);
            $fieldId = $this->addField($field);
            // If this is a parent field (no parentId), store its DB id in the map
            if ($field->getParentId() === null) {
                $parentMap[$field->getIndex()] = $fieldId;
            }
            if (
                in_array(
                    $field->getType(),
                    ['radio', 'checkbox', 'select', 'multi-select'],
                    true
                ) && !empty($fieldData['fieldOptions'])
            ) {
                foreach ($fieldData['fieldOptions'] as $optionData) {
                    // Always overwrite fieldId with the actual DB id of the field
                    $optionData['fieldId'] = $fieldId;
                    $option = FieldOptionsFactory::create($optionData);
                    $this->fieldOptionsRepository->add($option);
                }
            }
        }
    }

    /**
     * Update (and create/delete) fields and their options for a form.
     * @param array<int, array<string,mixed>> $fields
     * @param int $formId
     * @return int[] Submitted field IDs
     * @throws ValidationException|InvalidArgumentException
     */
    public function updateFieldsWithOptions(array $fields, int $formId): array
    {
        $submittedFieldIds = [];
        $parentMap = [];
        foreach ($fields as $fieldData) {
            $fieldData['formId'] = $fieldData['formId'] ?? $formId;
            FieldHelper::resolveParentId($fieldData, $parentMap);
            $field = FieldFactory::create($fieldData);
            $fieldId = $field->getId();

            if ($fieldId === 0) { // new field
                $newFieldId = $this->addField($field);
                if ($field->getParentId() === null) {
                    $parentMap[$field->getIndex()] = $newFieldId;
                }
                $submittedFieldIds[] = $newFieldId;
                if (empty($fieldData['fieldOptions'])) {
                    continue;
                }
                $newOptions = [];
                foreach ($fieldData['fieldOptions'] as $optionData) {
                    $optionData['fieldId'] = $newFieldId;
                    $newOptions[] = FieldOptionsFactory::create($optionData);
                }
                $this->fieldOptionsRepository->addMany($newOptions);
                continue;
            }

            // existing field
            $this->updateField($fieldId, $field);
            $submittedFieldIds[] = $fieldId;
            if (empty($fieldData['fieldOptions'])) {
                continue;
            }
            foreach ($fieldData['fieldOptions'] as &$optionData) {
                $optionData['fieldId'] = $fieldId;
            }
            unset($optionData);
            $this->batchProcessFieldOptions($fieldId, $fieldData);
        }
        return $submittedFieldIds;
    }

    /**
     * Fetch all fields (with options for radio/checkbox/select types).
     * @param int $formId
     * @return array<int, array<string,mixed>>
     * @throws ValidationException|QueryExecutionException
     */
    public function getAllFieldsWithOptions(int $formId): array
    {
        $fields = $this->fieldRepository->getFieldsById($formId);
        $fieldsWithOptions = [];
        foreach ($fields as $field) {
            $fieldArr = $field->toArray();
            if (in_array($field->getType(), ['radio', 'checkbox', 'select', 'multi-select'], true)) {
                $options = $this->fieldOptionsRepository->getByFieldId($field->getId());
                $fieldArr['fieldOptions'] = array_map(static fn($option) => $option->toArray(), $options);
            }
            $fieldsWithOptions[] = $fieldArr;
        }
        return $fieldsWithOptions;
    }

    /**
     * Collect existing field IDs for a form.
     * @param int $formId
     * @return int[]
     */
    public function collectExistingFieldIds(int $formId): array
    {
        $existingFields = $this->getAllFields($formId);
        return array_map(static fn($field) => $field->getId(), $existingFields ?? []);
    }

    /**
     * Batch process field options: add, update, and delete as needed.
     * @param int $fieldId
     * @param array<string, mixed> $fieldData
     * @throws ValidationException
     */
    public function batchProcessFieldOptions(
        int $fieldId,
        array $fieldData
    ): void {
        $existingOptions = $this->fieldOptionsRepository->getByFieldId($fieldId);
        $existingOptionIds = array_map(static fn($opt) => $opt->getId(), $existingOptions);
        $partition = FieldHelper::partitionFieldOptions($fieldData, $existingOptionIds);
        // Add new options
        $newOptionIds = $this->fieldOptionsRepository->addMany($partition['newOptions']);
        // Update existing options
        $this->fieldOptionsRepository->updateMany($partition['updateOptions']);
        // Determine submitted option IDs (existing updated + newly created)
        $submittedOptionIds = array_merge($partition['submittedOptionIds'], $newOptionIds);
        // Delete orphaned (those that existed but were not resubmitted)
        $orphanedOptionIds = array_diff($existingOptionIds, $submittedOptionIds);
        if (!empty($orphanedOptionIds)) {
            $this->fieldOptionsRepository->deleteByOptionIds($orphanedOptionIds);
        }
    }

    /**
     * Check for duplicate field values in submission.
     * @param Field[] $formFields
     * @param array<string,mixed> $submissionData
     * @param int $formId
     * @return array{0: bool, 1: array<int|string,bool>}
     */
    public function checkDuplicateFieldValues(
        array $formFields,
        array $submissionData,
        int $formId
    ): array {
        $duplicateErrors = [];
        $isDuplicate = false;
        $fieldValues = [];

        foreach ($formFields as $field) {
            $fid = $field->getId();
            $fidStr = (string)$fid;
            if (isset($submissionData[$fidStr])) {
                $fieldValues[$fidStr] = $submissionData[$fidStr];
            }
            if ($field->getFieldAdvancedSettings()->isNoDuplicates() && !empty($fieldValues[$fidStr])) {
                $val = $fieldValues[$fidStr];
                if ($this->entryFieldRepository->checkDuplicateValue($formId, $fid, $val)) {
                    $duplicateErrors[$fidStr] = true;
                    $isDuplicate = true;
                }
            }
        }

        return [$isDuplicate, $duplicateErrors];
    }

    /**
     * Validate that all fields have allowed types.
     * @param Field[]|mixed[] $formFields
     * @throws InvalidArgumentException
     */
    public function validateFieldsType(array $formFields): void
    {
        // Validate field types
        $allowedTypes = FieldType::getAllowedTypes();
        foreach ($formFields as $field) {
            // Check if field is an instance of Field or array
            $fieldType = $field instanceof Field ? $field->getType() : ($field['type'] ?? null);

            if ($fieldType === null) {
                throw new InvalidArgumentException(
                    BackendStrings::getExceptionStrings()['invalid_field_type'] . ' (type not found)'
                );
            }

            if (!in_array($fieldType, $allowedTypes, true)) {
                throw new InvalidArgumentException(
                    BackendStrings::getExceptionStrings()['invalid_field_type'] . ' ' . $fieldType
                );
            }
        }
    }
}

```
