phpcastinglaravel-livewirelaravel-9

Laravel Livewire accessing json attribute fails with type error


I have a model with some typical columns and one json column. The Json is casted as array:

Model:

protected $casts = [
    'client' => 'array'
];

In my Livewire component, I created the following validation rule

Livewire component:

protected $rules = [
  'editing.name' => 'required',

  ...

  'editing.client' => 'present|array',
  'editing.client.*.tenant' => 'required',
];

I call the 'editModal' method where I type-hint the model and set a public property with it's attributes. Already filtered to the specific item.

Livewire component:

public function editModal(TokenCacheProvider $provider)
{
    $this->editing = $provider;
    $this->dispatchBrowserEvent('open-modal', ['modal' => 'edit']);
}

My blade is a simple boiler blade component:

Blade:

<div>
    <x-input.group inline borderless for="name" label="Name" :error="$errors->first('editing.name')"/>
    <x-input.text name="name" class="w-full" wire:model="editing.name" />
</div>

<div>
    <x-input.group inline borderless for="name" label="Tenant" :error="$errors->first('editing.client.tenant')"/>
    <x-input.text name="tenant" class="w-full" wire:model="editing.client.tenant" />
</div>

Once I load the page I get the following type exception

foreach() argument must be of type array|object, string given

This is because the client attribute is still a string as in the database. It should be an array as I casted it:

public $editing

So, I don't understand why the client attribute is still a string and not an array as casted.

Thank you


Solution

  • Well it's more a work-around than a solution but Daantje found an Livewire issue on Github which might explain this behavior.

    I've changed the architecture from one to two public properties. One for the actual model and a second for the json column.

    Livewire component (truncated)

    public MyModel $editing; // the model
    public array $client; // for the json attribute
    
    protected $rules = [
    'editing.name' => 'required',
     ...
    
    'client.foo' => 'required',
    'client.bar' => 'required',
    'client.baz' => 'required',
     ...
    ];
    
    
    public function editModal(MyModel $model)
    {
        $this->editing = $model;
        $this->client = json_decode($model->client,true);
        $this->dispatchBrowserEvent('open-modal', ['modal' => 'edit']);
    }
    
    
    public function save()
    {
        $this->validate();
    
        $this->editing->client = json_encode($this->client);
    
        $this->editing->save();
    
        $this->dispatchBrowserEvent('close-modal', ['modal' => 'edit']);
    
        $this->event('Saved', 'success');
    }
    

    Two blade input field examples:

    <!-- ORM field(s) -->
    <div>
        <x-input.group inline borderless for="name" label="Name" :error="$errors->first('editing.name')"/>
        <x-input.text name="name" wire:model="editing.name" />
    </div>
    
    
    <!-- Json field(s) -->
    <div>
        <x-input.group inline borderless for="foo" label="Foo" :error="$errors->first('client.foo')"/>
        <x-input.text name="foo" wire:model="client.foo" />
    </div>
    

    Well, this works but as mentioned it's more a workaround