Skip to content

feat: replace generic validation rules with field-type-owned validation capabilities#75

Merged
ManukMinasyan merged 43 commits into3.xfrom
feat/validation-capabilities
Feb 24, 2026
Merged

feat: replace generic validation rules with field-type-owned validation capabilities#75
ManukMinasyan merged 43 commits into3.xfrom
feat/validation-capabilities

Conversation

@ManukMinasyan
Copy link
Collaborator

Summary

  • Replace the generic ValidationRule enum + repeater UI with a validation capabilities system where each field type declares its own validation capabilities via FieldSchema
  • Each capability (ValidationCapability interface) owns its admin form schema, Filament component application, and Laravel rule generation — eliminating the gap between what admins configure and what actually validates
  • Add support for relative date constraints (e.g., "3 days from now") via DateConstraintValue value object with DateConstraintMode and DateUnit enums
  • Include MigrateValidationRulesFormatStep upgrade step to convert old [{name, parameters}] format to new {key: value} format
  • Remove ~2,000 lines of legacy code: ValidationRule enum, ValidationRuleData DTO, CustomFieldValidationComponent repeater, ValidationRulesDataset

Capabilities Added

  • Text: MinLengthCapability, MaxLengthCapability
  • Numeric: MinValueCapability, MaxValueCapability, IntegerOnlyCapability, DecimalPlacesCapability
  • Selection: MinSelectionsCapability, MaxSelectionsCapability
  • Date: MinDateCapability, MaxDateCapability (with absolute + relative mode)
  • File: MaxFileSizeCapability, AcceptedFileTypesCapability

Test Plan

  • 336 tests passing (1,200 assertions)
  • PHPStan clean (43 errors, all pre-existing Filament 5 type stubs)
  • Pint formatting passes
  • Manual: create fields with validation capabilities in admin, verify form constraints apply
  • Manual: run custom-fields:upgrade on DB with old-format validation rules

Replace generic ValidationRule enum + repeater with field-type-owned
validation capabilities on FieldSchema for v3.
16-task TDD plan covering foundation, capabilities, wiring, admin UI,
field type updates, migration step, and cleanup.
…tion

Add upgrade step that converts validation_rules from old array-of-objects
format to the new key-value format. Maps rules based on field type:
text -> min/max_length, numeric -> min/max_value, multi-select ->
min/max_selections, date -> min/max_date with relative date support,
file -> max_size_kb and accepted_types. Unmappable rules are discarded
with warnings. Registered before clean-multivalue-rules step.
Copilot AI review requested due to automatic review settings February 17, 2026 09:43
Copy link

Copilot AI left a comment

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 replaces the generic ValidationRule enum-based validation system with a field-type-owned validation capabilities architecture. Each field type declares validation capabilities via FieldSchema fluent methods (e.g., ->canHaveMinDate(), ->canHaveMaxLength()). Each capability is a self-contained class implementing the ValidationCapability interface, owning its admin form UI, Filament component application, and Laravel rule generation. The system adds support for relative date constraints (e.g., "7 days from now") via DateConstraintValue with DateConstraintMode and DateUnit enums. The migration includes an MigrateValidationRulesFormatStep to convert from old [{name, parameters}] to new {key: value} format. The PR removes ~2,000 lines of legacy code including the ValidationRule enum, ValidationRuleData DTO, and CustomFieldValidationComponent repeater.

Changes:

  • Introduces ValidationCapability interface with 8 implemented capabilities (text length, selection count, date constraints, file validation)
  • Replaces validation_rules DataCollection cast with AsCollection, changing storage from [{name, parameters}] to {key: value} format
  • Adds DateConstraintValue value object supporting absolute and relative date constraints with new DateConstraintMode and DateUnit enums
  • Updates all 22 field type definitions to declare capabilities via FieldSchema instead of availableValidationRules
  • Removes ValidationRule enum (100+ cases), ValidationRuleData DTO, CustomFieldValidationComponent repeater, and related infrastructure

Reviewed changes

