From fb3110e6974f5e4fc1f353ffcb677a0adb4729e5 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 6 Nov 2026 20:21:20 +0000 Subject: [PATCH] Work out fieldtype config defaults once per request Fieldtype::config() rebuilt the whole config (every config field's default, merged with the field's own config) as collections on each call, even when asking for a single key. Single-key reads now return the field's own value or that config field's default directly, and each default is worked out once per config fields collection, which Blink already keeps for the request. Computed defaults are still resolved on every read. HasSelectOptions looks up an option's label with a loop instead of building a filtered collection. --- src/Fields/Fieldtype.php | 54 +++++++++++++++++++++++++---- src/Fieldtypes/HasSelectOptions.php | 8 +++-- tests/Fields/FieldtypeTest.php | 49 ++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 8 deletions(-) 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';