diff --git a/packages/schemas/src/Concerns/HasState.php b/packages/schemas/src/Concerns/HasState.php index 696e9ac298..b41a041b6d 100644 --- a/packages/schemas/src/Concerns/HasState.php +++ b/packages/schemas/src/Concerns/HasState.php @@ -284,6 +284,36 @@ trait HasState return $state; } + /** + * Takes the raw Livewire state and prunes it to only contain keys + * present in the template array. This preserves the sparse structure + * of validated data while using clean (unmutated) values from Livewire. + * + * @param array $source + * @param array $template + * @return array + */ + protected function pruneStateToMatchKeys(array $source, array $template): array + { + $result = []; + + foreach ($template as $key => $templateValue) { + if (! array_key_exists($key, $source)) { + $result[$key] = $templateValue; + + continue; + } + + if (is_array($templateValue) && is_array($source[$key])) { + $result[$key] = $this->pruneStateToMatchKeys($source[$key], $templateValue); + } else { + $result[$key] = $source[$key]; + } + } + + return $result; + } + /** * @param array | null $state */ @@ -417,6 +447,34 @@ trait HasState return Component::withVisibilityCache(function () use ($shouldCallHooksBefore, $afterValidate): array { $state = $this->validate(); + // `validate()` returns data that went through `prepareForValidation()`, + // which applies `mutateStateForValidation()` mutations. Those mutations + // should only affect validation rules, not the dehydrated output. Replace + // the mutated values with clean Livewire data, preserving the validated + // array's sparse key structure. + $statePath = $this->getStatePath(); + $livewire = $this->getLivewire(); + + if (filled($statePath)) { + $rawState = data_get($livewire, $statePath); + + if (is_array($rawState)) { + $validatedFormData = data_get($state, $statePath); + + if (is_array($validatedFormData)) { + data_set($state, $statePath, $this->pruneStateToMatchKeys($rawState, $validatedFormData)); + } + } + } else { + $rawState = []; + + foreach (array_keys($state) as $key) { + $rawState[$key] = data_get($livewire, $key); + } + + $state = $this->pruneStateToMatchKeys($rawState, $state); + } + if ($shouldCallHooksBefore) { $this->callBeforeStateDehydrated($state); diff --git a/tests/src/Forms/StateTest.php b/tests/src/Forms/StateTest.php index 20df70ffc4..8acdb6dc41 100644 --- a/tests/src/Forms/StateTest.php +++ b/tests/src/Forms/StateTest.php @@ -859,6 +859,43 @@ test('dehydrated state can be mutated when parent schema is hidden and dehydrate ]); }); +test('`mutateStateForValidationUsing()` does not affect the dehydrated state', function (): void { + $schema = Schema::make(Livewire::make()) + ->statePath('data') + ->components([ + Field::make($statePath = Str::random()) + ->default($state = Str::random()) + ->mutateStateForValidationUsing(fn ($state) => $state . '_MUTATED_FOR_VALIDATION'), + ]) + ->fill(); + + invade($schema->getLivewire())->cacheSchema('form', $schema); + + expect($schema->getState()) + ->toBe([ + $statePath => $state, + ]); +}); + +test('`mutateStateForValidationUsing()` does not affect the dehydrated state when using `dehydrateStateUsing()`', function (): void { + $schema = Schema::make(Livewire::make()) + ->statePath('data') + ->components([ + Field::make($statePath = Str::random()) + ->default($state = Str::random()) + ->mutateStateForValidationUsing(fn ($state) => $state . '_MUTATED_FOR_VALIDATION') + ->dehydrateStateUsing(fn ($state) => $state . '_DEHYDRATED'), + ]) + ->fill(); + + invade($schema->getLivewire())->cacheSchema('form', $schema); + + expect($schema->getState()) + ->toBe([ + $statePath => $state . '_DEHYDRATED', + ]); +}); + test('sibling state can be retrieved relatively from another component', function (): void { Schema::make(Livewire::make()) ->statePath('data')