Search code examples
phplaraveldatabaselaravel-livewire

Problem with Laravel Livewire : Attempt to read property "recipes" on array


I'm trying to do a search input from 2 datatables of my database, one containing vegetables and the other one containing recipes made of vegetables, i first made my search input with only the elements from the vegetable table. But now im trying to include the repice table but it wont work: I'm getting the error "Attempt to read property "recipes" on array" but I do't understand where the problem comes from.

This is my code:

Search.php:

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Vegetable;

class Search extends Component
{
    public $query = '';

    public $vegetables;
    public $recipes;

    public function updatedQuery() 
    {
        $this->vegetables = Vegetable::where('name', 'like', '%'.$this->query.'%')->get()->toArray();
    }

    public function render()
    {
        return view('livewire.search');
    }
}

search.blade.php :

<div class="searchcomponent">
    <h1>Recherche</h1>
    <input wire:model="query" type="text" placeholder="Rechercher...">
    
    @if(!empty($query))
        <ul>
            @if(!empty($vegetables))
                <div class="vegetables">

                    @foreach($vegetables as $vegetable)
                        <li><span class="material-icons">lunch_dining</span>{{ $vegetable['name'] }}</li>
                    @endforeach
                
                </div>
            @else
                <li>Pas de résultat</li>
            @endif
            
            <div class="recipes">

                @foreach($vegetables as $vegetable)
                    @foreach($vegetable->recipes as $recipe)
                        <li><span class="material-icons">menu_book</span>{{ $recipe['name'] }}<span class="ingredient">Ingrédient: {{ $vegetable['name'] }}</span></li>
                    @endforeach
                @endforeach
            </div>
        </ul>
    @endif
</div>

My model Vegetable :

<?php

namespace App\Models;

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

class Vegetable extends Model
{
    use HasFactory;

    public $timestamps = false;

    protected $fillable = ['name'];

    public function recipes(){
        return $this->belongsToMany(Recipe::class, 'vegetables_recipes', 'vegetable_id', 'recipe_id');
    }

    public function getName($id) {
        return $this->name;
    }
}

My model Recipe :

<?php

namespace App\Models;

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

class Recipe extends Model
{
    use HasFactory;
    public $timestamps = false;

    protected $fillable = ['name'];

    public function vegetables(){
        return $this->hasOne(Vegetable::class, 'vegetables_recipes', 'recipe_id', 'vegetable_id');
    }

    public function getName($id) {
        return $this->name;
    }
}

The model from my pivot table VegetablesRecipe :

<?php

namespace App\Models;

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

class VegetablesRecipe extends Model
{
    use HasFactory;

    public $timestamps = false;

    protected $fillable = ['vegetable_id', 'recipe_id'];
}

Thank you in advance


Solution

  • So I found the issue with your code; I didn't notice it yesterday as it was hidden off-screen in your Search.php's updatedQuery() function:

    $this->vegetables = Vegetable::where('name', 'like', '%'.$this->query.'%')
    ->get()
    ->toArray();
    

    When you call ->toArray(), you convert the Collection of Vegetable Model instances into an Array of Arrays. This means you cannot use Object Access to get Properties:

    $vegetable->recipes; // Fails with error "Attempt to read property "recipes" on array"
    

    Additionally, you cannot access any Model functions, as an Array is not an instance of a Model:

    $vegetables['recipes']; // Undefined array key "recipes"
    

    Since public function recipes() is a method of the Vegetable.php Model class, that won't work. It can work, if you load it first before casting to an Array:

    $this->vegetables = Vegetable::where('name', 'like', '%'.$this->query.'%')
    ->with('recipes')
    ->get()
    ->toArray();
    

    Now, $vegetable['recipes'] will work, but it's better to just drop the ->toArray() completely:

    $this->vegetables = Vegetable::where('name', 'like', '%'.$this->query.'%')
    ->with('recipes')
    ->get();
    

    Additionally, for performance reasons, you want to include ->with('recipes') to prevent N+1 queries being called while looping.

    With those changes made, you can write your livewire/search.blade.php code as follows:

    <div class="searchcomponent">
      <h1>Recherche</h1>
      <input wire:model="query" type="text" placeholder="Rechercher...">
      @if(!empty($query))
        <ul>
          @if(!empty($vegetables))
            <div class="vegetables">
              @foreach($vegetables as $vegetable)
                <li><span class="material-icons">lunch_dining</span>{{ $vegetable->name }}</li>
              @endforeach
            </div>
          @else
            <li>Pas de résultat</li>
          @endif
          <div class="recipes">
            @foreach($vegetables as $vegetable)
              @foreach($vegetable->recipes as $recipe)
                <li><span class="material-icons">menu_book</span>{{ $recipe->name }}<span class="ingredient">Ingrédient: {{ $vegetable->name }}</span></li>
              @endforeach
            @endforeach
          </div>
        </ul>
      @endif
    </div>
    

    Lastly, some cleanup:

    Vegetable.php and Recipe.php

    The methods getName($id) have no purpose; you're not doing anything with $id, and name is not a private property; you can simply do $vegetable->name or $recipe->name and it will do the same as what these methods are doing.

    Recipe.php

    As stated in the comments, belongsToMany() is the inverse of belongsToMany(), not hasOne():

    public function vegetables(){
      return $this->belongsToMany(Vegetable::class, 'vegetables_recipes');
    }
    

    Additionally, if the primary and foreign key names match the model names, you don't need them. The proper name for the pivot table would be recipes_vegetables (plural, alphabetical), so specifying that is required. You can do the same for Vegetable.php:

    public function recipes(){
      return $this->belongsToMany(Recipe::class, 'vegetables_recipes');
    }
    

    Lastly, your model VegetablesRecipe.php is not needed, and is currently not being used. You typically don't define a Model for a Pivot table, so you can either remove it, or keep it around should you ever need to directly modify the Pivot.