Skip to content
41 changes: 20 additions & 21 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@ name: run-tests

on:
push:
branches: [2.x]
branches: [3.x]
pull_request:
branches: [2.x]
branches: [3.x]

jobs:
test:
runs-on: ${{ matrix.os }}
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
php: [8.4]
laravel: [11.*]
laravel: [12.*]
stability: [prefer-stable]
include:
- laravel: 11.*
testbench: 9.*
carbon: 2.*
- laravel: 12.*
testbench: 10.*

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }}

steps:
- name: Checkout code
Expand All @@ -32,20 +30,21 @@ jobs:
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
coverage: xdebug

- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
coverage: none

- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction

- name: List Installed Dependencies
run: composer show -D
- name: Run Pint
run: vendor/bin/pint --test

- name: Run PHPStan
run: vendor/bin/phpstan analyse --no-progress

- name: Run Rector
run: vendor/bin/rector --dry-run --no-progress-bar

- name: Execute tests
run: vendor/bin/pest --ci
- name: Run Pest
run: vendor/bin/pest --ci
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ parameters:
ignoreErrors:
# Ignore unused trait warnings for library traits meant to be consumed by package users
- identifier: trait.unused
# Filament type stubs declare view() as expecting view-string|null
- identifier: argument.type
path: src/Filament/Integration/Components/Infolists/*
- identifier: argument.type
path: src/Filament/Integration/Components/Tables/Columns/*
parallel:
maximumNumberOfProcesses: 3
16 changes: 8 additions & 8 deletions src/Console/Commands/CleanupOrphanedValuesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function handle(): int
$class = $morphMap[$type] ?? $type;

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

continue;
}
Expand All @@ -61,10 +61,10 @@ public function handle(): int

$orphanedCount = DB::table($table)
->where('entity_type', $type)
->whereNotExists(function ($query) use ($entityTable) {
->whereNotExists(function ($query) use ($entityTable): void {
$query->select(DB::raw(1))
->from($entityTable)
->whereColumn("{$entityTable}.id", 'custom_field_values.entity_id');
->whereColumn($entityTable.'.id', 'custom_field_values.entity_id');
})
->count();

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

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

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

$count = DB::table($table)
->where('entity_type', $type)
->whereNotExists(function ($query) use ($entityTable) {
->whereNotExists(function ($query) use ($entityTable): void {
$query->select(DB::raw(1))
->from($entityTable)
->whereColumn("{$entityTable}.id", 'custom_field_values.entity_id');
->whereColumn($entityTable.'.id', 'custom_field_values.entity_id');
})
->delete();

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

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

return self::SUCCESS;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Relaticle\CustomFields\Console\Commands\Upgrade\UpgradeStep;
use Relaticle\CustomFields\Console\Commands\Upgrade\UpgradeStepResult;
use Relaticle\CustomFields\CustomFields;
use Throwable;

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

$invalidRules = $rules->filter(
fn ($rule) => in_array($rule->name, self::STRING_ONLY_RULES, true)
fn ($rule): bool => in_array($rule->name, self::STRING_ONLY_RULES, true)
);

if ($invalidRules->isEmpty()) {
continue;
}

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

if (! $dryRun) {
$cleanedRules = $rules->reject(
fn ($rule) => in_array($rule->name, self::STRING_ONLY_RULES, true)
fn ($rule): bool => in_array($rule->name, self::STRING_ONLY_RULES, true)
)->values()->toArray();

try {
$field->update([
'validation_rules' => $cleanedRules === [] ? null : $cleanedRules,
]);
$processed++;
} catch (\Throwable $e) {
$command->line(" <error>Failed to update field {$field->id}: {$e->getMessage()}</error>");
} catch (Throwable $e) {
$command->line(sprintf(' <error>Failed to update field %s: %s</error>', $field->id, $e->getMessage()));
$failed++;
}
} else {
Expand Down
18 changes: 14 additions & 4 deletions src/FieldTypeSystem/BaseFieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
use Relaticle\CustomFields\Data\FieldTypeData;

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

/**
* Get field type data with proper type hints and caching
* Normalize a value before storage and comparison.
*/
public function setValue(string $value): string
{
return $value;
}

/**
* Transform a stored value for display.
*/
public function getValue(string $value): string
{
return $value;
}

public function getData(): FieldTypeData
{
if (! $this->_data instanceof FieldTypeData) {
Expand Down
9 changes: 5 additions & 4 deletions src/FieldTypeSystem/Definitions/LinkFieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
use Relaticle\CustomFields\Filament\Integration\Components\Infolists\LinkEntry;
use Relaticle\CustomFields\Filament\Integration\Components\Tables\Columns\LinkColumn;

/**
* ABOUTME: Field type definition for Link fields
* ABOUTME: Provides Link functionality with URL validation and multi-value support
*/
class LinkFieldType extends BaseFieldType
{
public function configure(): FieldSchema
Expand All @@ -38,4 +34,9 @@ public function configure(): FieldSchema
ValidationRule::MAX,
]);
}

