Skip to content

Commit a0a6a24

Browse files
Merge pull request #90 from Relaticle/fix/unique-field-value-normalization
fix: normalize values in unique field validation and wire ignoreEntityId
2 parents 1533348 + 6f356a1 commit a0a6a24

File tree

17 files changed

+476
-122
lines changed

17 files changed

+476
-122
lines changed

.github/workflows/run-tests.yml

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,24 @@ name: run-tests
22

33
on:
44
push:
5-
branches: [2.x]
5+
branches: [3.x]
66
pull_request:
7-
branches: [2.x]
7+
branches: [3.x]
88

99
jobs:
10-
test:
11-
runs-on: ${{ matrix.os }}
10+
tests:
11+
runs-on: ubuntu-latest
1212
strategy:
1313
fail-fast: true
1414
matrix:
15-
os: [ubuntu-latest]
1615
php: [8.4]
17-
laravel: [11.*]
16+
laravel: [12.*]
1817
stability: [prefer-stable]
1918
include:
20-
- laravel: 11.*
21-
testbench: 9.*
22-
carbon: 2.*
19+
- laravel: 12.*
20+
testbench: 10.*
2321

24-
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
22+
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }}
2523

2624
steps:
2725
- name: Checkout code
@@ -32,20 +30,21 @@ jobs:
3230
with:
3331
php-version: ${{ matrix.php }}
3432
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
35-
coverage: xdebug
36-
37-
- name: Setup problem matchers
38-
run: |
39-
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
40-
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
33+
coverage: none
4134

4235
- name: Install dependencies
4336
run: |
44-
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update
37+
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
4538
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
4639
47-
- name: List Installed Dependencies
48-
run: composer show -D
40+
- name: Run Pint
41+
run: vendor/bin/pint --test
42+
43+
- name: Run PHPStan
44+
run: vendor/bin/phpstan analyse --no-progress
45+
46+
- name: Run Rector
47+
run: vendor/bin/rector --dry-run --no-progress-bar
4948

50-
- name: Execute tests
51-
run: vendor/bin/pest --ci
49+
- name: Run Pest
50+
run: vendor/bin/pest --ci

phpstan.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@ parameters:
1414
ignoreErrors:
1515
# Ignore unused trait warnings for library traits meant to be consumed by package users
1616
- identifier: trait.unused
17+
# Filament type stubs declare view() as expecting view-string|null
18+
- identifier: argument.type
19+
path: src/Filament/Integration/Components/Infolists/*
20+
- identifier: argument.type
21+
path: src/Filament/Integration/Components/Tables/Columns/*
1722
parallel:
1823
maximumNumberOfProcesses: 3

src/Console/Commands/CleanupOrphanedValuesCommand.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function handle(): int
5050
$class = $morphMap[$type] ?? $type;
5151

5252
if (! class_exists($class)) {
53-
$this->warn("Skipping unknown entity type: {$type}");
53+
$this->warn('Skipping unknown entity type: '.$type);
5454

5555
continue;
5656
}
@@ -61,10 +61,10 @@ public function handle(): int
6161

6262
$orphanedCount = DB::table($table)
6363
->where('entity_type', $type)
64-
->whereNotExists(function ($query) use ($entityTable) {
64+
->whereNotExists(function ($query) use ($entityTable): void {
6565
$query->select(DB::raw(1))
6666
->from($entityTable)
67-
->whereColumn("{$entityTable}.id", 'custom_field_values.entity_id');
67+
->whereColumn($entityTable.'.id', 'custom_field_values.entity_id');
6868
})
6969
->count();
7070

@@ -82,7 +82,7 @@ public function handle(): int
8282

8383
$this->table(['Entity Type', 'Orphaned Values'], $rows);
8484
$this->newLine();
85-
$this->line("Total orphaned values: {$totalOrphaned}");
85+
$this->line('Total orphaned values: '.$totalOrphaned);
8686

8787
if ($isDryRun) {
8888
$this->newLine();
@@ -107,19 +107,19 @@ public function handle(): int
107107

108108
$count = DB::table($table)
109109
->where('entity_type', $type)
110-
->whereNotExists(function ($query) use ($entityTable) {
110+
->whereNotExists(function ($query) use ($entityTable): void {
111111
$query->select(DB::raw(1))
112112
->from($entityTable)
113-
->whereColumn("{$entityTable}.id", 'custom_field_values.entity_id');
113+
->whereColumn($entityTable.'.id', 'custom_field_values.entity_id');
114114
})
115115
->delete();
116116

117-
$this->info("Deleted {$count} orphaned values for {$type}.");
117+
$this->info(sprintf('Deleted %d orphaned values for %s.', $count, $type));
118118
$deleted += $count;
119119
}
120120

121121
$this->newLine();
122-
$this->comment("Cleaned up {$deleted} orphaned custom field values.");
122+
$this->comment(sprintf('Cleaned up %d orphaned custom field values.', $deleted));
123123

124124
return self::SUCCESS;
125125
}

src/Console/Commands/Upgrade/Steps/CleanMultiValueValidationRulesStep.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Relaticle\CustomFields\Console\Commands\Upgrade\UpgradeStep;
99
use Relaticle\CustomFields\Console\Commands\Upgrade\UpgradeStepResult;
1010
use Relaticle\CustomFields\CustomFields;
11+
use Throwable;
1112

1213
/**
1314
* Removes string-only validation rules from multi-value field types.
@@ -71,28 +72,28 @@ public function execute(bool $dryRun, Command $command): UpgradeStepResult
7172
$rules = $field->validation_rules?->toCollection() ?? collect();
7273

7374
$invalidRules = $rules->filter(
74-
fn ($rule) => in_array($rule->name, self::STRING_ONLY_RULES, true)
75+
fn ($rule): bool => in_array($rule->name, self::STRING_ONLY_RULES, true)
7576
);
7677

7778
if ($invalidRules->isEmpty()) {
7879
continue;
7980
}
8081

8182
$ruleNames = $invalidRules->pluck('name')->implode(', ');
82-
$command->line(" Processing field '{$field->name}' (type: {$field->type}, id: {$field->id}): removing [{$ruleNames}]");
83+
$command->line(sprintf(" Processing field '%s' (type: %s, id: %s): removing [%s]", $field->name, $field->type, $field->id, $ruleNames));
8384

8485
if (! $dryRun) {
8586
$cleanedRules = $rules->reject(
86-
fn ($rule) => in_array($rule->name, self::STRING_ONLY_RULES, true)
87+
fn ($rule): bool => in_array($rule->name, self::STRING_ONLY_RULES, true)
8788
)->values()->toArray();
8889

8990
try {
9091
$field->update([
9192
'validation_rules' => $cleanedRules === [] ? null : $cleanedRules,
9293
]);
9394
$processed++;
94-
} catch (\Throwable $e) {
95-
$command->line(" <error>Failed to update field {$field->id}: {$e->getMessage()}</error>");
95+
} catch (Throwable $e) {
96+
$command->line(sprintf(' <error>Failed to update field %s: %s</error>', $field->id, $e->getMessage()));
9697
$failed++;
9798
}
9899
} else {

src/FieldTypeSystem/BaseFieldType.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
use Relaticle\CustomFields\Data\FieldTypeData;
1010

1111
/**
12-
* Abstract base class for Custom Fields field types
13-
* Provides sensible defaults and supports both class-based and inline component definitions
14-
*
1512
* @property-read FieldTypeData $data Field type configuration data with full type hints
1613
*/
1714
abstract class BaseFieldType implements FieldTypeDefinitionInterface
@@ -21,8 +18,21 @@ abstract class BaseFieldType implements FieldTypeDefinitionInterface
2118
abstract public function configure(): FieldSchema;
2219

