Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3cac62a
docs: add validation capabilities system design
ManukMinasyan Feb 16, 2026
bb79a5a
docs: add validation capabilities implementation plan
ManukMinasyan Feb 16, 2026
d5823f4
feat: add DateConstraintMode and DateUnit enums
ManukMinasyan Feb 16, 2026
f93f03e
feat: add DateConstraintValue value object with relative date resolution
ManukMinasyan Feb 16, 2026
d61ccf7
feat: add ValidationCapability interface
ManukMinasyan Feb 16, 2026
60598f3
feat: add validation capability registration to FieldSchema
ManukMinasyan Feb 16, 2026
6583028
feat: add text length validation capabilities
ManukMinasyan Feb 16, 2026
51b2371
feat: add selection count validation capabilities
ManukMinasyan Feb 16, 2026
6f30046
feat: add file validation capabilities
ManukMinasyan Feb 16, 2026
ea5fb6a
feat: add date validation capabilities with relative date support
ManukMinasyan Feb 16, 2026
613bb56
refactor: update model cast and ValidationService to use capabilities
ManukMinasyan Feb 16, 2026
a58e777
feat: wire validation capabilities into form component rendering
ManukMinasyan Feb 16, 2026
f868c13
feat: replace validation tab with capability-rendered settings
ManukMinasyan Feb 16, 2026
b00d600
feat: add MigrateValidationRulesFormatStep for validation rules migra…
ManukMinasyan Feb 17, 2026
07caadf
refactor: remove legacy ValidationRule enum and generic validation re…
ManukMinasyan Feb 17, 2026
10886c5
fix: resolve phpstan warnings in validation capabilities code
ManukMinasyan Feb 17, 2026
eb97b55
refactor: move validation settings inline and improve field form UX
ManukMinasyan Feb 18, 2026
0fd0c47
refactor: simplify date validation to relative-only with direction su…
ManukMinasyan Feb 18, 2026
272e048
docs: delete outdated design and implementation docs for validation c…
ManukMinasyan Feb 19, 2026
5392bb1
Merge branch '3.x' into feat/validation-capabilities
ManukMinasyan Feb 19, 2026
c33a9c6
Merge branch '3.x' into feat/validation-capabilities
ManukMinasyan Feb 23, 2026
fc9164a
fix: address PR review feedback and improve validation architecture
ManukMinasyan Feb 23, 2026
3afd506
docs: add date validation redesign design doc
ManukMinasyan Feb 23, 2026
247bdf0
docs: add date validation implementation plan
ManukMinasyan Feb 23, 2026
2f1db34
feat: add DateAnchor and DateOffsetDirection enums, remove DateDirection
ManukMinasyan Feb 23, 2026
5c7fc8f
feat: rewrite DateConstraintValue with anchor-based resolution
ManukMinasyan Feb 23, 2026
4cb7436
feat: add DateConstraintRule for submit-time date validation
ManukMinasyan Feb 23, 2026
faf2759
feat: update date capabilities to use anchor-based resolution and Dat…
ManukMinasyan Feb 23, 2026
081aebb
feat: rewrite DateConstraintField with preset-driven UI
ManukMinasyan Feb 23, 2026
1b0dc8d
feat: add reactive field references for cross-field date constraints
ManukMinasyan Feb 23, 2026
935eeec
fix: resolve phpstan error for model created_at access
ManukMinasyan Feb 23, 2026
dcc7457
test: add feature tests for date validation management and integration
ManukMinasyan Feb 23, 2026
2ca0d9a
fix: resolve empty reference field dropdown in date constraint UI
ManukMinasyan Feb 23, 2026
12d33c8
fix: guard against legacy validation data missing anchor key
ManukMinasyan Feb 24, 2026
72f7f78
test: add E2E date validation tests and legacy data regression tests
ManukMinasyan Feb 24, 2026
0668a6b
fix: resolve all phpstan errors in validation capabilities
ManukMinasyan Feb 24, 2026
c2ead99
refactor: replace unit tests with feature-level integration tests
ManukMinasyan Feb 24, 2026
d13b000
fix: migration step outputs correct date constraint format
ManukMinasyan Feb 24, 2026
c757a9c
refactor: use CustomFields facade for all model instantiation
ManukMinasyan Feb 24, 2026
4b1237f
Merge remote-tracking branch 'origin/3.x' into feat/validation-capabi…
ManukMinasyan Feb 24, 2026
9c09f82
refactor: replace IntegerOnlyCapability with DecimalPlacesCapability
ManukMinasyan Feb 24, 2026
775305d
chore: fix rector and phpstan issues for clean CI pipeline
ManukMinasyan Feb 24, 2026
9ae827c
fix: restore phpstan ignores for Filament view-string type mismatch
ManukMinasyan Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 6 additions & 42 deletions database/factories/CustomFieldFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,11 @@ public function definition(): array
/**
* Configure the field with specific validation rules.
*
* @param array<string|array{name: string, parameters: array}> $rules
* @param array<string, mixed> $rules
*/
public function withValidation(array $rules): self
{
return $this->state(function (array $attributes) use ($rules) {
$validationRules = collect($rules)->map(function ($rule) {
if (is_string($rule)) {
return ['name' => $rule, 'parameters' => []];
}

return $rule;
})->toArray();

return ['validation_rules' => $validationRules];
});
return $this->state(fn () => ['validation_rules' => $rules]);
}

