-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Description
Description
FileUpload components inside a Repeater that uses a JSON column (not a relationship) throw an error when the page loads with existing data:
foreach() argument must be of type array|object, string given
The error occurs in BaseFileUpload::getUploadedFiles() at line ~730.
Steps to Reproduce
- Create a model with a JSON column (e.g.,
press_release) - Add a Repeater to a form that stores data in this JSON column
- Add a FileUpload inside the Repeater:
Forms\Components\Repeater::make('press_release')
->schema([
Forms\Components\FileUpload::make('image')
->image()
->directory('uploads'),
Forms\Components\TextInput::make('title'),
])- Save some data with uploaded images
- Edit the record - the page crashes with the foreach error
Root Cause Analysis
The bug is caused by a timing mismatch between StateCast application and Repeater's state restructuring.
The Flow
- Initial hydration:
hydrateState()runs on all components - Child schemas created with numeric keys (0, 1, 2)
- FileUpload's StateCast runs: normalizes
"path.jpg"→{uuid: "path.jpg"}✓ - Repeater's
afterStateHydratedruns (after children are hydrated):- Restructures
[0 => data, 1 => data]to[uuid1 => data, uuid2 => data] - Calls
rawState($items)
- Restructures
rawState()triggersclearCachedDefaultChildSchemas()(line 560 in HasState.php)- The already-hydrated child schemas are discarded
Later (when bug occurs)
getItems()is called (e.g., during Livewire render)- NEW child schemas created via
getClone()with UUID keys - These schemas are never hydrated - they're just clones
- FileUpload's
getRawState()returns the raw string"path.jpg" getUploadedFiles()doesforeach ($this->getRawState() ?? [])on a string → ERROR
Key Insight
The comment in HasState.php (lines 557-559) explains the design intent:
// For components such as repeaters and builders, the default child schemas depend on the state of the component.
// When loading state into these fields after the state is already present, the cached child schemas need to be
// cleared so that they can be re-evaluated based on the new state.This is intentional - but the newly created schemas in getItems() don't have their StateCasts applied.
Proposed Fix
Apply StateCasts to child components after creating them in getItems(). The method castStateAfterLoadingFromRelationships() already exists and does exactly this:
public function getItems(): array
{
$relationship = $this->getRelationship();
$records = $relationship ? $this->getCachedExistingRecords() : null;
$items = [];
foreach ($this->getRawState() ?? [] as $itemKey => $itemData) {
$childSchema = $this
->getChildSchema()
->statePath($itemKey)
->constantState(((! ($relationship && $records->has($itemKey))) && is_array($itemData)) ? $itemData : null)
->model($relationship ? $records[$itemKey] ?? $this->getRelatedModel() : null)
->inlineLabel(false)
->getClone();
// Apply StateCasts to child components for non-relationship repeaters.
// This fixes components like FileUpload whose state isn't normalized
// after Repeater's afterStateHydrated restructures items and clears cached schemas.
if (! $relationship) {
foreach ($childSchema->getComponents(withActions: false, withHidden: true) as $component) {
$component->castStateAfterLoadingFromRelationships();
}
}
$items[$itemKey] = $childSchema;
}
return $items;
}Why this fix works
castStateAfterLoadingFromRelationships()reads from Livewire state viagetRawState()- Applies all registered StateCasts via their
set()method - Writes normalized state back via
rawState() - The method is idempotent: applying StateCast to already-normalized state returns the same result
- Only applied for non-relationship repeaters (relationship-based repeaters already handle this via
loadStateFromRelationships)
Why this affects ALL StateCast components
This isn't just a FileUpload bug. Any component with a StateCast inside a JSON column Repeater would have this issue. The fix in Repeater ensures all StateCast components work correctly.
Environment
- Filament Version: 4.x
- Laravel Version: 11.x
- PHP Version: 8.4
- Database: MySQL/PostgreSQL (any)
Related Issues
- [v4.x] Error on testing a RichEditor component #17472
- RichEditor error in repeater #18017
- Refreshing form with Action on FileUpload throws foreach() argument must be of type array|object, string given #13572
- Wizard form with same relationship in multiple steps doesn't work #13339
- FileUpload field causing ErrorException: foreach() argument must be of type array|object, string given on update page #12076
Metadata
Metadata
Assignees
Labels
Type
Projects
Status