2320
/**
24-
* Get field type data with proper type hints and caching
21+
* Normalize a value before storage and comparison.
2522
*/
23+
public function setValue(string $value): string
24+
{
25+
return $value;
26+
}
27+
28+
/**
29+
* Transform a stored value for display.
30+
*/
31+
public function getValue(string $value): string
32+
{
33+
return $value;
34+
}
35+
2636
public function getData(): FieldTypeData
2737
{
2838
if (! $this->_data instanceof FieldTypeData) {

src/FieldTypeSystem/Definitions/LinkFieldType.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
use Relaticle\CustomFields\Filament\Integration\Components\Infolists\LinkEntry;
1212
use Relaticle\CustomFields\Filament\Integration\Components\Tables\Columns\LinkColumn;
1313

14-
/**
15-
* ABOUTME: Field type definition for Link fields
16-
* ABOUTME: Provides Link functionality with URL validation and multi-value support
17-
*/
1814
class LinkFieldType extends BaseFieldType
1915
{
2016
public function configure(): FieldSchema
@@ -38,4 +34,9 @@ public function configure(): FieldSchema
3834
ValidationRule::MAX,
3935
]);
4036
}
37+
38+
public function setValue(string $value): string
39+
{
40+
return preg_replace('#^https?://#i', '', trim($value));
41+
}
4142
}

