parentId map if needed. * * @param array $data Passed by reference; parentId modified in place. * @param array $parentMap Map of fieldIndex => newly created parent field id. */ public static function resolveParentId(array &$data, array $parentMap): void { if (!array_key_exists('parentId', $data)) { return; } $parentId = $data['parentId']; //parentId is 0 or null → use current field's fieldIndex if (($parentId === 0 || $parentId === '0' || $parentId === null) && isset($data['fieldIndex'])) { $parentIndex = $data['fieldIndex']; if (isset($parentMap[$parentIndex])) { $data['parentId'] = $parentMap[$parentIndex]; } return; } //parentId is numeric → use it as parent's fieldIndex if (is_numeric($parentId)) { if (isset($parentMap[$parentId])) { $data['parentId'] = $parentMap[$parentId]; } } } /** * Remap parentId for duplicated field using old-to-new field ID map. * * @param array $fieldData Passed by reference; parentId modified in place. * @param array $fieldIdMap Map of old field ID => new field ID. */ public static function remapParentIdForDuplication(array &$fieldData, array $fieldIdMap): void { if ($fieldData['parentId'] !== null && isset($fieldIdMap[$fieldData['parentId']])) { $fieldData['parentId'] = $fieldIdMap[$fieldData['parentId']]; } } /** * Partition field options into new, update, and submitted IDs. * * @param array $fieldData * @param array $existingOptionIds * @return array{ * newOptions: array, * updateOptions: array, * submittedOptionIds: array * } * @throws ValidationException */ public static function partitionFieldOptions(array $fieldData, array $existingOptionIds): array { $newOptions = []; $updateOptions = []; $submittedOptionIds = []; foreach ($fieldData['fieldOptions'] as $optionData) { $option = FieldOptionsFactory::create($optionData); if ( isset($optionData['id']) && $optionData['id'] > 0 && in_array($optionData['id'], $existingOptionIds, true) ) { $updateOptions[] = $option; $submittedOptionIds[] = $optionData['id']; continue; } $newOptions[] = $option; } return [ 'newOptions' => $newOptions, 'updateOptions' => $updateOptions, 'submittedOptionIds' => $submittedOptionIds, ]; } /** * Check if a field type has options (radio, checkbox, select, multi-select). * * @param string $fieldType * @return bool */ public static function fieldTypeHasOptions(string $fieldType): bool { return in_array($fieldType, ['radio', 'checkbox', 'select', 'multi-select'], true); } /** * Check if a field type is a security/CAPTCHA field. * * Security fields are used for bot/spam protection and should be excluded * from stored entries, integration payloads, placeholder lists, and any * other processing that targets user-submitted data. * * @param string $fieldType * @return bool */ public static function isSecurityField(string $fieldType): bool { return in_array($fieldType, ['recaptcha', 'turnstile', 'hcaptcha'], true); } /** * Duplicate field options from one field to another. * * @param int $oldFieldId * @param int $newFieldId * @param FieldOptionsRepositoryInterface $repository * @return void * @throws ValidationException */ public static function duplicateFieldOptions( int $oldFieldId, int $newFieldId, FieldOptionsRepositoryInterface $repository ): void { $options = $repository->getByFieldId($oldFieldId); foreach ($options as $option) { $optionData = $option->toArray(); unset($optionData['id']); $optionData['fieldId'] = $newFieldId; $repository->add(FieldOptionsFactory::create($optionData)); } } /** * Process htmlContent to upload base64 images * * @param array $fieldData Passed by reference; htmlContent modified in place * @param ImageService $imageService * @return void */ public static function processFieldHtmlContent(array &$fieldData, ImageService $imageService): void { if (!empty($fieldData['htmlContent'])) { $processedContent = $imageService->processImagesInContent( $fieldData['htmlContent'], 'field' ); $fieldData['htmlContent'] = $processedContent; } } }