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

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

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.

Related

Can't display the values from my data base with Laravel Livewire

I'm using a Livewire component, this is my code:
Search.php :
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Recipe;
use App\Models\Vegetable;
use App\Models\VegetablesRecipe;
class Search extends Component
{
public $query = '';
public $vegetables;
public function mount()
{
$this->resetQuery();
}
public function resetQuery()
{
$this->vegetables = [];
}
public function render()
{
if ($this->query != null) {
return view('livewire.search', [
'vegetables' => Vegetable::where('name', 'like', '%'.$this->query.'%')->get()->toArray()
]);
} else {
return view('livewire.search', [
'vegetables' => Vegetable::all()->toArray()
]);
}
}
}
search.blade.php :
<div class="searchcomponent">
<h1>Search</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>No result</li>
#endif
</ul>
#endif
</div>
My vegetable Model:
<?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 problem is, every time im typing something in my search bar, I just have a No result appearing on my screen even tho i seeded my data base with some vegetables already. Why doesn't it show me for example Carrot when i type it?
Or how can i fill my array vegetables with all the names of my vegetables datatable ?
Defining public $vegetables; will prevent you from passing it to the view.
Remove that. Also remove your mounting logic.
Thus you should have:
class Search extends Component
{
public $query = '';
public function render()
{
if ($this->query != null) {
return view('livewire.search', [
'vegetables' => Vegetable::where('name', 'like', '%'.$this->query.'%')->get()->toArray()
]);
} else {
return view('livewire.search', [
'vegetables' => Vegetable::all()->toArray()
]);
}
}
}
Also make sure you are calling #livewireScripts and #livewireStyles or their alternative syntaxes <livewire:styles /> <livewire:scripts /> from your main view which uses <livewire:search />.
Edit:
Alternatively, you could also use a public properly and write to it using $this->vegetables like so:
class Search extends Component
{
public $query = '';
public $vegetables;
public function render()
{
$this->vegetables = Vegetable::where('name', 'like', '%' . $this->query . '%')->get()->toArray();
return view('livewire.search');
}
}
Also, you can remove the #if(!empty($query)) and the #if(!empty($vegetables)) in place of a #forelse utilising the #empty statement rather than a #foreach. E.g.
#forelse($vegetables as $vegetable)
<li><span class="material-icons">lunch_dining</span>{{ $vegetable['name'] }}</li>
#empty
<li>No result</li>
#endforelse
That way, you will still get a list of your vegetables when the search box is empty, and if it's not empty but there's no matching results, you'll get your "No result" message.
This is because on the mount you are resetting the vegetable array to an empty array.
Refer to this fiddle for more details:
https://laravelplayground.com/#/snippets/f51e212a-dab3-4325-b6b0-4c6af4c0ab72

Laravel showing the categories of book with pivot table error

I am learning Laravel and i have two tables books and categories
i created a pivot table with the book_id and category_id columns to make a relationship between the tables, but when i tried to show the categories in the show book page and i made foreach on the $books->categories i faced an error saying that the foreach parameter is null
the Book model file:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
use HasFactory;
protected $fillable = ['title', 'desc', 'img'];
public function categories()
{
return $this->belongsTo('App\Models\Category');
}
}
the Category model file
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = ['name'];
public function books()
{
return $this->belongsTo(Book::class)->withPivot('book_category');
}
}
the show.blade page:
#extends('layout')
#section('title')
Book #{{ $book->id }}
#endsection
#section('content')
<h2>{{ $book->title }}</h2>
<p>{{ $book->desc }}</p>
#foreach ($book->categories as $category)
<span class="text-muted mx-3">{{ $category->name }}</span>
#endforeach
<div class="row">
<div class="col-md-3 p-5">
<img src="{{ asset('uploads/books') }}/{{$book->img}}" class="w-100">
</div>
</div>
Back
Edit
Delete
#endsection
the pivot table:
the pivot table
the BookController code:
public function show($id)
{
$book = Book::findOrFail($id);
return view('books/show', compact('book'));
}
You can change relationship to belongsToMany since you are using pivot table.
In Book model
public function categories()
{
return $this->belongsToMany(Category::class);
}
and in Category model
public function books()
{
return $this->belongsToMany(Book::class);
}
In your controller
$book = Book::with('categories')->findOrFail($id);
so in your view
{{$book->$title}}
#foreach ($book->categories as $category)
<span class="text-muted mx-3">{{ $category->name }}</span>
#endforeach

App\Todo::lead must return a relationship instance, but "null" was returned. Was the "return" keyword used?