public function setValue(string $value): string
{
return preg_replace('#^https?://#i', '', trim($value));
}
}
44 changes: 26 additions & 18 deletions src/Filament/Integration/Base/AbstractFormComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ protected function configure(
filled($state)
)
->required($this->validationService->isRequired($customField))
->rules($this->getFieldValidationRules($customField))
->rules(fn (Field $component): array => $this->getFieldValidationRules(
$customField,
$component->getRecord()?->getKey()
))
->columnSpan(
FeatureManager::isEnabled(CustomFieldsFeature::UI_FIELD_WIDTH_CONTROL)
? $customField->width->getSpanValue()
Expand All @@ -96,18 +99,21 @@ private function getFieldValue(
mixed $state,
mixed $record
): mixed {
return value(function () use ($customField, $state, $record) {
$value = $record?->getCustomFieldValue($customField) ??
($state ?? ($customField->isMultiChoiceField() ? [] : null));

return $value instanceof Carbon
? $value->format(
$customField->isDateField()
? 'Y-m-d'
: 'Y-m-d H:i:s'
)
: $value;
});
$recordValue = $record?->getCustomFieldValue($customField);

if ($recordValue !== null) {
$value = $recordValue;
} elseif ($state !== null) {
$value = $state;
} else {
$value = $customField->isMultiChoiceField() ? [] : null;
}

if ($value instanceof Carbon) {
return $value->format($customField->isDateField() ? 'Y-m-d' : 'Y-m-d H:i:s');
}

return $value;
}

/**
Expand Down Expand Up @@ -136,15 +142,17 @@ private function applyVisibility(
$allFields
);

return in_array($jsExpression, [null, '', '0'], true)
? $field
: $field->live()->visibleJs($jsExpression);
if (blank($jsExpression) || $jsExpression === '0') {
return $field;
}

return $field->live()->visibleJs($jsExpression);
}

/** @return array<int, mixed> */
protected function getFieldValidationRules(CustomField $customField): array
protected function getFieldValidationRules(CustomField $customField, string|int|null $ignoreEntityId = null): array
{
return $this->validationService->getValidationRules($customField);
return $this->validationService->getValidationRules($customField, $ignoreEntityId);
}

/**
Expand Down
14 changes: 9 additions & 5 deletions src/Filament/Integration/Components/Forms/LinkComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

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

$fieldType = app(FieldManager::class)->getFieldTypeInstance($customField->type);

return MultiValueInputComponent::make($customField->getFieldName())
->url()
->allowMultiple($allowMultiple)
Expand All @@ -25,11 +28,12 @@ public function create(CustomField $customField): MultiValueInputComponent
->placeholder(__('custom-fields::custom-fields.link.add_link_placeholder'))
->nestedRecursiveRules(['max:2048', 'regex:/^(https?:\/\/)?([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/'])
->rules(['array', 'max:'.$maxValues])
->dehydrateStateUsing(static fn (mixed $state): array => collect($state)
->map(fn (mixed $v): ?string => preg_replace('#^https?://#i', '', trim((string) $v)))
->filter(fn (mixed $v): bool => filled($v))
->dehydrateStateUsing(fn (mixed $state): array => collect($state)
->map(fn (mixed $v): string => $fieldType
? $fieldType->setValue(trim((string) $v))
: trim((string) $v))
->filter(fn (string $v): bool => filled($v))
->values()
->all()
);
->all());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Filament\Infolists\Components\ViewEntry;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
use Relaticle\CustomFields\Data\AvatarConfiguration;
use Relaticle\CustomFields\Facades\Entities;
use Relaticle\CustomFields\Filament\Integration\Base\AbstractInfolistEntry;
Expand Down Expand Up @@ -105,7 +106,7 @@ private function getRecordUrl(Model $record, mixed $entity): ?string
}

if (! array_key_exists($recordPage, $resourceClass::getPages())) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
"Entity '%s' has recordPage '%s' but %s does not define a '%s' page. Available pages: %s.",
$entity->getLabelSingular(),
$recordPage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function make(CustomField $customField): BaseColumn

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

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

$column = $this->applyBadgeColorsIfEnabled($column, $customField);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Filament\Tables\Columns\Column;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
use Relaticle\CustomFields\Data\AvatarConfiguration;
use Relaticle\CustomFields\Facades\Entities;
use Relaticle\CustomFields\Filament\Integration\Base\AbstractTableColumn;
Expand Down Expand Up @@ -144,7 +145,7 @@ private function getRecordUrl(Model $record): ?string
}

if (! array_key_exists($recordPage, $resourceClass::getPages())) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
"Entity '%s' has recordPage '%s' but %s does not define a '%s' page. Available pages: %s.",
$this->entity->getLabelSingular(),
$recordPage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,7 @@ private function resolveLookupValue(CustomField $customField, mixed $value): int
throw $throwable;
}

throw new RowImportFailedException(
'Error resolving lookup value: '.$throwable->getMessage()
);
throw new RowImportFailedException('Error resolving lookup value: '.$throwable->getMessage(), $throwable->getCode(), $throwable);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Filament/Management/Schemas/FieldForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public static function schema(bool $withOptionsRelationship = true): array
->required()
->columnSpan(9)
->rules([
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get): void {
if (blank($value)) {
return;
}
Expand Down
Loading