Nested Eager loading relationships in Laravel - laravel

I am creating a forum software using Laravel and I'm trying to access the most recent posts made within a specific topic using latestPost as defined in my model below
public function latestPost() {
return $this->hasOne('App\Post')->with('thread')->with('author')->latest();
}
Here is how the nested relationships are defined in App\Post.php
public function author()
{
return $this->belongsTo('App\User', 'author_id');
}
public function thread()
{
return $this->belongsTo('App\Thread', 'thread_id');
}
public function topic() {
return $this->belongsTo('App\Topic');
}
I have been able to access posts of threads, authors etc no problem, and when I have {{ $topic->latestPost }} in my views, it will list the object including relationships successfully. But as soon as I try to display a specific part such as {{ $topic->latestPost->author->username }} I am getting the following error: Trying to get property of non-object
In this specific view, topics is yet another relationship pulled from Categories like so in the Controller and getting $topics using a #foreach on $categories->topics:
public function index() {
$categories = Category::orderBy('order', 'asc')->get();
return view('welcome', compact('categories'));
}
Here is a dump of $categories: http://dumptext.com/H8Nq16ea

Maybe you want to use nested eager loading.
Change: return $this->hasOne('App\Post')->with('thread')->with('author')->latest();
To this: return $this->hasOne('App\Post')->with('thread.author')->latest();
Also i never heard about latest() and the end of your return statement. Maybe you can try it with orderBy as shown below.
`return $this->hasOne('App\Post')->with('thread.author')->orderBy('id')->first;`
I guess you can use the orderBy('id')->reverse() function if the result is not in the order you want. Let me know if it helped you out.

You should make sure you have author for latest post you want to display. Otherwise if you expect not to have one for each record, you should try:
{{ $topic->latestPost->author ? $topic->latestPost->author->username : 'No author' }}
In addition instead of:
return $this->hasOne('App\Post')->with('thread')->with('author')->latest();
you can use
return $this->hasOne('App\Post')->with('thread','author')->latest();

Related

Laravel Route with Multiple Parameters - How to get correct database result

I am a couple of weeks in to learning Laravel and have come across a problem which I can not find the answer to by myself, or online.
I am building a directory website with urls structured like:
directory.co.uk/parks
directory.co.uk/parks/{county-name}
directory.co.uk/parks/{county-name}/{park-name}
As {park-name} is not unique, I am struggling to return the page for an individual park. The controller needs to look up the county.id using the county.slug and then the park.id using the park.county_id and the park.slug.
I have routes in the web.php file such as:
Route::get('/parks','ParksController#index')->name('parks');
Route::get('/parks/{county}/{park}','ParksController#show')->name('park');
I have Parks and Counties models and (belongsTo and hasMany relationships set up between the two).
I have this is in both models:
public function getRouteKeyName()
{
return 'seo_url';
}
Then in my ParksController, I am at a loss. I currently have:
public function show(Counties $county, Parks $park)
{
//return $park;
//dd($park);
return view('parks.park', ['park'=>$park]);
}
I have also tried the non-Eloquent way:
public function show($county_slug,$park_slug)
{
$county = DB::table('counties')->where('seo_url',$county_slug)->get();
$county_id = $county->pluck('id');
$park = DB::table('parks')->where('county_id', $county_id)->where('seo_url', $park_slug)->get();
//dd($county_id);
//return $park;
return view('parks.park', ['park'=>$park]);
}
This returns a 404 error. Any help would be much appreciated. (I have done a lot of reading on Route model binding, but can not see any examples like mine.)
Laravel has an undocumented feature in its explicit model binding, where the callback can be given the current Route the binding is for. This can allow you to access the other parameters and use them to add conditionals.
Router::bind('park', static function ($value, Route $route) {
$query = Parks::where('seo_url', '=', $value);
if ($route->hasParameter('county')) {
$county = $route->parameter('county');
$query->where('county_id', '=', $county instanceof Counties ? $county->id : $county);
}
return $query->first() ?? abort(404);
});

How to retrieve multiple relations with multiple tables in laravel eloquent