I have Todo and Tbl_leads model and there corresponding tables todos and tbl_leads. When I am trying todo have the lead name it throw me an error.
#This is Tbl_leads model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tbl_leads extends Model
{
//Table Name
protected $table = 'tbl_leads';
//Primary key
public $primaryKey = 'ld_id';
//Timestamps
public $timestamps = true;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'ld_id', 'first_name', 'last_name', 'email',
];
public function tasks() {
$this->hasMany('App\Todo', 'lead_id','ld_id');
}
}
This is Todo Model
<?php
namespace App;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Todo extends Model
{
use SoftDeletes;
protected $fillable = [
'title',
'description',
'user_id',
'outcome_id',
'lead_id',
'tasktype_id',
'due_time',
'complete_time',
];
protected $casts = [
'due_time' => 'datetime',
'complete_time',
];
public function lead() {
$this->belongsTo('App\Tbl_leads', 'lead_id');
}
}
This is my index method
public function index()
{
$tasks = Todo::latest()->paginate(5);
return view('taskmanagement.cruds.index',compact('tasks'));
}
this is blade where I want get the the first name from Tbl_leads model
div class="card-body">
<ul class="todo-list" data-widget="todo-list">
#forelse($tasks as $task)
<div class="col-3">
#if(!empty($task->lead_id))
<div>
<small>Related to</small>
</div>
<div class="mt-0">
<a href="#">
<strong class="mx-4">{{$task->lead->first_name}}</strong>
</a>
</div>
#endif
</div>
</div>
</div>
</li>
#empty
<p class="text-center">No Tasks is available</p>
#endforelse
Plz can anyone tells me what I have done wrong. And I know the model name and there primary key slightly different is not set accordingly to laravel. Is my model relationship correct?
Let me clarify few points to you. Firstly you need to put a return in each relationship. For example:
public function lead() {
return $this->belongsTo('App\Tbl_leads', 'lead_id');
}
Next thing you are showing this portion {{$task->lead->first_name}} which is called lazy loading. It means if you are displaying 100 records of Todo then you are querying database 101 times. 1 for Todo and 100 times for lead->first_name. Which is not good. So what you can do in your index method pass your relation in with() so that it will be eager loaded. Means it will become just one or two query or simply a join. So it will be fast. Example of your index method...
public function index()
{
$tasks = Todo::with('lead')->latest()->paginate(5);
return view('taskmanagement.cruds.index',compact('tasks'));
}

Append new element to eloquent collection

