Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/Console/Commands/MakeFieldTypeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ protected function getConfiguratorForDataType(FieldDataType $dataType): string
FieldDataType::BOOLEAN => 'boolean()',
FieldDataType::SINGLE_CHOICE => 'singleChoice()',
FieldDataType::MULTI_CHOICE => 'multiChoice()',
FieldDataType::FILE => 'file()',
};
}

Expand All @@ -201,6 +202,7 @@ protected function getFormComponentImport(FieldDataType $dataType): string
FieldDataType::BOOLEAN => 'use Filament\Forms\Components\Toggle;',
FieldDataType::SINGLE_CHOICE => 'use Filament\Forms\Components\Select;',
FieldDataType::MULTI_CHOICE => 'use Filament\Forms\Components\CheckboxList;',
FieldDataType::FILE => 'use Filament\Forms\Components\FileUpload;',
};
}

Expand Down Expand Up @@ -246,6 +248,9 @@ protected function getFormComponent(FieldDataType $dataType): string
FieldDataType::MULTI_CHOICE => 'return CheckboxList::make($customField->getFieldName())
->label($customField->name)
->columnSpanFull();',
FieldDataType::FILE => 'return FileUpload::make($customField->getFieldName())
->label($customField->name)
->columnSpanFull();',
};
}

Expand Down
5 changes: 5 additions & 0 deletions src/Enums/FieldDataType.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum FieldDataType: string
case BOOLEAN = 'boolean';
case SINGLE_CHOICE = 'single_choice';
case MULTI_CHOICE = 'multi_choice';
case FILE = 'file';

/**
* Check if this category represents optionable fields.
Expand Down Expand Up @@ -76,6 +77,10 @@ public function getCompatibleOperators(): array
VisibilityOperator::IS_EMPTY,
VisibilityOperator::IS_NOT_EMPTY,
],
self::FILE => [
VisibilityOperator::IS_EMPTY,
VisibilityOperator::IS_NOT_EMPTY,
],
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/FieldTypeSystem/Definitions/FileUploadFieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class FileUploadFieldType extends BaseFieldType
{
public function configure(): FieldSchema
{
return FieldSchema::string()
return FieldSchema::file()
->key('file-upload')
->label('File Upload')
->icon('heroicon-o-paper-clip')
Expand Down
10 changes: 10 additions & 0 deletions src/FieldTypeSystem/FieldSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ public static function string(): self
return new self(FieldDataType::STRING);
}

/**
* Configure for file upload fields.
* Stores file paths in string_value but skips string column validation constraints,
* since validation runs against the uploaded file, not the stored path.
*/
public static function file(): self
{
return new self(FieldDataType::FILE);
}

