diff --git a/src/Fields/Fieldtype.php b/src/Fields/Fieldtype.php index 2195d284c73..ed8e493b5b9 100644 --- a/src/Fields/Fieldtype.php +++ b/src/Fields/Fieldtype.php @@ -4,6 +4,7 @@ use Facades\Statamic\Fields\FieldtypeRepository; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Support\Collection; use Statamic\Extend\HasHandle; use Statamic\Extend\RegistersItself; use Statamic\Facades\Blink; @@ -11,6 +12,7 @@ use Statamic\Query\Scopes\Filters\Fields\FieldtypeFilter; use Statamic\Statamic; use Statamic\Support\Str; +use WeakMap; use function Statamic\trans as __; @@ -38,6 +40,7 @@ abstract class Fieldtype implements Arrayable protected $defaultValue; protected $configFields = []; protected static $extraConfigFields = []; + private static ?WeakMap $configDefaults = null; protected $icon; public static function title() @@ -378,15 +381,54 @@ public function config(?string $key = null, $fallback = null) } $fieldConfig = $this->field->config(); + $fields = $this->configFields()->all(); - $config = $this->configFields()->all() - ->reject(fn ($field, $handle) => array_key_exists($handle, $fieldConfig)) - ->map->defaultValue() - ->merge($fieldConfig); + // Most calls ask for a single key, so answer those without building the whole config. + if (is_string($key) && $key !== '') { + if (array_key_exists($key, $fieldConfig)) { + return $fieldConfig[$key] ?? $fallback; + } + + return ($fields->has($key) ? self::configDefault($fields, $key) : null) ?? $fallback; + } + + $config = []; + + foreach ($fields as $handle => $field) { + if (! array_key_exists($handle, $fieldConfig)) { + $config[$handle] = self::configDefault($fields, $handle); + } + } + + $config = array_merge($config, $fieldConfig); return $key - ? ($config->get($key) ?? $fallback) - : $config->all(); + ? ($config[$key] ?? $fallback) + : $config; + } + + /** + * A config field's default value. It's worked out once per config fields + * collection, which Blink keeps for the request. Computed defaults are + * still resolved on every read. + */ + private static function configDefault(Collection $fields, string $handle) + { + $field = $fields->get($handle); + + if ($field->hasComputedDefault()) { + return $field->defaultValue(); + } + + self::$configDefaults ??= new WeakMap; + $defaults = self::$configDefaults[$fields] ?? []; + + if (! array_key_exists($handle, $defaults)) { + $defaults[$handle] = $field->defaultValue(); + self::$configDefaults[$fields] = $defaults; + } + + return $defaults[$handle]; } public function preload() diff --git a/src/Fieldtypes/HasSelectOptions.php b/src/Fieldtypes/HasSelectOptions.php index 0f04e6a38d7..338fa7af5de 100644 --- a/src/Fieldtypes/HasSelectOptions.php +++ b/src/Fieldtypes/HasSelectOptions.php @@ -145,9 +145,13 @@ private function getLabel($actualValue) $value = $this->castFromBoolean($value); } - $option = collect($this->getOptions())->filter(fn ($option) => $option['value'] === $value)->first(); + foreach ($this->getOptions() as $option) { + if ($option['value'] === $value) { + return $option['label'] ?? $option['value']; + } + } - return $option ? ($option['label'] ?? $option['value']) : $actualValue; + return $actualValue; } private function castToBoolean($value) diff --git a/tests/Fields/FieldtypeTest.php b/tests/Fields/FieldtypeTest.php index e0bc5d2d0dd..c5a0cc3dad3 100644 --- a/tests/Fields/FieldtypeTest.php +++ b/tests/Fields/FieldtypeTest.php @@ -7,6 +7,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use Statamic\Facades\Blink; +use Statamic\Facades\Field as FieldFacade; use Statamic\Fields\Blueprint; use Statamic\Fields\ConfigFields; use Statamic\Fields\Field; @@ -793,6 +795,43 @@ public function it_only_computes_default_values_for_config_fields_missing_from_t $this->assertSame(1, FieldtypeWithCountedDefaultValue::$timesDefaultValueWasComputed); } + #[Test] + public function config_field_default_values_are_only_computed_once_per_request() + { + FieldtypeWithCountedDefaultValue::$timesDefaultValueWasComputed = 0; + + (new FieldtypeWithCountedDefaultValue)::register(); + + foreach (range(1, 3) as $i) { + $fieldtype = (new TestFieldtypeWithCountedConfigField)->setField(new Field('test', [])); + + $this->assertEquals('default!', $fieldtype->config('alfa')); + $this->assertEquals(['alfa' => 'default!'], $fieldtype->config()); + } + + $this->assertSame(1, FieldtypeWithCountedDefaultValue::$timesDefaultValueWasComputed); + + Blink::flush(); + + $this->assertEquals('default!', (new TestFieldtypeWithCountedConfigField)->setField(new Field('test', []))->config('alfa')); + $this->assertSame(2, FieldtypeWithCountedDefaultValue::$timesDefaultValueWasComputed); + } + + #[Test] + public function computed_config_field_defaults_are_resolved_on_every_read() + { + $count = 0; + FieldFacade::computedDefault('counted', function () use (&$count) { + return 'computed '.++$count; + }); + + $fieldtype = (new TestFieldtypeWithComputedConfigField)->setField(new Field('test', [])); + + $this->assertEquals('computed 1', $fieldtype->config('alfa')); + $this->assertEquals('computed 2', $fieldtype->config('alfa')); + $this->assertEquals(['alfa' => 'computed 3'], $fieldtype->config()); + } + #[Test] #[Group('graphql')] public function it_gets_the_graphql_type_of_string_by_default() @@ -1002,6 +1041,16 @@ class TestFieldtypeWithCountedConfigField extends Fieldtype ]; } +class TestFieldtypeWithComputedConfigField extends Fieldtype +{ + protected $configFields = [ + 'alfa' => [ + 'type' => 'text', + 'default' => 'computed:counted', + ], + ]; +} + class FieldtypeWithCountedDefaultValue extends Fieldtype { protected static $handle = 'counted_default';