I have two tables posts and photos. Each post has 5 photos. I want to list in view each post with one photo (profile pic), first picture.
$published_post = Post::where('created_by',auth()->user()->id)
->where('status','published')
->get();
$photo = Photo::where('post',$published_post->id)->get();
These two gives me two different collection. How can I add the first photo of a particular post to its array so in view I can display using a foreach loop.
This is how I want in view:
#foreach($published_post as $item)
{{ $item->title }}
{{ $item->profile_photo }}
#endforeach
I tried put and push, but doesn't seem to be working. Not sure how exactly does we append a new key value pair to an object.
My two models:
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->timestamps();
});
Schema::create('photos', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('image');
$table->integer('post');
$table->timestamps();
});
class Post extends Model
{
protected $table = 'posts';
}
class Photo extends Model
{
protected $table = 'photos';
protected $fillable = ['image', 'post'];
}
Post Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/*This is used to specify the table which this model is associated with*/
protected $table = 'posts';
protected $fillable = [
'title'
];
public $timestamps = true;
public function photos(){
return $this->hasMany(Photos::class,'post');
//post is the foreign key for posts table
}
}
Photo Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Photo extends Model
{
/*This is used to specify the table which this model is associated with*/
protected $table = 'photos';
protected $fillable = [
'image', 'post'
];
public $timestamps = true;
}
View:
#foreach($published_post as $item)
{{ $item->title }}
{{ $item->photos->first()->image }} // photos relation is invoked and fetched first image
#endforeach
You need to create 2 Models, one for Posts and one for Photos.
php artisan make:model Post
php artisan make:model Photo
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Posts extends Model
{
//
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Photo extends Model
{
//
}
Then create a hasMany relationship on the Post model to link to the Photo model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Photo;
class Post extends Model
{
public function photos()
{
return $this->hasMany(Photo::class);
}
}
Then in your view you can lazy load the photos whenever you like
#foreach($posts as $post)
{{ $post->title }}
{{ $post->photo[0]->name}}
#endforeach
The syntax to go in your view will be slightly different, but this gives you a good idea on how the functionality should work.
Ok, first you should change your Post model like this:
class Post extends Model
{
protected $table = 'posts';
public function photos()
{
return $this->hasMany(Photo::class, 'post');
}
}
And then, add the following to your Photo model:
class Photo extends Model
{
protected $table = 'photos';
public function post()
{
return $this->belongsTo(Post::class, 'post');
}
}
With this, you've created the relation between your models, and now you can get your data this way:
$published_post = Post::where('created_by',auth()->user()->id)
->where('status','published')
->with('photos')
->get();
And in your view, you can get the first photo this way:
#foreach($published_post as $item)
{{ $item->title }}
{{ $item->photos()->first()->name }}
#endforeach
For more info on relations, you might want to read the docs.
I hope this helps!

Logical complex insert into 2 following tables - Laravel

I have a house management project and i am trying to execute a complex function that consists of:
Each time i insert a new Bill into the DB, another table will be filled creating a new row for each share of this bill according to the number of flatmates this house has. I managed to insert into the Bills table but the program returns me to the expected page but with no insert into the Shares table. Not sure if logically i'm doing alright. The code bellow is how i tried to retrieve information of the last insert into the Bills table, which should then have its objects properties used into the Shares table. Does someone have any clue on how i can i proceed?
This is my controller function:
public function store(Request $request){
$bill = Bill::create($request->all());
$users = User::where('house_id', Auth::user()->house->id);
$nflatmates = Auth::user()->house->nflatmates;
$shared_amount = $bill->bill_amount / $nflatmates;
foreach($users as $user){
$data = ['user_id'=>$user->id,
'bill_id'=>$bill->id,
'share_amount'=>$shared_amount];
Share::create($data);
}
return redirect('/admin/bills');
}
This is my form blade. I believe the problem doesnt come from here. Just in case.
{!! Form::open(['method'=>'post', 'action'=>'AdminBillsController#store']) !!}
<div class="form-group' has-error' : '' }}">
<div class="col-md-6">
{!! Form::text('description',null,['class'=>'form-control', 'placeholder'=>'Bill Description']) !!}
</div>
</div>
<div class="form-group' has-error' : '' }}">
<div class="col-md-6">
{!! Form::number('bill_amount',null,['class'=>'form-control', 'placeholder'=>'Amount', 'required|between:0,99.99']) !!}
</div>
</div>
<input type="hidden" name="house_id" value="{{Auth::user()->house->id}}">
<br>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<button type="submit" class="btn btn-primary">
Register
</button>
</div>
</div>
{!! Form::close() !!}
These are my relationships:
This is the Share Model
<?php
namespace App;
use App\User;
use App\Bill;
use Illuminate\Database\Eloquent\Model;
class Share extends Model{
protected $fillable = [
'id', 'user_id', 'bill_id', 'share_amount', 'share_status'
];
public function user(){
return $this->belongsTo('App\User');
}
public function bill(){
return $this->belongsTo('App\Bill');
}
}
And this is the Bill Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\House;
use App\User;
use App\Share;
class Bill extends Model{
protected $fillable = [
'id', 'description', 'bill_amount', 'house_id'
];
public function house(){
return $this->belongsTo('App\House');
}
public function share(){
return $this->hasMany('App\Share');
}
}
This is the User Model:
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\House;
use App\Role;
use App\Task;
use App\Share;
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'id','name', 'email', 'is_active','house_id','role_id','password',
];
protected $hidden = [
'password', 'remember_token',
];
public function house(){
return $this->belongsTo('App\House');
}
public function role(){
return $this->belongsTo('App\Role');
}
public function task(){
return $this->hasMany('App\Task');
}
public function share(){
return $this->hasMany('App\Share');
}
}
And this is the house Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\User;
use App\Bill;
class House extends Model {
protected $fillable = [
'id','house_address', 'house_admin', 'nflatmates'
];
public function user(){
return $this->hasMany('App\User');
}
public function bill(){
return $this->hasMany('App\Bill');
}
}
The thing is that you obviously have oneToMany relationship there, so what you want to do is something like this.
public function store(Request $request){
$bill = Bill::create($request->all());
$users = User::where('house_id', Auth::user()->house->id);
$nflatmates = Auth::user()->house->nflatmates;
$shared_amount = $bill->bill_amount / $nflatmates;
foreach($users as $user){
$data = [
'share_amount' => $shared_amount,
'share_status' => 'XXXXXX'
];
$share = new Share($data);
//Associating user with this share
$share->user()->associate($user);
//Associating bill with this share
$share->bill()->associate($bill);
//Saving share
$share->save();
}
return redirect('/admin/bills');
}
EDIT:
In order for the code above to work, you must have a valid relationships set across your models.
EDIT 2:
I thought that nflatmates was a oneToMany relationship, but it isn't so there is no need for attach function.
We are now creating a Share object and through it's relationships that are defined we are using associate function based on Belongs To Relationships which you can find on official docs, just scroll down a bit.

Categories