I'm using Laravel 5.8 to build a babysitting site. I have 4 tables with different relationships as below:
please see this image
The relationships are:
Babysitter->hasMany(session)
Sessions->hasOne(Review)
Sessions->hasOne(Kids)
Sessions->hasOne(Babysitter)
Sessions->hasOne(Parent)
I want to achieve 2 things:
First one
I want to show this result when listing all babysitters. I'm showing this information for each babysitter:
plsease see this image
See here what I couldn't achieve
plsease see this image
This is my code
Sitters::where('Status', 'active')->where('Verified', 1)->get();
Second one
Also, I've tried to show kids name with parent review as shown here:
plsease see this image
This is what i'm using
Sessions::select('Reviews.*', 'Sessions.Parent_id')->join('Reviews', 'Reviews.Session_id', '=', 'Sessions.id')->with('owner')->where('Trainer_id', session('user')->Id)->where('Status', '=', 'complete')->with('owner')->orderBy('Sessions.id', 'DESC')->get();
Here is Session.php Model
public function owner(){
return $this->belongsTo('App\Models\Parents', 'Parent_id');
}
As discussed change the relations:
Babysitter->hasMany(sesstion)
Sessions->hasOne(Review)
Sessions->belongsTo(Kids)
Sessions->belongsTo(Babysitter)
Sessions->belongsTo(Parent)
First one
in Babysitter.php declare the following attributes
class Babysitter extends Model
{
public function reviews()
{
$this->hasManyThrough(Review::class, Session::class);
}
public function getAverageReviewAttribute()
{
return $this->reviews()->avg('Rating');
}
}
Then you just need to call it on the model instance.
$babysitter = Babysitter::first();
return $babysitter->average_review;
Second one
Just use the relation
$babysitter = BabySitter::with(['sessions' => public function ($session) {
$session->with(['review','parent','kids']);
})->where('trainer_id', '=', session('user')->Id) //did not understand this condition
->first();
This assumes you have parent, kids and review relation declared on Session::class. (change the names if needed)
After a few days of searching & testing, this is what worked for me:
Inside (Sitters) Model, put this relation
public function sessions()
{
return $this->hasMany(Sessions::class, 'sitter_id')
->withCount('reviews')
->withCount(['reviews as review_avg' => function($query){
$query->select(DB::raw('AVG(Rating)'));
}]);
}
Also, inside (Sessions) Model, put this relation
public function reviews()
{
return $this->hasOne(Reviews::class, 'Session_id');
}
Now you query like this
return $sitters = Sitters::with('sessions')->get();
I hope this can help someone :)

How to use where condition in laravel eloquent

I am using laravel eloquent. I have fetched data from two table using eloquent.
I have post table and chat table. For post table I have model Post.php and for chat table I have model Chat.php. Here is the the eloquent relation I have created to fetch chat for individual post for a user.
in Post.php
public function TeamMessage()
{
return $this->hasMany('App\Chat','post_id');
}
And in Chat.php
public function ChatRelation()
{
return $this->belongsTo('App\Post');
}
it is working perfect. But this relation fetch all messages for a specific post. I want to fetch all unread message from chat table. I have a column named unread in chat table.
Now my question is how I can fetch only unread message for a specific post.
While the other answers all work, they either depend on scopes (which are very useful in many circumstances) or on you having already instantiated an instance of $post, which doesn't let you eager load multiple posts with their messages.
The dynamic solution is this, which will let you fetch either 1 or more posts and eager load their messages with subquery:
$posts = Post::with(['TeamMessage' => function ($query) {
$query->where('unread', true); // This part applies to the TeamMessage query
}])->get();
See in documentation
Edit:
If you, however, want to filter the posts, to only show those that have unread messages, you need to use whereHas instead of with:
$posts = Post::whereHas(['TeamMessage' => function ($query) {
$query->where('unread', true); // This part applies to the TeamMessage query
}])->get();
More in the documentation.
You can also chain whereHas(...) with with(...).
For querying relationships, you have to call them as functions instead of properties, like this:
$unreadPosts = $post->TeamMessage()->where('unread', true)->get();
For more information on this you can take a look at the docs.
You need to create a local scope on your model, information on local scopes can be found here: https://laravel.com/docs/5.6/eloquent#local-scopes
public function scopeUnread($query)
{
return $query->where('unread', 1);
}
Then in your controller/view
$unread = $yourmodel->unread()
First I would change your relation names to the name of the entity in lower case:
in Post.php
public function chats()
{
return $this->hasMany('App\Chat','post_id');
}
And in Chat.php
public function post()
{
return $this->belongsTo('App\Post');
}
public function scopeUnread($query)
{
return $query->where('unread', 1);
}
Then you can use
$post->chats()->unread()->get();

Laravel oneToMany accessor usage in eloquent and datatables

