Skip to content

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

Open
wants to merge 13 commits into
base: 1.x
Choose a base branch
from

Conversation

repl6669
Copy link
Contributor

@repl6669 repl6669 commented Feb 14, 2025

Description

This PR aims to generalize how attribute data fields are handled in Filament and fix #2091 and #2071.

  1. Component state for all fields is set to field value by using the ->getValue() method.
  2. Instance of the field with its value set is returned after dehydration

Todo

  • Test that fields are rendered correctly on ProductEditPage
  • Test that the form is saved correctly for all Lunar fields as well as custom, user defined fields
    • Test custom RepeaterField
    • Test custom BuilderField
  • Test IRL scenarios

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of attribute form fields (including repeaters/builders) with better state normalization and saving behavior.
  • Documentation

    • Updated public API documentation for storefront session management to clarify available methods and types.
  • Tests

    • Added comprehensive tests ensuring all attribute fields render and persist correctly in product editing.
    • Introduced test stubs and synthesizers to validate builder and repeater field behavior.
  • Chores

    • Updated ignore rules to exclude a local IDE configuration file from version control.

Copy link

vercel bot commented Feb 14, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
lunar-docs Ready Ready Preview Comment Aug 18, 2025 9:30pm

@alecritson
Copy link
Collaborator

@repl6669 Is this still being worked on?

@repl6669
Copy link
Contributor Author

repl6669 commented May 2, 2025

Hello @alecritson, I will give it some more time this weekend. I just need to find out how to properly test it.

@repl6669 repl6669 force-pushed the feat/generalize-attribute-data-handling-in-filament branch from de8f71e to 739d28c Compare May 13, 2025 21:07
@repl6669 repl6669 reopened this Aug 18, 2025
Copy link
Contributor

coderabbitai bot commented Aug 18, 2025

Walkthrough

The 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

Cohort / File(s) Summary
Attribute state handling
packages/admin/src/Support/Forms/AttributeData.php
Normalizes component state: unwraps FieldType values via getValue(), reindexes/filter arrays, and generalizes dehydration by coercing to expected FieldType without special-casing FileFieldType.
Storefront session PHPDoc
packages/core/src/Facades/StorefrontSession.php
Overhauls facade annotations: explicit typed accessors for Channel/Currency/Customer/Groups, adds init/reset methods and utility methods; no runtime logic changes.
Feature tests
tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php
Pre-mount property synthesis; adds tests rendering/saving builder and repeater attribute fields and coverage for multiple attribute types.
Test stubs: FieldTypes
tests/admin/Stubs/FieldTypes/BuilderField.php, tests/admin/Stubs/FieldTypes/RepeaterField.php
Introduces JSON-backed FieldType stubs with array validation/normalization for builder and repeater.
Test stubs: Filament FieldTypes + Synth
tests/admin/Stubs/Support/FieldTypes/BuilderField.php, tests/admin/Stubs/Support/FieldTypes/RepeaterField.php, tests/admin/Stubs/Support/Synthesizers/BuilderSynth.php
Adds Filament components for builder/repeater with state formatting/dehydration; adds synthesizer for builder value normalization.
Repo config
.gitignore
Ignores /.phpactor.json.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Handle custom attribute file field type saving by not relying on strict FileFieldType comparison in AttributeData (#2091)

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@repl6669 repl6669 marked this pull request as ready for review August 18, 2025 22:25
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 variant

Using 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.json
packages/core/src/Facades/StorefrontSession.php (2)

19-24: Add @mixin annotation to StorefrontSession facade

All init*, forget and getSessionKey methods have been confirmed on StorefrontSessionManager. 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 groups

We’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 consistently

Minor 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 clarity

The 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 setValue

For 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 dehydration

The 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 normalization

You'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 flakiness

This 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 environment

The 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.

📥 Commits

Reviewing files that changed from the base of the PR and between ecb4b46 and 5bba165.

📒 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
Both StorefrontSessionInterface::setChannel() and StorefrontSessionManager::setChannel() accept only a ChannelContract instance (no string overload), so the facade’s PHPDoc is already correct. No |string union is needed.


15-18: Accessors signatures align with docs

All currency/customer methods match the documented return types and support fluent chaining:

  • Interface (StorefrontSessionInterface):
    • getCurrency(): Currency
    • getCustomer(): ?Customer
    • setCurrency(Currency): static
    • setCustomer(Customer): static

  • Implementation (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 good

The 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 Repeater

Clear 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 payloads

The 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 support

Returning 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 — OK

A 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 mounting

Pre-loading synthesizers reduces flakiness when interacting with FieldType instances in form state.

Comment on lines +52 to +63
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;
Copy link
Contributor

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.

Suggested change
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;
})

Comment on lines +74 to 86
// 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;
Copy link
Contributor

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.

Suggested change
// 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Custom Attribute File Field Type Fails to Save Due to Strict Type Comparison in AttributeData
4 participants