Skip to content

fix: apply presence rule to custom field validation on import [3.x] - #192

Merged
ManukMinasyan merged 1 commit into
3.xfrom
fix/nullable-optional-custom-fields-on-import
Aug 3, 2026
Merged

fix: apply presence rule to custom field validation on import [3.x]#192
ManukMinasyan merged 1 commit into
3.xfrom
fix/nullable-optional-custom-fields-on-import

Conversation

@ManukMinasyan

Copy link
Copy Markdown
Collaborator

Fixes optional custom fields failing import validation when the column is mapped but the cell is empty (Journey: 868kkcw4h).

Root cause

ValidationService::getValidationRules() returns value-shape rules only — no required, no nullable. Filament form fields hide that gap because Field::getRequiredValidationRule() always prepends one. The importer calls Laravel's validator directly, so it shipped a rule set that was wrong in both directions.

Per row, Filament runs Importer::castData() before Importer::validateData(), and ImportColumn::castStateItem() turns every blank cell into null. So a mapped-but-empty column reaches the validator as a present null, which Laravel considers validatable. Without nullable, date / numeric / boolean / file / regex all reject it.

Reproduced

Against all 24 registered field types, optional field + mapped column + empty cell — 11 types rejected the row:

types error
number radio select toggle-buttons must be a number / must be an integer
currency must be a number / 0-15 decimal places
date date-time must be a valid date
checkbox toggle must be true or false
color-picker format is invalid
file-upload must be a file

The same run with validation_rules = ['required' => true] exposed the inverse bug: required custom fields were never enforced on import. text, textarea, rich-editor, markdown-editor, email, phone, link, multi-select, checkbox-list, tags-input, record all accepted an empty mapped cell. The remaining types only rejected it by accident, via the type rule, with a misleading message.

Fix

Make presence a first-class part of the ValidationService contract, with the safe default:

  • getValidationRules() — presence rule + value rules. The correct set for any caller handing a value straight to a Laravel validator (imports, APIs, jobs).
  • getValueValidationRules() — value rules only, for callers that supply presence themselves.
  • getPresenceRule() — single source of truth for required vs nullable.

ImportColumnConfigurator needs no change: it already calls getValidationRules(), which is now complete. The two Filament form adapters move to getValueValidationRules() because they apply ->required() separately and conditionally on field visibility — that conditionality is exactly why presence must not be baked into the form path.

Patching the importer call site alone would have fixed this ticket in three lines and left the same trap for the next consumer. This makes the obvious call correct and requires the special case to opt out.

Future-proofing

The one-line fix isn't what stops this recurring. tests/Feature/Imports/ImportColumnPresenceValidationTest.php iterates CustomFieldsType::toCollection() and asserts, per registered type:

  • optional + empty mapped cell → accepted
  • required + empty mapped cell → rejected
  • optional + representative value → accepted

Because it enumerates the registry rather than a hardcoded list, any newly added field type — including app-registered ones — is covered in both directions automatically.

Behaviour change

Required custom fields are now actually enforced on import. Rows that previously imported with a required custom field left blank will now fail with a clear "field is required" message instead of silently landing incomplete.

Verification

  • pest --parallel — 778 passed, 0 failed (3 todos)
  • phpstan analyse — no errors
  • pint --dirty — passed
  • rector --dry-run — clean
  • Type coverage sits at 99.4% both before and after this change (pre-existing, in DateConstraintField.php and FieldForm.php; not gated in CI)

Known gaps, deliberately out of scope

  1. Required + unmapped column is still silent. Importer::getValidationRules() skips unmapped columns entirely, so a required custom field the user never mapped is never validated. Filament's answer is ImportColumn::requiredMapping(). Left out because it would block existing import workflows at the mapping step — worth a separate, deliberate PR.
  2. file-upload fields are unimportable with a filled cell. FileUploadFieldType declares defaultValidationRules(['file']), and a CSV cell is always a string, so a filled cell fails with "must be a file" regardless of this change. nullable only rescues the empty case. getDatabaseValidationRules() already special-cases FieldDataType::FILE for the same reason; the import path should too.

ValidationService::getValidationRules() returned value-shape rules only —
no required, no nullable. Filament form fields hide that gap because
Field::getRequiredValidationRule() always prepends one. The importer calls
Laravel's validator directly, so it shipped a rule set that was wrong in
both directions.

Filament's ImportColumn::castStateItem() turns every blank cell into null
before Importer::validateData() runs, so a mapped-but-empty column reaches
the validator as a present null. Without nullable, date/numeric/boolean/
file/regex all reject it: 11 of 24 field types failed the row for an
optional field the user simply left blank. Conversely, required custom
fields were never enforced at all — 12 types silently accepted an empty
mapped cell, the rest only rejected it by accident via the type rule, with
a misleading message.

getValidationRules() now returns the presence rule followed by the value
rules, so any caller handing a value straight to a validator is correct by
default. The Filament form adapters, which supply presence themselves
conditionally on field visibility, move to the new getValueValidationRules().
Form behaviour is unchanged.

Covered by an invariant test that iterates every registered field type, so
newly added types — including app-registered ones — are checked in both
directions automatically.
Copilot AI review requested due to automatic review settings August 3, 2026 16:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes custom field import validation so that presence (required vs nullable) is correctly enforced when validating mapped import columns, aligning import behavior with how Filament form fields handle required/optional fields.

Changes:

  • Extend ValidationService to make presence validation explicit and reusable via getPresenceRule(), with a safe default in getValidationRules().
  • Introduce getValueValidationRules() for callers (Filament form adapters) that already apply presence separately.
  • Add a comprehensive import-focused test that iterates over the registered custom field type registry to prevent regressions for newly added types.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
tests/Feature/Imports/ImportColumnPresenceValidationTest.php Adds coverage to ensure optional/required presence behaves correctly across all registered field types during imports.
src/Services/ValidationService.php Makes presence (required/nullable) first-class and separates full rules vs value-only rules.
src/Filament/Integration/Concerns/Forms/ConfiguresValidation.php Switches Filament form integration to use value-only rules since presence is applied via ->required().
src/Filament/Integration/Base/AbstractFormComponent.php Updates form component validation rule retrieval to use value-only rules and documents the presence-handling rationale.

@ManukMinasyan
ManukMinasyan merged commit cc78133 into 3.x Aug 3, 2026
5 checks passed
@ManukMinasyan
ManukMinasyan deleted the fix/nullable-optional-custom-fields-on-import branch August 3, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants