vincent15000's avatar

Livewire 3 : Can't set model as property if it hasn't been persisted yet

Hello,

I'm testing the Laravel 3 features.

I'm trying to set a custom property type.

https://livewire.laravel.com/docs/properties

Here is the model and the controller.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Livewire\Wireable;

class Category extends Model implements Wireable
{
    use HasFactory;

    protected $fillable = [
        'name',
    ];

    protected $name;
 
    public function __construct($name = null)
    {
        $this->name = $name;
    }
 
    public function toLivewire()
    {
        return [
            'name' => $this->name,
        ];
    }
 
    public static function fromLivewire($data)
    {
        $name = $data['name'];
 
        return new static($name);
    }

    public function recipes()
    {
        return $this->hasMany(Recipe::class);
    }
}
<?php

namespace App\Livewire\Pages;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Category;
use App\Traits\TrimStringAndSetNullIfEmptyString;

class Categories extends Component
{
    use WithPagination;
    use AuthorizesRequests;
    use TrimStringAndSetNullIfEmptyString;

    public Category $category;

    public $updateMode = false;
    public $showModal = false;

    public $fieldsToClean = [
        'category' => [
            'name',
        ],
    ];

    public function paginationView()
    {
        return 'utils.pagination-links-view';
    }

    public function mount()
    {
        $this->category = new Category;
    }

    public function initForm()
    {
        $this->updateMode = false;
        $this->category = new Category;
    }

    public function updatedCategory()
    {
        if ($this->updateMode == false) {
            $this->resetPage();
        }
    }

    public function save()
    {
        $this->cleanFields();
        $this->validate();
        $this->showModal = false;

        if ($this->updateMode) {
            $this->authorize('update', $this->category);

            $this->category->update();
        } else {
            $this->authorize('create', Category::class);
            
            $this->category->save();
        }
        $this->initForm();
    }

    public function edit($id)
    {
        $this->updateMode = true;
        $this->showModal = true;

        $this->category = Category::find($id);
    }

    public function cancel()
    {
        $this->showModal = false;

        $this->initForm();
    }

    public function render()
    {
        $categories = Category::
            withCount('recipes')
            ->when($this->category->name, function ($query) {
                $query->where('name', 'like', '%'.$this->category->name.'%');
            })
            ->orderBy('name')
            ->paginate(10);
            
        return view('livewire.pages.categories', compact('categories'));
    }
}

It works fine with Livewire 2, but not with Livewire 3 for which I get this error.

Can't set model as property if it hasn't been persisted yet

What happens ?

Thanks for your help.

V

0 likes
17 replies
vincent15000's avatar

@lucasvscn Ok ... but even without Wireable, it doesn't work.

I need to create an empty model and bind the model properties to some fields in the form.

If it's not possible, does it mean that the only way is to use several variables (one for each property of the model) ?

lucasvscn's avatar

@vincent15000 yeah! It seems you cannot set Model properties if there's no persisted data yet. The reason for this is that Livewire has no means to serialize a not persisted model ( check the code )

Check code example bellow, to an alternative:

<?php

class Categories extends Component
{
    #[Locked]
    public $categoryId;

    /**
     * Stores the attributes of the category creation form.
     * 
     * @var array<string, string>
     */
    public $input = [];

    public $isUpdating = false;

    public function save()
    {
        if ($this->isUpdating) {
            // Creates a new Category.
            $category = new Category($this->input);
        } else {
            // Updates the existing Category.
            $category = Category::find($this->categoryId);
            $category->update($this->input);
        }

        $this->initForm();
    }

    public function edit($id)
    {
        $this->categoryId = $id;
        $this->isUpdating = true;
        $this->input = Category::find($id)->toArray();
    }
}

Your template would be like this:

<form>
<label>Category name</label>
<input wire:model="input.name">

<label>Category description</label>
<input wire:model="input.description">
</form>

This way you can bind the Category model attributes into the input array and once the user hit the submit button, you can persist the data creating a new Category. Otherwise, if user needs to update an existing category, then you should bind the refered object into the input array in order to fill the form fields as well.

2 likes
vincent15000's avatar

@lucasvscn Oh yes it seems interesting to initiate a new model with an empty array. I will try this and I tell you if it works.

1 like
S1d's avatar

I also encountered the same error while trying the livewire 3 updates, the error happens because you haven't given the Eloquent model the correct data yet.

Livewire does not automatically fetch Eloquent models for you in the mount method. Instead, Livewire provides you with the capability to pass parameters to the component through the URL, and it will try to match those parameters to the constructor or the mount method of the component.

That's why in the documentation, the example is in the ShowPost component and it expects parameter/s from route to match in the mount().

What I'll do I want to initialize all the categories:

// If you want to call it in mount()
public Collection $category;
		
public function mount(){
    $this->category = Category::all() // you can select specific columns
}

// Or just directly pass it to render without declaring above code

public function render()
{
    $categories = Category::withCount('recipes')
        ->when('name', function ($query) {
                    $query->where('name', 'like', '%'. public wire:model instance here .'%');
        })->orderBy('name')->paginate(10);

    return view('livewire.create-post', compact('categories'));
}
1 like
S1d's avatar

However if your route has parameter like

Route::get('/categories/{category}', ShowCategory::class);

There will be no problem

public Category $category;
		
public function mount(Category $category){
    $this->category = $category;
}

It will work

1 like
vincent15000's avatar

@S1d I'm not sure that you have read my post entirely and the different comments ;).

Snapey's avatar
Snapey
Best Answer
Level 122

You could probably create a new, empty model and then set an array so that you have all the right model properties:

public array $category;

public function mount()
{
     $this->category = new Category()->toArray();
}

then in your front end you can wire:model the input fields to the array members

eg

<input name="name" wire:model="category.name" />

Treat it as an array until you want to persist it, and then write the array to eloquent.

2 likes
vincent15000's avatar

@Snapey With the previous Livewire version, I handled the Category model and not an array. This was easier when I wanted to save a new category, I only had to execute $category->save().

If it's not possible to do another way, I will try with an array, your suggestion seems to be very interesting.

1 like
kokoshneta's avatar

@fabianm1705 Yes, because it’s invalid PHP. The new keyword returns a new instance, but that instance is not immediately chainable; it needs to be wrapped in brackets to work. Both these are valid:

// Declare and use separately
$category = new Category();
return $category->toArray();

// Wrap declaration in brackets
return (new Category())->toArray();
1 like
vincent15000's avatar

Thank you very much.

I will also try with a Livewire Form. I will perhaps start a new post for Livewire Form if I don't get it work.

Sabonzy's avatar

you can continue to use the V2 model behavior by setting legacy_model_binding to true In your livewire.php config

2 likes
kingsleyuchenna's avatar

FROM THE DOCS

"Livewire 2 supported wire:model binding directly to Eloquent model properties. For example, the following was a common pattern:"

public Post $post;
 
protected $rules = [
    'post.title' => 'required',
    'post.description' => 'required',
];
<input wire:model="post.title">
<input wire:model="post.description">

In Livewire 3, binding directly to Eloquent models has been disabled in favor of using individual properties, or extracting Form Objects.

However, because this behavior is so heavily relied upon in Livewire applications, version 3 maintains support for this behavior via a configuration item in config/livewire.php:

'legacy_model_binding' => true,

By setting legacy_model_binding to true, Livewire will handle Eloquent model properties exactly as it did in version 2.

Eloquent model binding

4 likes

Please or to participate in this conversation.