/**
* Configure for numeric fields
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Models/CustomFieldValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public static function getValueColumn(string $fieldType): string
$dataType = $fieldType->dataType;

return match ($dataType) {
FieldDataType::STRING => 'string_value',
FieldDataType::STRING, FieldDataType::FILE => 'string_value',
FieldDataType::TEXT => 'text_value',
FieldDataType::NUMERIC, FieldDataType::SINGLE_CHOICE => 'integer_value',
FieldDataType::FLOAT => 'float_value',
Expand Down
16 changes: 16 additions & 0 deletions src/Services/ValidationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace Relaticle\CustomFields\Services;

use Relaticle\CustomFields\Data\ValidationRuleData;
use Relaticle\CustomFields\Enums\FieldDataType;
use Relaticle\CustomFields\Enums\ValidationRule;
use Relaticle\CustomFields\Facades\CustomFieldsType;
use Relaticle\CustomFields\FieldTypeSystem\FieldManager;
use Relaticle\CustomFields\Models\CustomField;
use Relaticle\CustomFields\Models\CustomFieldValue;
Expand Down Expand Up @@ -97,6 +99,13 @@ private function convertUserRulesToValidatorFormat(?DataCollection $rules, Custo
*/
public function getDatabaseValidationRules(string $fieldType, bool $isEncrypted = false): array
{
// File types validate the uploaded file, not the stored path
$fieldTypeData = CustomFieldsType::getFieldType($fieldType);

if ($fieldTypeData?->dataType === FieldDataType::FILE) {
Comment on lines +102 to +105
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

CustomFieldsType::getFieldType() delegates to FieldManager::getFieldType(), which rebuilds the full field type collection via toCollection() each call (instantiates all field type definitions). Adding this lookup here introduces a potentially expensive operation on every validation call. Consider avoiding CustomFieldsType::getFieldType() in this hot path by reusing the FieldManager instance/field type instance already accessed in getFieldTypeDefaultRules(), or by passing the resolved dataType/FieldTypeData into getDatabaseValidationRules so this method can decide FILE vs non-FILE without another full rebuild.

Suggested change
// File types validate the uploaded file, not the stored path
$fieldTypeData = CustomFieldsType::getFieldType($fieldType);
if ($fieldTypeData?->dataType === FieldDataType::FILE) {
// Cache the FILE/non-FILE decision per field type to avoid repeated expensive lookups.
static $isFileTypeCache = [];
if (! array_key_exists($fieldType, $isFileTypeCache)) {
$fieldTypeData = CustomFieldsType::getFieldType($fieldType);
$isFileTypeCache[$fieldType] = $fieldTypeData?->dataType === FieldDataType::FILE;
}
// File types validate the uploaded file, not the stored path
if ($isFileTypeCache[$fieldType]) {

Copilot uses AI. Check for mistakes.
return [];
}

// Determine the database column for this field type
$columnName = CustomFieldValue::getValueColumn($fieldType);

Expand Down Expand Up @@ -202,6 +211,13 @@ private function mergeAllValidationRules(array $fieldTypeDefaults, array $userRu
// Add user rules (can override or supplement defaults)
$mergedRules = $this->combineRules($mergedRules, $userRules);

// File types validate the uploaded file, not the stored path — skip DB constraints
$fieldTypeData = CustomFieldsType::getFieldType($fieldType);

if ($fieldTypeData?->dataType === FieldDataType::FILE) {
return $mergedRules;
}
Comment on lines +214 to +219
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This repeats a CustomFieldsType::getFieldType() lookup (which rebuilds the field type collection) even though getDatabaseValidationRules() already performs the same FILE check. To reduce overhead and duplication, consider doing the FILE/skip-DB-constraints decision once (e.g., pass a resolved dataType/flag into mergeAllValidationRules, or have getDatabaseValidationRules return enough information to skip DatabaseFieldConstraints::getConstraintsForColumn() without another field type resolution).

Copilot uses AI. Check for mistakes.

// Apply database constraint rules using existing logic
$columnName = CustomFieldValue::getValueColumn($fieldType);
$dbConstraints = DatabaseFieldConstraints::getConstraintsForColumn($columnName);
Expand Down
72 changes: 72 additions & 0 deletions tests/Feature/Integration/FileUploadValidationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

use Relaticle\CustomFields\Enums\FieldDataType;
use Relaticle\CustomFields\FieldTypeSystem\FieldTypeConfigurator;
use Relaticle\CustomFields\Models\CustomField;
use Relaticle\CustomFields\Models\CustomFieldSection;
use Relaticle\CustomFields\Models\CustomFieldValue;
use Relaticle\CustomFields\Services\ValidationService;
use Relaticle\CustomFields\Tests\Fixtures\Models\Post;
use Relaticle\CustomFields\Tests\Fixtures\Models\User;

beforeEach(function (): void {
$this->user = User::factory()->create();
$this->actingAs($this->user);

config()->set('custom-fields.field_type_configuration', FieldTypeConfigurator::configure()
->enabled([])
->disabled([])
->discover(true)
->cache(enabled: false));

$this->section = CustomFieldSection::factory()
->forEntityType(Post::class)
->create();
});

it('resolves FILE data type to string_value column', function (): void {
$field = CustomField::factory()->create([
'custom_field_section_id' => $this->section->getKey(),
'entity_type' => Post::class,
'code' => 'document',
'type' => 'file-upload',
]);

$column = CustomFieldValue::getValueColumn($field->type);

expect($column)->toBe('string_value');
});

it('does not apply string database constraints to file upload fields', function (): void {
$field = CustomField::factory()->create([
'custom_field_section_id' => $this->section->getKey(),
'entity_type' => Post::class,
'code' => 'attachment',
'type' => 'file-upload',
]);

$service = app(ValidationService::class);
$rules = $service->getValidationRules($field);

expect($rules)
->toContain('file')
->not->toContain('string')
->not->toContain('max:255');
});

it('returns empty database validation rules for file types', function (): void {
$service = app(ValidationService::class);
$dbRules = $service->getDatabaseValidationRules('file-upload');

expect($dbRules)->toBe([]);
});

it('has FILE case in FieldDataType enum', function (): void {
$file = FieldDataType::FILE;

expect($file->value)->toBe('file')
->and($file->isChoiceField())->toBeFalse()
->and($file->isMultiChoiceField())->toBeFalse();
});