fieldRepository = $fieldRepository; $this->fieldOptionsRepository = $fieldOptionsRepository; } /** * Create a new field. * * @param Field $fieldData * * @return int Form ID * * @throws InvalidArgumentException If the field data is invalid */ public function addField(Field $fieldData): int { // Validate form data if (empty($fieldData->getFormId())) { throw new InvalidArgumentException( BackendStrings::getExceptionStrings()['form_id_required'] ); } // Create form via repository and return new form ID return $this->fieldRepository->add($fieldData); } /** * Update an existing field. * * @param int $fieldId * @param Field $fieldData * * @return bool * * @throws InvalidArgumentException If the form data is invalid */ public function updateField(int $fieldId, Field $fieldData): bool { // Validate form data before updating if (empty($fieldData->getId())) { throw new InvalidArgumentException( BackendStrings::getExceptionStrings()['field_id_required'] ); } return $this->fieldRepository->update($fieldId, $fieldData); } /** * Delete a field by its ID. * * @param int $fieldId * @return int * @throws QueryExecutionException */ public function deleteField(int $fieldId): int { // Delete field options first $this->fieldOptionsRepository->deleteByFieldIds([$fieldId]); // Then delete field return $this->fieldRepository->delete($fieldId); } /** * Batch delete fields by IDs using one SQL query. * * @param int[] $fieldIds Raw field IDs to attempt to delete (will be sanitized). * @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($value) => $value > 0)); if (empty($ids)) { return 0; } // Delete field options first $this->fieldOptionsRepository->deleteByFieldIds($ids); // Then delete fields return $this->fieldRepository->deleteMany($ids); } /** * Get fields by form ID * * @param int $id * * @return mixed[]|null */ public function getAllFields(int $id): ?array { return $this->fieldRepository->getFieldsById($id); } /** * Delete fields by multiple form IDs. * * @param array $formIds * @return int Number of deleted fields * @throws InvalidArgumentException If no form IDs are provided * @throws QueryExecutionException */ public function deleteFieldsByFormIds(array $formIds): int { if (empty($formIds)) { throw new InvalidArgumentException( BackendStrings::getExceptionStrings()['no_form_ids_provided'] ); } // Delete field options first $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', $formIds); // Then delete fields return $this->fieldRepository->deleteManyByForeignKeyValues($formIds, 'formId'); } /** * Delete fields by a single form ID. * * @param int $formId * @return int Number of deleted fields * @throws QueryExecutionException */ public function deleteFieldsByFormId(int $formId): int { // Secure deletion: only allow whitelisted column $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', [$formId]); // Then delete fields return $this->fieldRepository->deleteOneByForeignKeyValue($formId, 'formId'); } /** * @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 and their options for a form. * * @param array $fields * @param int $formId * @param FieldOptionsService $fieldOptionsService * @return void * @throws ValidationException|InvalidArgumentException */ public function saveFieldsWithOptions(array $fields, int $formId, FieldOptionsService $fieldOptionsService): void { foreach ($fields as $fieldData) { $fieldData['formId'] = $fieldData['formId'] ?? $formId; $field = FieldFactory::create($fieldData); $fieldId = $this->addField($field); // Always use fieldOptions for radio, checkbox, select if ( in_array($field->getType(), ['radio', 'checkbox', '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); $fieldOptionsService->addOption($option); } } } } /** * Update fields and their options for a form. * * @param array> $fields * @param int $formId * @param FieldOptionsService $fieldOptionsService * @return array List of submitted field IDs * @throws ValidationException|InvalidArgumentException */ public function updateFieldsWithOptions(array $fields, int $formId, FieldOptionsService $fieldOptionsService): array { $submittedFieldIds = []; $parentMap = []; foreach ($fields as $fieldData) { $fieldData['formId'] = $fieldData['formId'] ?? $formId; Helper::resolveParentId($fieldData, $parentMap); $field = FieldFactory::create($fieldData); $fieldId = $field->getId(); if ($fieldId === 0) { // Add 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); } $fieldOptionsService->addOptions($newOptions); continue; } $this->updateField($fieldId, $field); $submittedFieldIds[] = $fieldId; if (empty($fieldData['fieldOptions'])) { continue; } foreach ($fieldData['fieldOptions'] as &$optionData) { $optionData['fieldId'] = $fieldId; } unset($optionData); Helper::batchProcessFieldOptions($fieldId, $fieldData, $fieldOptionsService, $this->fieldOptionsRepository); } return $submittedFieldIds; } /** * Fetch all fields for a form, attaching fieldOptions for radio/checkbox/select fields. * * @param int $formId * @param FieldOptionsRepository $fieldOptionsRepository * @return array> * @throws ValidationException|QueryExecutionException */ public function getAllFieldsWithOptions(int $formId, FieldOptionsRepository $fieldOptionsRepository): array { $fields = $this->fieldRepository->getFieldsById($formId); $fieldsWithOptions = []; foreach ($fields as $field) { $fieldArr = $field->toArray(); // Attach fieldOptions for radio, checkbox, select if (in_array($field->getType(), ['radio', 'checkbox', 'select'])) { $options = $fieldOptionsRepository->getByFieldId($field->getId()); $fieldArr['fieldOptions'] = array_map(function ($option) { return $option->toArray(); }, $options); } $fieldsWithOptions[] = $fieldArr; } return $fieldsWithOptions; } /** * Collect existing field IDs for a form * @return int[] */ public function collectExistingFieldIds(int $formId): array { $existingFields = $this->getAllFields($formId); return array_map(static fn($field) => $field->getId(), $existingFields ?? []); } /** * Check for duplicate field values in a form submission. * * @param Field[] $formFields * @param array $submissionData * @param int $formId * @param EntryService $entryService * @return array{0: bool, 1: array} */ public function checkDuplicateFieldValues( array $formFields, array $submissionData, int $formId, EntryService $entryService ): 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 ($entryService->checkDuplicateValue($formId, $fid, $val)) { $duplicateErrors[$fidStr] = true; $isDuplicate = true; } } } return [$isDuplicate, $duplicateErrors]; } }