src/Filament/Integration/Base/AbstractFormComponent.php

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ protected function configure(
6969
filled($state)
7070
)
7171
->required($this->validationService->isRequired($customField))
72-
->rules($this->getFieldValidationRules($customField))
72+
->rules(fn (Field $component): array => $this->getFieldValidationRules(
73+
$customField,
74+
$component->getRecord()?->getKey()
75+
))
7376
->columnSpan(
7477
FeatureManager::isEnabled(CustomFieldsFeature::UI_FIELD_WIDTH_CONTROL)
7578
? $customField->width->getSpanValue()
@@ -96,18 +99,21 @@ private function getFieldValue(
9699
mixed $state,
97100
mixed $record
98101
): mixed {
99-
return value(function () use ($customField, $state, $record) {
100-
$value = $record?->getCustomFieldValue($customField) ??
101-
($state ?? ($customField->isMultiChoiceField() ? [] : null));
102-
103-
return $value instanceof Carbon
104-
? $value->format(
105-
$customField->isDateField()
106-
? 'Y-m-d'
107-
: 'Y-m-d H:i:s'
108-
)
109-
: $value;
110-
});
102+
$recordValue = $record?->getCustomFieldValue($customField);
103+
104+
if ($recordValue !== null) {
105+
$value = $recordValue;
106+
} elseif ($state !== null) {
107+
$value = $state;
108+
} else {
109+
$value = $customField->isMultiChoiceField() ? [] : null;
110+
}
111+
112+
if ($value instanceof Carbon) {
113+
return $value->format($customField->isDateField() ? 'Y-m-d' : 'Y-m-d H:i:s');
114+
}
115+
116+
return $value;
111117
}
112118

113119
/**
@@ -136,15 +142,17 @@ private function applyVisibility(
136142
$allFields
137143
);
138144

139-
return in_array($jsExpression, [null, '', '0'], true)
140-
? $field
141-
: $field->live()->visibleJs($jsExpression);
145+
if (blank($jsExpression) || $jsExpression === '0') {
146+
return $field;
147+
}
148+
149+
return $field->live()->visibleJs($jsExpression);
142150
}
143151

144152
/** @return array<int, mixed> */
145-
protected function getFieldValidationRules(CustomField $customField): array
153+
protected function getFieldValidationRules(CustomField $customField, string|int|null $ignoreEntityId = null): array
146154
{
147-
return $this->validationService->getValidationRules($customField);
155+
return $this->validationService->getValidationRules($customField, $ignoreEntityId);
148156
}
149157

150158
/**

src/Filament/Integration/Components/Forms/LinkComponent.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Relaticle\CustomFields\Filament\Integration\Components\Forms;
66

7+
use Relaticle\CustomFields\FieldTypeSystem\FieldManager;
78
use Relaticle\CustomFields\Filament\Integration\Base\AbstractFormComponent;
89
use Relaticle\CustomFields\Filament\Integration\Components\Forms\MultiValueInput\MultiValueInputComponent;
910
use Relaticle\CustomFields\Models\CustomField;
@@ -17,6 +18,8 @@ public function create(CustomField $customField): MultiValueInputComponent
1718
? ($customField->settings->max_values ?? 10)
1819
: 1;
1920

21+
$fieldType = app(FieldManager::class)->getFieldTypeInstance($customField->type);
22+
2023
return MultiValueInputComponent::make($customField->getFieldName())
2124
->url()
2225
->allowMultiple($allowMultiple)
@@ -25,11 +28,12 @@ public function create(CustomField $customField): MultiValueInputComponent
2528
->placeholder(__('custom-fields::custom-fields.link.add_link_placeholder'))
2629
->nestedRecursiveRules(['max:2048', 'regex:/^(https?:\/\/)?([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/'])
2730
->rules(['array', 'max:'.$maxValues])
28-
->dehydrateStateUsing(static fn (mixed $state): array => collect($state)
29-
->map(fn (mixed $v): ?string => preg_replace('#^https?://#i', '', trim((string) $v)))
30-
->filter(fn (mixed $v): bool => filled($v))
31+
->dehydrateStateUsing(fn (mixed $state): array => collect($state)
32+
->map(fn (mixed $v): string => $fieldType
33+
? $fieldType->setValue(trim((string) $v))
34+
: trim((string) $v))
35+
->filter(fn (string $v): bool => filled($v))
3136
->values()
32-
->all()
33-
);
37+
->all());
3438
}
3539
}

src/Filament/Integration/Components/Infolists/RecordEntry.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Filament\Infolists\Components\ViewEntry;
88
use Illuminate\Database\Eloquent\Model;
9+
use InvalidArgumentException;
910
use Relaticle\CustomFields\Data\AvatarConfiguration;
1011
use Relaticle\CustomFields\Facades\Entities;
1112
use Relaticle\CustomFields\Filament\Integration\Base\AbstractInfolistEntry;
@@ -105,7 +106,7 @@ private function getRecordUrl(Model $record, mixed $entity): ?string
105106
}
106107

107108
if (! array_key_exists($recordPage, $resourceClass::getPages())) {
108-
throw new \InvalidArgumentException(sprintf(
109+
throw new InvalidArgumentException(sprintf(
109110
"Entity '%s' has recordPage '%s' but %s does not define a '%s' page. Available pages: %s.",
110111
$entity->getLabelSingular(),
111112
$recordPage,

src/Filament/Integration/Components/Tables/Columns/MultiChoiceColumn.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function make(CustomField $customField): BaseColumn
3939

4040
$remaining = count($values) - $limit;
4141

42-
return [...array_slice($values, 0, $limit), "+{$remaining}"];
42+
return [...array_slice($values, 0, $limit), '+'.$remaining];
4343
});
4444

4545
$column = $this->applyBadgeColorsIfEnabled($column, $customField);

0 commit comments

Comments
 (0)