-
Notifications
You must be signed in to change notification settings - Fork 434
Feat: Generalize attribute data field state handling in Filament #2105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 1.x
Are you sure you want to change the base?
Feat: Generalize attribute data field state handling in Filament #2105
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
@repl6669 Is this still being worked on? |
Hello @alecritson, I will give it some more time this weekend. I just need to find out how to properly test it. |
de8f71e
to
739d28c
Compare
WalkthroughThe PR updates AttributeData state handling/coercion, revises StorefrontSession facade PHPDoc annotations, adds test stubs (Repeater/Builder field types and synthesizer), augments product edit feature tests to cover attribute fields (including builder/repeater), and adds /.phpactor.json to .gitignore. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (13)
.gitignore (1)
6-6
: Root-only ignore for Phpactor config — confirm scope; consider YAML variantUsing a leading slash limits ignoring
.phpactor.json
to the repo root. If that’s intended, this is fine. If you want to ignore it anywhere in the tree (e.g., in nested packages/fixtures), drop the leading slash. Also, consider whether.phpactor.yml
is used in your workflows and should be ignored as well for consistency.Optional change if you want non-root matching:
-/.phpactor.json +.phpactor.jsonpackages/core/src/Facades/StorefrontSession.php (2)
19-24
: Add @mixin annotation to StorefrontSession facadeAll
init*
,forget
andgetSessionKey
methods have been confirmed onStorefrontSessionManager
. Adding a@mixin
will help IDEs infer the full API without needing to enumerate every method.• File:
packages/core/src/Facades/StorefrontSession.php
– In the class-level docblock, add:/** + * @mixin \Lunar\Managers\StorefrontSessionManager * @method static \Lunar\Models\Contracts\Channel getChannel() * @method static \Lunar\Managers\StorefrontSessionManager setChannel(\Lunar\Models\Contracts\Channel $channel) * @method static \Illuminate\Support\Collection getCustomerGroups() * … * @method static void initCustomer() * @method static void forget() * @method static string getSessionKey()
11-14
: Document concrete Collection value-type for customer groupsWe’ve verified that StorefrontSessionManager::__construct() and resetCustomerGroups() always initialize $this->customerGroups to a new Collection, so getCustomerGroups() never returns null. It’s safe to tighten the facade PHPDoc with generics for better IDE/static-analysis support:
File: packages/core/src/Facades/StorefrontSession.php
- * @method static \Illuminate\Support\Collection getCustomerGroups() + * @method static \Illuminate\Support\Collection<int, \Lunar\Models\Contracts\CustomerGroup> getCustomerGroups() - * @method static \Lunar\Managers\StorefrontSessionManager setCustomerGroups(\Illuminate\Support\Collection $customerGroups) + * @method static \Lunar\Managers\StorefrontSessionManager setCustomerGroups(\Illuminate\Support\Collection<int, \Lunar\Models\Contracts\CustomerGroup> $customerGroups)packages/admin/src/Support/Forms/AttributeData.php (2)
66-68
: Use the imported contract consistentlyMinor consistency: use the FieldTypeContract alias here as well.
- if ($state instanceof \Lunar\Base\FieldType) { + if ($state instanceof FieldTypeContract) { return $state->getValue(); }
16-16
: Nit: fix Dropdown alias typo for clarityThe alias is spelled DrodownFieldType which might be confusing when scanning the file.
-use Lunar\FieldTypes\Dropdown as DrodownFieldType; +use Lunar\FieldTypes\Dropdown as DropdownFieldType; @@ - DrodownFieldType::class => Dropdown::class, + DropdownFieldType::class => Dropdown::class,Also applies to: 31-31
tests/admin/Stubs/FieldTypes/RepeaterField.php (1)
18-35
: Optional: be lenient with null/blank inputs in setValueFor test ergonomics, consider treating null/blank as [] instead of throwing, aligning with some core field types’ behavior.
- public function setValue($value) + public function setValue($value) { - if (! is_array($value)) { + if ($value === null) { + $value = []; + } + if (! is_array($value)) { throw new FieldTypeException(self::class.' value must be an array.'); } $this->value = json_encode($value); }tests/admin/Stubs/Support/FieldTypes/BuilderField.php (1)
24-73
: LGTM: UI-layer normalization complements dehydrationThe format/dehydrate callbacks mirror the domain stub’s normalize logic, keeping the component state predictable. With the proposed change in AttributeData, we avoid double-filtering of scalar arrays while still reindexing builder items.
If you adopt the AttributeData normalization tweak, please rerun the product edit tests that exercise builder/repeater to confirm no regressions. I can help expand coverage to include File multiple and ListField cases.
tests/admin/Stubs/Support/Synthesizers/BuilderSynth.php (2)
26-38
: Avoid double-normalization; let FieldType::setValue() own normalizationYou're pre-normalizing in the nested set() path, but the CoreBuilderField::setValue() already normalizes comprehensively (including stripping empty/null data, enforcing shape). Relying on the FieldType normalization avoids duplicate work and keeps all rules in one place.
Suggested change:
public function set(&$target, $key, $value) { if ($key === '' || $key === null) { - $target->setValue(is_array($value) ? $value : []); + // Let the FieldType perform full normalization. + $target->setValue(is_array($value) ? $value : []); return; } $fieldValue = (array) $target->getValue(); Arr::set($fieldValue, $key, $value); - $target->setValue($this->normalize($fieldValue)); + // Delegate normalization to the FieldType implementation. + $target->setValue($fieldValue); }
40-47
: Mirror set() simplification in unset()Same rationale as set(): pre-normalizing here is redundant given CoreBuilderField::setValue() normalizes fully, including re-indexing.
Suggested change:
public function unset(&$target, $index) { $fieldValue = (array) $target->getValue(); Arr::forget($fieldValue, $index); - $target->setValue($this->normalize($fieldValue)); + $target->setValue($fieldValue); }tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php (4)
78-206
: Pre-synthesize here too for consistency and to reduce flakinessThis test registers custom field types (Repeater/Builder). While it only asserts rendering, pre-synthesizing keeps behavior consistent across tests and avoids subtle issues with nested state resolution.
Suggested insertion:
\Lunar\Admin\Support\Facades\AttributeData::registerFieldType( \Lunar\Tests\Admin\Stubs\FieldTypes\RepeaterField::class, \Lunar\Tests\Admin\Stubs\Support\FieldTypes\RepeaterField::class, ); + // Preload property synthesizers for dynamic field types + \Lunar\Admin\Support\Facades\AttributeData::synthesizeLivewireProperties();Additionally, since this page renders variant pricing contexts, you already added a default Currency in this test — good. Consider applying the same to the other new tests for consistency (see suggestions below).
264-368
: Add a default Currency to mirror the other test’s environmentThe EditProduct page often expects a default Currency for variant-related UI. You created it in the “render all fields” test but not here. Add it to avoid environment-dependent failures.
Suggested insertion after the TaxClass factory:
\Lunar\Models\Language::factory()->create(['default' => true]); \Lunar\Models\TaxClass::factory()->create(['default' => true]); + \Lunar\Models\Currency::factory()->create(['default' => true]);
Optional: there’s a fair bit of setup duplication across tests (language/tax/currency, groups, attributables). If this grows, consider extracting test helpers/factories to keep tests compact.
369-486
: Pre-synthesize and add default Currency for parity
- Registering the Repeater field type is correct, but, like above, pre-synthesizing reduces surprises in nested array handling.
- Add a default Currency to keep the environment aligned with your other test and the page’s expectations.
Suggested changes:
\Lunar\Admin\Support\Facades\AttributeData::registerFieldType( \Lunar\Tests\Admin\Stubs\FieldTypes\RepeaterField::class, \Lunar\Tests\Admin\Stubs\Support\FieldTypes\RepeaterField::class, ); + \Lunar\Admin\Support\Facades\AttributeData::synthesizeLivewireProperties(); \Lunar\Models\Language::factory()->create([ 'default' => true, ]); \Lunar\Models\TaxClass::factory()->create([ 'default' => true, ]); + \Lunar\Models\Currency::factory()->create([ + 'default' => true, + ]);Note: Great end-to-end assertion of saved state for both product and variant repeaters.
1-486
: Add a regression test for custom File field type (covers linked issue #2091)Given the PR’s objective to let custom file attribute types save correctly (removing strict class checks), please add a test that exercises a custom File subtype end-to-end. This will prevent regressions and explicitly validates the fix for #2091.
I can draft it if helpful. Sketch:
- Define a stub CustomFileField extends \Lunar\FieldTypes\File (no changes).
- Register it with AttributeData to the same Filament file component used by core File.
- Create a product + attribute of type CustomFileField.
- Pre-synthesize, mount EditProduct, fill with new CustomFileField('foo/bar.jpg'), save.
- Assert $product->attr('custom_file') === 'foo/bar.jpg'.
Would you like me to open a follow-up PR with the test and stubs?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
.gitignore
(1 hunks)packages/admin/src/Support/Forms/AttributeData.php
(3 hunks)packages/core/src/Facades/StorefrontSession.php
(1 hunks)tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php
(3 hunks)tests/admin/Stubs/FieldTypes/BuilderField.php
(1 hunks)tests/admin/Stubs/FieldTypes/RepeaterField.php
(1 hunks)tests/admin/Stubs/Support/FieldTypes/BuilderField.php
(1 hunks)tests/admin/Stubs/Support/FieldTypes/RepeaterField.php
(1 hunks)tests/admin/Stubs/Support/Synthesizers/BuilderSynth.php
(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
tests/admin/Stubs/FieldTypes/RepeaterField.php (2)
packages/core/src/Exceptions/FieldTypeException.php (1)
FieldTypeException
(5-8)tests/admin/Stubs/Support/FieldTypes/RepeaterField.php (1)
RepeaterField
(10-22)
tests/admin/Stubs/Support/FieldTypes/RepeaterField.php (2)
tests/admin/Stubs/FieldTypes/RepeaterField.php (1)
RepeaterField
(9-43)packages/core/src/Models/Attribute.php (1)
Attribute
(35-94)
tests/admin/Stubs/Support/Synthesizers/BuilderSynth.php (3)
packages/admin/src/Support/Synthesizers/AbstractFieldSynth.php (1)
AbstractFieldSynth
(8-41)tests/admin/Stubs/FieldTypes/BuilderField.php (4)
BuilderField
(9-69)getValue
(23-26)normalize
(37-61)setValue
(28-35)tests/admin/Stubs/Support/FieldTypes/BuilderField.php (1)
BuilderField
(10-75)
tests/admin/Stubs/Support/FieldTypes/BuilderField.php (2)
tests/admin/Stubs/FieldTypes/BuilderField.php (1)
BuilderField
(9-69)tests/admin/Stubs/Support/Synthesizers/BuilderSynth.php (1)
BuilderSynth
(9-55)
tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php (5)
packages/admin/src/Support/Forms/AttributeData.php (3)
AttributeData
(28-117)synthesizeLivewireProperties
(111-116)registerFieldType
(92-97)tests/admin/Stubs/FieldTypes/RepeaterField.php (1)
RepeaterField
(9-43)packages/core/src/Models/AttributeGroup.php (1)
AttributeGroup
(23-59)packages/core/src/Models/Attribute.php (1)
Attribute
(35-94)tests/admin/TestCase.php (1)
asStaff
(88-91)
tests/admin/Stubs/FieldTypes/BuilderField.php (3)
packages/core/src/Exceptions/FieldTypeException.php (1)
FieldTypeException
(5-8)tests/admin/Stubs/Support/FieldTypes/BuilderField.php (1)
BuilderField
(10-75)tests/admin/Stubs/Support/Synthesizers/BuilderSynth.php (1)
normalize
(49-54)
packages/admin/src/Support/Forms/AttributeData.php (7)
packages/admin/src/Support/FieldTypes/TextField.php (1)
TextField
(11-38)packages/admin/src/Support/FieldTypes/File.php (1)
getFilamentComponent
(14-43)packages/admin/src/Support/FieldTypes/Vimeo.php (1)
getFilamentComponent
(14-23)packages/core/src/Base/Traits/HasTranslations.php (1)
translate
(17-40)packages/core/src/FieldTypes/Toggle.php (2)
getValue
(51-54)setValue
(61-68)packages/core/src/FieldTypes/File.php (2)
getValue
(59-62)setValue
(69-76)packages/core/src/FieldTypes/Vimeo.php (2)
getValue
(51-54)setValue
(61-68)
🔇 Additional comments (9)
packages/core/src/Facades/StorefrontSession.php (2)
9-10
: Facade PHPDoc Signature Confirmed
BothStorefrontSessionInterface::setChannel()
andStorefrontSessionManager::setChannel()
accept only aChannelContract
instance (no string overload), so the facade’s PHPDoc is already correct. No|string
union is needed.
15-18
: Accessors signatures align with docsAll currency/customer methods match the documented return types and support fluent chaining:
Interface (
StorefrontSessionInterface
):
• getCurrency(): Currency
• getCustomer(): ?Customer
• setCurrency(Currency): static
• setCustomer(Customer): staticImplementation (
StorefrontSessionManager
):
• getCurrency(): CurrencyContract – always initialized in the constructor via initCurrency(), so never null at runtime.
• getCustomer(): ?CustomerContract – initialized in initCustomer(), returns null only when no session value is set.
• setCurrency()/setCustomer() both return static, enabling fluent chaining.Facade (
StorefrontSession
): docblock annotations reflect the above exactly.No changes required.
packages/admin/src/Support/Forms/AttributeData.php (1)
44-51
: Simplified component resolution and label application looks goodThe one-liner panel field-type resolution and chaining label() read clean and align with Filament patterns.
tests/admin/Stubs/Support/FieldTypes/RepeaterField.php (1)
14-21
: LGTM: focused Filament stub for RepeaterClear schema, sensible defaults, and collapsed by default. This pairs well with the generalized state handling.
tests/admin/Stubs/FieldTypes/BuilderField.php (1)
28-35
: LGTM: solid normalization for builder payloadsThe normalization enforces the expected shape and strips null/empty values. Matches the UI stub behavior and should make assertions deterministic.
Also applies to: 37-61
tests/admin/Stubs/Support/Synthesizers/BuilderSynth.php (3)
9-14
: Solid Synthesizer target/key setup
$key
and$targetClass
are correctly specialized for the builder stub. This ensures Livewire routes property ops to this synth only for your CoreBuilderField.
15-24
: Balanced get() behavior with root/nested supportReturning a normalized full array at root and nested values via Arr::get() is appropriate for a Repeater-like structure. This aligns with Livewire's expected get semantics.
49-54
: normalize() scope is intentionally minimal — OKA shallow filter + reindex is fine if you keep normalization logic centralized in the FieldType. After applying the above refactors, this method will effectively be used only by get() to present stable state, which is reasonable.
tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php (1)
54-56
: Good call: synthesize Livewire properties prior to mountingPre-loading synthesizers reduces flakiness when interacting with FieldType instances in form state.
if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) { | ||
$state = new $attribute->type; | ||
} | ||
|
||
return $state; | ||
$value = $state->getValue(); | ||
|
||
// Ensure arrays are normalized for components expecting array items only | ||
if (is_array($value)) { | ||
$value = array_values(array_filter($value, 'is_array')); | ||
} | ||
|
||
return $value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Fix: formatStateUsing drops default/persisted scalar state and over-filters arrays
Two issues here:
- New instance is created but the existing/default raw state is not set on it, so defaults/persisted scalars (e.g., text, file path) are lost.
- The array normalization removes scalar list items (e.g., File multiple or ListField which are arrays of strings), collapsing valid state to an empty array.
Update to preserve the raw state and only normalize when the value is an array of arrays (repeater/builder-like):
- if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) {
- $state = new $attribute->type;
- }
-
- $value = $state->getValue();
-
- // Ensure arrays are normalized for components expecting array items only
- if (is_array($value)) {
- $value = array_values(array_filter($value, 'is_array'));
- }
-
- return $value;
+ $initial = $state;
+ if (! $state instanceof FieldTypeContract || ! ($state instanceof $attribute->type)) {
+ $state = new $attribute->type;
+ // Preserve existing/default state for display
+ $state->setValue($initial);
+ }
+
+ $value = $state->getValue();
+
+ // Only normalize if the value is an array of arrays (e.g. repeater/builder).
+ if (is_array($value) && $value !== [] && is_array(reset($value))) {
+ $value = array_values(array_filter($value, 'is_array'));
+ }
+
+ return $value;
To ensure no regressions:
- Verify that File (multiple) and ListField values (arrays of strings) render unchanged.
- Verify that Repeater/Builder values (arrays of arrays) are reindexed and preserved.
Would you like me to add/adjust tests for File multiple/ListField rendering and saving?
#!/bin/bash
set -e
Locate and display the beginning of AttributeData.php to inspect $state initialization
file=$(fd AttributeData.php packages/admin/src/Support/Forms)
echo "Found file at: $file"
echo "Lines 1–200 of $file:"
sed -n '1,200p' "$file"
🏁 Script executed:
#!/bin/bash
set -e
# Locate AttributeData.php
file=$(fd --type f AttributeData.php packages/admin/src/Support/Forms)
echo "AttributeData.php path: $file"
# Display the first 200 lines for context
echo "=== AttributeData.php (lines 1–200) ==="
sed -n '1,200p' "$file"
# Show the FieldTypeContract interface to check available methods
echo "=== FieldTypeContract definition ==="
rg -n "interface FieldTypeContract" -C3 packages
# Search for setValue method definitions
echo "=== setValue definitions ==="
rg -n "function setValue" -C3 packages
# Search for getValue method definitions
echo "=== getValue definitions ==="
rg -n "function getValue" -C3 packages
Length of output: 4685
🏁 Script executed:
#!/usr/bin/env bash
# Avoid abort on errors
# Locate AttributeData.php
file=$(fd --type f AttributeData.php packages/admin/src/Support/Forms)
echo "=== AttributeData.php (${file}) ==="
# Show the first 200 lines for context
sed -n '1,200p' "$file"
# Show the formatStateUsing closure
echo "=== formatStateUsing closure ==="
rg -n -C2 "formatStateUsing" "$file"
# Show the mutateDehydratedStateUsing closure
echo "=== mutateDehydratedStateUsing closure ==="
rg -n -C2 "mutateDehydratedStateUsing" "$file"
# List all FieldType.php files under packages
echo "=== Listing FieldType.php in packages ==="
fd --type f FieldType.php packages || true
# Inspect each FieldType.php for getValue/setValue methods
for f in $(fd --type f FieldType.php packages || true); do
echo "=== Inspecting $f ==="
sed -n '1,50p' "$f"
echo "--- getValue ---"
rg -n "function getValue" "$f" || echo "no getValue"
echo "--- setValue ---"
rg -n "function setValue" "$f" || echo "no setValue"
done
Length of output: 7725
Fix formatStateUsing
to preserve scalar state and only normalize nested arrays
The current closure in AttributeData::getFilamentComponent
(packages/admin/src/Support/Forms/AttributeData.php lines 52–63) drops any scalar default or persisted value when instantiating a new field type, and blindly filters an array to only sub-arrays—removing valid scalar items (e.g., string IDs or file paths).
Please update as follows:
• Capture the incoming state before replacing the instance, then reapply it via setValue
.
• Only run array_values(array_filter(..., 'is_array'))
when the value is a non-empty array of arrays (e.g., repeaters/builders), leaving flat lists of strings untouched.
Proposed diff:
--- a/packages/admin/src/Support/Forms/AttributeData.php
+++ b/packages/admin/src/Support/Forms/AttributeData.php
@@ ->formatStateUsing(function ($state) use ($attribute) {
- if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) {
- $state = new $attribute->type;
- }
-
- $value = $state->getValue();
-
- // Ensure arrays are normalized for components expecting array items only
- if (is_array($value)) {
- $value = array_values(array_filter($value, 'is_array'));
- }
-
- return $value;
+ // Preserve the raw incoming state when creating a fresh instance
+ $initial = $state;
+ if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) {
+ $state = new $attribute->type;
+ $state->setValue($initial);
+ }
+
+ $value = $state->getValue();
+
+ // Only strip non-array items if this is a nested-array field (e.g. repeater/builder)
+ if (
+ is_array($value)
+ && $value !== []
+ && is_array(reset($value))
+ ) {
+ $value = array_values(array_filter($value, 'is_array'));
+ }
+
+ return $value;
• Verify that “File multiple” and “ListField” (arrays of strings) still render unchanged.
• Verify that repeater/builder data (arrays of arrays) are reindexed and preserved.
• Add/update tests covering both scenarios.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) { | |
$state = new $attribute->type; | |
} | |
return $state; | |
$value = $state->getValue(); | |
// Ensure arrays are normalized for components expecting array items only | |
if (is_array($value)) { | |
$value = array_values(array_filter($value, 'is_array')); | |
} | |
return $value; | |
->formatStateUsing(function ($state) use ($attribute) { | |
// Preserve the raw incoming state when creating a fresh instance | |
$initial = $state; | |
if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) { | |
$state = new $attribute->type; | |
$state->setValue($initial); | |
} | |
$value = $state->getValue(); | |
// Only strip non-array items if this is a nested-array field (e.g. repeater/builder) | |
if ( | |
is_array($value) | |
&& $value !== [] | |
&& is_array(reset($value)) | |
) { | |
$value = array_values(array_filter($value, 'is_array')); | |
} | |
return $value; | |
}) |
// Normalize arrays before wrapping into FieldType instance | ||
if (is_array($state)) { | ||
$state = array_values(array_filter($state, 'is_array')); | ||
} | ||
|
||
if ( | ||
! $state || | ||
(get_class($state) != $attribute->type) | ||
) { | ||
return new $attribute->type; | ||
if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) { | ||
$field = (new $attribute->type); | ||
$field->setValue($state); | ||
|
||
return $field; | ||
} | ||
|
||
return $state; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Dehydration: avoid dropping scalar list items and support subclass checks
- The array normalization currently removes scalar entries, breaking File (multiple) and ListField values (arrays of strings).
- Prefer instanceof against $attribute->type over get_class(...) to support subclassing/proxies.
- // Normalize arrays before wrapping into FieldType instance
- if (is_array($state)) {
- $state = array_values(array_filter($state, 'is_array'));
- }
-
- if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) {
- $field = (new $attribute->type);
- $field->setValue($state);
-
- return $field;
- }
+ // Only normalize if the state is an array of arrays (e.g. repeater/builder)
+ if (is_array($state) && $state !== [] && is_array(reset($state))) {
+ $state = array_values(array_filter($state, 'is_array'));
+ }
+
+ if (! $state instanceof FieldTypeContract || ! ($state instanceof $attribute->type)) {
+ $field = new $attribute->type;
+ $field->setValue($state);
+
+ return $field;
+ }
Please verify saving for:
- File (multiple) attributes: dehydrated state should remain an array of strings.
- ListField attributes: dehydrated state should remain an array of strings.
- Repeater/Builder: dehydrated state reindexes and strips non-array items only.
I can extend the product edit feature tests to cover these cases.
🏁 Script executed:
#!/bin/bash
set -e
# Locate the AttributeData file
file=$(fd packages/admin/src/Support/Forms/AttributeData.php)
echo "Found file: $file"
# Display lines 1-120 to include context around the snippet
echo "---- File content (lines 1-120) ----"
sed -n '1,120p' "$file"
Length of output: 254
🏁 Script executed:
#!/bin/bash
set -e
echo "Searching for AttributeData.php:"
fd -I AttributeData.php
echo "Searching for class AttributeData in PHP files:"
rg -nP --type=php 'class\s+AttributeData' || true
# If found, show snippet around lines 1-120
file=$(fd AttributeData.php | head -n1)
if [ -n "$file" ]; then
echo "Found file: $file"
echo "---- File content (1-120) ----"
sed -n '1,120p' "$file"
else
echo "AttributeData.php not found."
fi
Length of output: 1819
🏁 Script executed:
#!/bin/bash
set -e
# Find the AttributeData in the Forms directory
file=$(fd -I --search-path packages/admin/src/Support/Forms -g 'AttributeData.php' | head -n1)
if [ -z "$file" ]; then
echo "Error: AttributeData.php not found in Forms."
exit 1
fi
echo "Found file: $file"
echo "---- File content (1-120) ----"
sed -n '1,120p' "$file"
Length of output: 4596
Refactor array normalization and subclass checks in mutateDehydratedStateUsing
The current implementation:
- Strips out all non-array items from any array state, which breaks “multiple File” and
ListField
values (arrays of strings). - Uses
get_class($state) !== $attribute->type
, preventing subclass or proxy instances from being recognized.
Please update this block in packages/admin/src/Support/Forms/AttributeData.php around line 74:
// Normalize arrays before wrapping into FieldType instance
- if (is_array($state)) {
- $state = array_values(array_filter($state, 'is_array'));
- }
+ // Only normalize nested-array states (e.g. repeater/builder), preserve scalar lists
+ if (
+ is_array($state)
+ && $state !== []
+ && is_array(reset($state))
+ ) {
+ $state = array_values(array_filter($state, 'is_array'));
+ }
- if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) {
+ // Use instanceof against the attribute’s type to support subclasses/proxies
+ if (
+ ! $state instanceof FieldTypeContract
+ || ! ($state instanceof $attribute->type)
+ ) {
$field = new $attribute->type;
$field->setValue($state);
return $field;
}
Please verify that:
- File (multiple) attributes still hydrate/dehydrate as arrays of strings.
- ListField attributes still hydrate/dehydrate as arrays of strings.
- Repeater/Builder states continue to reindex and strip only non-array items.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Normalize arrays before wrapping into FieldType instance | |
if (is_array($state)) { | |
$state = array_values(array_filter($state, 'is_array')); | |
} | |
if ( | |
! $state || | |
(get_class($state) != $attribute->type) | |
) { | |
return new $attribute->type; | |
if (! $state instanceof FieldTypeContract || (get_class($state) !== $attribute->type)) { | |
$field = (new $attribute->type); | |
$field->setValue($state); | |
return $field; | |
} | |
return $state; | |
// Normalize arrays before wrapping into FieldType instance | |
// Only normalize nested-array states (e.g. repeater/builder), preserve scalar lists | |
if ( | |
is_array($state) | |
&& $state !== [] | |
&& is_array(reset($state)) | |
) { | |
$state = array_values(array_filter($state, 'is_array')); | |
} | |
// Use instanceof against the attribute’s type to support subclasses/proxies | |
if ( | |
! $state instanceof FieldTypeContract | |
|| ! ($state instanceof $attribute->type) | |
) { | |
$field = new $attribute->type; | |
$field->setValue($state); | |
return $field; | |
} | |
return $state; |
🤖 Prompt for AI Agents
packages/admin/src/Support/Forms/AttributeData.php around lines 74-86: the array
normalization currently strips every non-array element and the class check uses
get_class equality; change it so arrays are only reindexed/filtered when they
contain arrays (i.e., only for repeater/builder-like states) — detect this by
checking if the array contains at least one array and then apply
array_values(array_filter($state, 'is_array')); otherwise leave arrays of
strings intact; and replace the strict get_class($state) !== $attribute->type
check with a type-safe check that accepts subclasses and proxies (use instanceof
or is_subclass_of to determine if $state is already an instance of the attribute
type) so subclass/proxy instances are treated as valid and only non-matching
values are wrapped with new $attribute->type and setValue.
Description
This PR aims to generalize how attribute data fields are handled in Filament and fix #2091 and #2071.
->getValue()
method.Todo
ProductEditPage
RepeaterField
BuilderField
Summary by CodeRabbit
Bug Fixes
Documentation
Tests
Chores