Copilot reviewed 70 out of 70 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/Contracts/ValidationCapability.php New interface defining capability contract
src/Validation/Capabilities/*.php 8 capability implementations (date, text, selection, file)
src/Enums/DateConstraintMode.php, DateUnit.php New enums for relative date support
src/Data/DateConstraintValue.php Value object for date constraint resolution
src/FieldTypeSystem/FieldSchema.php Added capability registration methods
src/Models/CustomField.php Changed validation_rules cast to AsCollection
src/Services/ValidationService.php Simplified to iterate capabilities
src/Filament/Integration/Base/AbstractFormComponent.php Applies capabilities to form components
src/Filament/Management/Schemas/FieldForm.php Renders capability-based validation tab
src/Console/Commands/Upgrade/Steps/MigrateValidationRulesFormatStep.php Migration from old to new format
tests/* 336 tests covering capabilities, migration, integration
src/FieldTypeSystem/Definitions/*.php All 22 field types updated to use capabilities

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Move required toggle from validation tab into settings fieldset
- Replace validation tab with inline validation fieldset (only visible
  when field type has capabilities)
- Switch settings toggles to inline mode for compact layout
- Replace helperText with hintIcon tooltips for cleaner UI
- Use 3-column grid for settings fieldset
- Remove unused Exception import
…pport

- Remove DateConstraintMode enum (absolute mode)
- Add DateDirection enum (ago/from_now) for intuitive date constraints
- Add Quarters to DateUnit
- Simplify DateConstraintField UI: value + unit + direction (3-column)
- Move required toggle into validation fieldset
- Move validation fieldset above options repeater
- Remove unnecessary minLength/maxLength from email, phone, link types
- Update migration step to warn on absolute dates instead of converting
- Update all related tests
…apabilities

- Remove `2026-02-17-validation-capabilities-design.md`
- Remove `2026-02-17-validation-capabilities-implementation.md`
- Documentation no longer reflects current architecture after recent refactors
- Use DateConstraintValue::from() for proper Spatie Data hydration
- Move validation fieldset into its own tab before Visibility
- Reimplement import validation using ValidationService
- Add Copilot custom instructions for Filament v5 namespaces
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Covers anchor-based data model (today/fixed/custom-field/record-created),
preset-driven config UI with progressive disclosure, and runtime
changes for submit-time validation and cross-field reactivity.
9-task TDD plan covering enums, DTO, validation rule, capabilities,
config UI, reactive field refs, circular reference detection, and
integration testing.
Add DateValidationManagementTest with 7 tests verifying persistence of
date fields with various validation constraints (today anchor, fixed
date, custom field reference, record created, offset, no restrictions).

Add DateValidationIntegrationTest with 6 tests verifying runtime
validation on CreatePost forms (reject past dates, accept valid dates,
max date enforcement, fixed date constraint, offset validation).

Fix ValidationService::combineRules() and DatabaseFieldConstraints to
handle non-string validation rules (DateConstraintRule objects).
Use $record injection instead of $get() path traversal to read
entity_type and code. The hidden entity_type field wasn't being
properly hydrated in Filament 5's action form context, causing
$get() to return null at any traversal depth. Falling back to
$get() for the create-new-field path where $record is null.
Add defensive checks in date capabilities to handle pre-existing
database records saved before sanitizeValidationRules was introduced.
Extract AbstractDateCapability to centralize shared logic and guards.
Full round-trip E2E tests covering all anchor types for date and
date-time fields. Regression tests for legacy data with preset:none
but no anchor key.
Fix incorrect Component namespace imports, add phpstan-ignore for
dynamic Field method dispatch, and simplify migration step code.
Remove 11 unit test files (85 tests) and 2 plan documents. Add
ValidationCapabilitiesTest with 29 feature tests covering text,
numeric, selection, required, and date edge cases through real
Livewire form submission.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

parseDateConstraint was outputting relative_value/relative_unit/direction
keys which don't match DateConstraintValue DTO that expects
anchor/offset/offset_unit/offset_direction. Updated to produce the
correct format.
Replace direct CustomField::query() and new CustomField/CustomFieldValue
with CustomFields::newCustomFieldModel() and CustomFields::newValueModel()
to respect configurable model overrides. Add architecture test with
datasets to enforce this pattern across all 4 configurable models.
Remove IntegerOnlyCapability from NumberFieldType and use
DecimalPlacesCapability instead (decimal_places=0 for integers).
Limit decimal places to 0-4 with placeholder hint. Update migration
step to map legacy 'integer' rule to decimal_places=0.
Apply Rector auto-fixes (FQCN imports, sprintf conversions, newlines).
Remove stale phpstan ignore patterns no longer needed after Rector fixes.
The argument.type errors in Infolists and Table Columns are caused by
Filament's view-string type declarations varying across versions. Use
reportUnmatched: false so it works on both local and CI environments.
@ManukMinasyan ManukMinasyan merged commit ab7a413 into 3.x Feb 24, 2026
3 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 78 out of 78 changed files in this pull request and generated 2 comments.

->withValidationCapabilities(
MinValueCapability::class,
MaxValueCapability::class,
DecimalPlacesCapability::class,
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

NumberFieldType uses FieldSchema::numeric() (stored via FieldDataType::NUMERICinteger_value and SafeValueConverter::toSafeInteger()), but enables DecimalPlacesCapability. When decimal_places > 0, the generated decimal:* rule can allow decimals that will then be truncated when persisted, causing silent data loss. Either restrict this capability to float-backed field types, or change the number field’s data type/storage to float when decimals are supported.

Suggested change
DecimalPlacesCapability::class,

Copilot uses AI. Check for mistakes.
Comment on lines 32 to 36
public function getValidationRules(CustomField $customField, string|int|null $ignoreEntityId = null): array
{
// Convert user rules to Laravel validator format
$userRules = $this->convertUserRulesToValidatorFormat($customField->validation_rules, $customField);
// Get capability-based rules from stored values
$capabilityRules = $this->getCapabilityRules($customField);

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

getValidationRules() no longer adds the field-level required constraint to the returned rule list. This is fine for Filament forms that also call ->required(...), but callers that only use getValidationRules() (e.g. import column rules) will stop enforcing required fields. Consider prepending 'required' when $this->isRequired($customField) is true (or providing a dedicated method/flag for non-Filament contexts).

Copilot uses AI. Check for mistakes.
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