On my User model I have the following:
public function isOnline()
{
return $this->hasMany('App\Accounting', 'userid')->select('rtype')->latest('ts');
}
The accounting table has activity records and I'd like this to return the latest value for field 'rtype' for a userid when used.
In my controller I am doing the following:
$builder = App\User::query()
->select(...fields I want...)
->with('isOnline')
->ofType($realm);
return $datatables->eloquent($builder)
->addColumn('info', function ($user) {
return $user->isOnline;
}
})
However I don't get the value of 'rtype' for the users in the table and no errors.
It looks like you're not defining your relationship correctly. Your isOnline method creates a HasMany relation but runs the select method and then the latest method on it, which will end up returning a Builder object.
The correct approach is to only return the HasMany object from your method and it will be treated as a relation.
public function accounts()
{
return $this->hasMany('App\Accounting', 'userid');
}
Then if you want an isOnline helper method in your App\User class you can add one like this:
public function isOnline()
{
// This gives you a collection of \App\Accounting objects
$usersAccounts = $this->accounts;
// Do something with the user's accounts, e.g. grab the last "account"
$lastAccount = $usersAccounts->last();
if ($lastAccount) {
// If we found an account, return the rtype column
return $lastAccount->rtype;
}
// Return something else
return false;
}
Then in your controller you can eager load the relationship:
$users = User::with('accounts')->get(['field_one', 'field_two]);
Then you can do whatever you want with each App\User object, such as calling the isOnline method.
Edit
After some further digging, it seems to be the select on your relationship that is causing the problem. I did a similar thing in one of my own projects and found that no results were returned for my relation. Adding latest seemed to work alright though.
So you should remove the select part at very least in your relation definition. When you only want to retrieve certain fields when eager loading your relation you should be able to specify them when using with like this:
// Should bring back Accounting instances ONLY with rtype field present
User::with('accounts:rtype');
This is the case for Laravel 5.5 at least, I am not sure about previous versions. See here for more information, under the heading labelled Eager Loading Specific Columns
Thanks Jonathon
USER MODEL
public function accounting()
{
return $this->hasMany('App\Accounting', 'userid', 'userid');
}
public function isOnline()
{
$rtype = $this->accounting()
->latest('ts')
->limit(1)
->pluck('rtype')
->first();
if ($rtype == 'Alive') {
return true;
}
return false;
}
CONTROLLER
$builder = App\User::with('accounting:rtype')->ofType($filterRealm);
return $datatables->eloquent($builder)
->addColumn('info', function (App\User $user) {
/*
THIS HAS BEEN SUCCINCTLY TRIMMED TO BE AS RELEVANT AS POSSIBLE.
ARRAY IS USED AS OTHER VALUES ARE ADDED, JUST NOT SHOWN HERE
*/
$info[];
if ($user->isOnline()) {
$info[] = 'Online';
} else {
$info[] = 'Offline';
}
return implode(' ', $info);
})->make();

Route Parameters laravel issue

I simply made this but /maps/{category}/{map} does not work correctly.
putting anything on {category} showing same result.
/maps/php/1
/maps/laravel/1
I want to show a result when category's name and map's id exactly matches otherwise redirect to homepage.
My route
Route::get('/maps/{category}', 'MapsController#index');
Route::get('/maps/{category}/{map}', 'MapsController#show');
My controller
public function show(Category $category, Map $map)
{
return view('maps.show', compact('map'));
}
My blade template
{{ $map->title }}
Swap over the routes in your routes file so that the more specific one is first.
Route::get('/maps/{category}/{map}', 'MapsController#show');
Route::get('/maps/{category}', 'MapsController#index');
Firstly, there is no need to swap order of routes if you are using 5.4, since I started with 5.4 to use laravel i cannot say anything for previous versions.
If you you want to filter asked map if it matches given category, you can use "whereHas" method with eloquent if you defined relationship between category and map.
"One To Many (Inverse)" relationship is what you need to use, check here: https://laravel.com/docs/5.4/eloquent-relationships#one-to-many-inverse
And querying relationship is what you need to know, check here: https://laravel.com/docs/5.4/eloquent-relationships#querying-relationship-existence
How your relationship should look like in Map model:
/**
* Get the category that owns the map.
*/
public function category()
{
return $this->belongsTo('App\Category');
}
An example code:
public function show($category, $map)
{
$map = Map::whereHas('category',function($query) use($category){
$query->where('id', $category);
});
return view('maps.show', compact('map'));
}
Route::get('/maps/{category}', 'MapsController#index');
is a more generalized route so it matches more than
Route::get('/maps/{category}/{map}', 'MapsController#show');
You should list the show route first.
To match an exact model attribute (i.e. name), you should customize the resolution logic the router uses in the route service provider boot method. For example:
Route::bind('category', function ($value) {
return App\Category::where('name', $value)->first();
});
Route::bind('map', function ($value) {
return App\Map::where('id', $value)->first();
});
public function show($category,$map)
{
$category = App\Category::where('category_name',$category);
$map_id =App\Map::where('map_id',$map);
if(!empty($category) && !empty($map_id))
{
return view('maps.show', compact('map'));
}
else
{
return view('homepage', compact('map'));
}
}

Resources