/**
Expand Down Expand Up @@ -180,35 +170,9 @@ public function systemDefined(): self
*/
public function ofType(string $type): self
{
$defaultValidation = match ($type) {
'text' => [
['name' => 'string', 'parameters' => []],
['name' => 'max', 'parameters' => [255]],
],
'number' => [
['name' => 'numeric', 'parameters' => []],
],
'link' => [
['name' => 'url', 'parameters' => []],
],
'date' => [
['name' => 'date', 'parameters' => []],
],
'checkbox', 'toggle' => [
['name' => 'boolean', 'parameters' => []],
],
'select', 'radio' => [
['name' => 'in', 'parameters' => ['option1', 'option2', 'option3']],
],
'multi_select', 'checkbox_list', 'tags_input' => [
['name' => 'array', 'parameters' => []],
],
default => [],
};

return $this->state([
'type' => $type,
'validation_rules' => $defaultValidation,
'validation_rules' => [],
]);
}

Expand All @@ -219,7 +183,7 @@ public function required(): self
{
return $this->state(function (array $attributes) {
$validationRules = $attributes['validation_rules'] ?? [];
array_unshift($validationRules, ['name' => 'required', 'parameters' => []]);
$validationRules['required'] = true;

return ['validation_rules' => $validationRules];
});
Expand All @@ -234,11 +198,11 @@ public function withLength(?int $min = null, ?int $max = null): self
$validationRules = $attributes['validation_rules'] ?? [];

if ($min !== null) {
$validationRules[] = ['name' => 'min', 'parameters' => [$min]];
$validationRules['min_length'] = $min;
}

if ($max !== null) {
$validationRules[] = ['name' => 'max', 'parameters' => [$max]];
$validationRules['max_length'] = $max;
}

return ['validation_rules' => $validationRules];
Expand Down
235 changes: 235 additions & 0 deletions docs/plans/2026-02-17-validation-capabilities-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# Validation Capabilities System

Replace the generic `ValidationRule` enum + repeater with field-type-owned validation capabilities declared on `FieldSchema`.

## Problem

The current validation system exposes raw Laravel validation rules (after, before, min, max, etc.) via a generic repeater UI. Users type string parameters into text inputs. This creates:

- Poor UX: users must know Laravel validation syntax
- No UI constraints: date pickers show all dates, validation only fails on submit
- No relative date support beyond `today`/`tomorrow`/`yesterday`
- A 100+ case enum (`ValidationRule`) where each field type uses ~3-5 rules

## Solution

Each field type declares **validation capabilities** on `FieldSchema`. Each capability is a self-contained class that owns its admin UI, Filament component application, and Laravel rule generation.

### FieldSchema API

```php
// DateFieldType::configure()
FieldSchema::date()
->key('date')
->label('Date')
->canHaveMinDate()
->canHaveMaxDate();

// NumberFieldType::configure()
FieldSchema::number()
->key('number')
->label('Number')
->canHaveMinValue()
->canHaveMaxValue()
->canBeIntegerOnly();

// TextFieldType::configure()
FieldSchema::text()
->key('text')
->label('Text')
->canHaveMinLength()
->canHaveMaxLength();
```

Capability methods are added to the existing `FieldSchema` fluent builder alongside `->searchable()`, `->filterable()`, etc.

### Capability Contract

```php
interface ValidationCapability
{
public function key(): string;
public function label(): string;
public function formSchema(): array;
public function applyToComponent(Field $component, mixed $value): void;
public function toRules(mixed $value): array;
}
```

Each capability:
1. **`formSchema()`** — Returns Filament components for the admin Validation tab
2. **`applyToComponent()`** — Calls Filament's dual-effect methods (e.g., `->minDate()` sets UI constraint AND adds validation rule)
3. **`toRules()`** — Produces Laravel rules for non-Filament contexts (API, CSV import)

### Auto-wiring

The system automatically:
- Renders the Validation tab by iterating the field type's declared capabilities
- Applies capabilities to end-user form components via `AbstractFormComponent::configure()`
- Collects Laravel rules from capabilities for non-Filament validation

Field type authors just chain capability methods — no Data classes, no form component changes, no rule conversion logic.

## Capability Inventory

### 12 built-in capabilities

| Capability | Key | Field Types | Filament Method | Laravel Rule |
|---|---|---|---|---|
| `MinDate` | `min_date` | Date, DateTime | `->minDate()` | `after_or_equal:{date}` |
| `MaxDate` | `max_date` | Date, DateTime | `->maxDate()` | `before_or_equal:{date}` |
| `MinValue` | `min_value` | Number, Currency | `->minValue()` | `min:{value}` |
| `MaxValue` | `max_value` | Number, Currency | `->maxValue()` | `max:{value}` |
| `IntegerOnly` | `integer_only` | Number | `->integer()` | `integer` |
| `DecimalPlaces` | `decimal_places` | Currency | `->decimal()` | `decimal:0,{places}` |
| `MinLength` | `min_length` | Text, Textarea, Markdown, RichEditor, Link, Email, Phone | `->minLength()` | `min:{length}` |
| `MaxLength` | `max_length` | Text, Textarea, Markdown, RichEditor, Link, Email, Phone | `->maxLength()` | `max:{length}` |
| `MinSelections` | `min_selections` | MultiSelect, CheckboxList, TagsInput, Record | `->minItems()` | `min:{count}` |
| `MaxSelections` | `max_selections` | MultiSelect, CheckboxList, TagsInput, Record | `->maxItems()` | `max:{count}` |
| `AcceptedFileTypes` | `accepted_types` | FileUpload | `->acceptedFileTypes()` | `mimes:{types}` |
| `MaxFileSize` | `max_size_kb` | FileUpload | `->maxSize()` | `max:{kb}` |

### Field types with no capabilities

Toggle, Checkbox, Select, Radio, ToggleButtons, ColorPicker — Validation tab shows only the base-level "Required" toggle.

## Base-level Settings

`required` and `unique_per_entity_type` are handled by `AbstractFormComponent::configure()`, not by capabilities. Available for all field types.

## Storage

### Column

`validation_rules` — existing column, no schema change.

### Cast

`AsCollection::class` — null-safe, supports `->get()`, `->has()`, `->keys()`.

### Format

Old format (removed):
```json
[{"name": "required", "parameters": []}, {"name": "min", "parameters": [{"value": "5"}]}]
```

New format:
```json
{"required": true, "min_length": 5}
```

Date with relative value:
```json
{
"required": true,
"min_date": {"mode": "relative", "value": 7, "unit": "days"},
"max_date": {"mode": "absolute", "value": "2026-12-31"}
}
```

Each capability reads/writes its own key from the collection.

## Date Constraint Value Object

```php
class DateConstraintValue extends Data
{
public DateConstraintMode $mode; // Absolute | Relative
public ?string $absoluteValue = null; // Y-m-d
public ?int $relativeValue = null; // e.g., 7
public ?DateUnit $relativeUnit = null; // Days | Weeks | Months | Years

public function resolve(): Carbon { ... }
}
```

### Enums

- `DateConstraintMode` — `Absolute`, `Relative`
- `DateUnit` — `Days`, `Weeks`, `Months`, `Years`

### Admin UI Component

`DateConstraintField` — reusable Filament form component. Toggle between absolute (date picker) and relative (number input + unit select). Direction is implicit: min_date is future-relative, max_date can be either.

## Extensibility

Third-party developers can create custom capabilities:

```php
class MyCustomCapability implements ValidationCapability
{
public function key(): string { return 'my_rule'; }
public function label(): string { return 'My Rule'; }
public function formSchema(): array { return [...]; }
public function applyToComponent(Field $component, mixed $value): void { ... }
public function toRules(mixed $value): array { return [...]; }
}
```

Register on a custom field type:
```php
FieldSchema::text()
->key('my-field')
->withValidationCapability(MyCustomCapability::class);
```

## What Gets Removed

- `ValidationRule` enum (entire file, 100+ cases)
- `ValidationRuleData` DTO
- `CustomFieldValidationComponent` (generic repeater)
- `ValidationService::convertUserRulesToValidatorFormat()`
- All parameter validation/normalization/help-text methods
- Translation keys for validation rule labels/descriptions

## What Gets Modified

- `FieldSchema` — add capability methods, remove `availableValidationRules()`/`defaultValidationRules()`
- `FieldTypeData` — carry registered capabilities instead of validation rule list
- `CustomField` model — change `validation_rules` cast from `DataCollection` to `AsCollection`
- `ValidationService` — simplify to handle base-level rules + iterate capabilities for rule generation
- `AbstractFormComponent::configure()` — iterate capabilities, call `applyToComponent()`
- All 22 field type definitions — replace `availableValidationRules([...])` with capability methods
- `FieldForm` (management form) — Validation tab renders capability form schemas

## Upgrade Path

### Data Migration

New `MigrateValidationRulesFormatStep` in existing `UpgradeCommand`:

1. Read each field's `type` + old `[{name, parameters}]` format
2. Convert to new `{key: value}` format based on field type context:
- `{"name": "required"}` -> `{"required": true}`
- `{"name": "min", "parameters": [{"value": "5"}]}` on text -> `{"min_length": 5}`
- `{"name": "min", "parameters": [{"value": "5"}]}` on number -> `{"min_value": 5.0}`
- `{"name": "after", "parameters": [{"value": "today"}]}` -> `{"min_date": {"mode": "relative", "value": 0, "unit": "days"}}`
- `{"name": "after", "parameters": [{"value": "2026-01-01"}]}` -> `{"min_date": {"mode": "absolute", "value": "2026-01-01"}}`
3. Unmappable rules (dropped format rules: alpha, starts_with, etc.) logged as warnings, discarded
4. Dry-run and skip support per existing pattern

### Schema Migration

None required. Same column, new format.

### Forward Compatibility

- New capability = new key in JSON. Fields without it have no key — capability returns null.
- Removed capability = orphaned key in JSON, never read, harmless.

## Decisions Log

| Decision | Choice | Rationale |
|---|---|---|
| Validation ownership | Field types via capabilities | Better UX, type-safe, Filament-native |
| Relative date input | Structured number + unit | User-friendly, error-proof |
| Relative date reference | Always today | Simplicity, covers 95% of cases |
| Time units | Days, weeks, months, years | Practical coverage |
| Settings classes | None — capabilities handle typing | Less boilerplate, capabilities are self-contained |
| Storage column | Keep `validation_rules` | Zero schema migration, clear separation |
| Column cast | `AsCollection` | Null-safe, method chaining |
| Text format rules | Dropped | Rarely used, aggressive simplification |
| Scope | All 22 field types at once | Clean v3 cut, no hybrid state |
| Base-level settings | `required`, `unique_per_entity_type` | Universal, handled by base class |
Loading