Semi-complex Eloquent query - php

I am working on a eloquent query to compile a newsletter but I have hit a brick wall.
What I'm trying to do is create a UI where the user can select a publication and date. Ideally it would then compile a list of that publication's categories (where stories > 0) and stories belonging to it.
Here are my 3 models:
Story
public function user()
{
return $this->belongsTo('User', 'user_id');
}
public function publication()
{
return $this->belongsTo('Workbench\Dailies\Publication', 'publication_id');
}
public function category()
{
return $this->belongsTo('Workbench\Dailies\Category', 'category_id');
}
Publication
public function story()
{
return $this->belongsTo('Workbench\Dailies\Story');
}
public function stories()
{
return $this->hasMany('Workbench\Dailies\Story', 'publication_id');
}
public function category()
{
return $this->belongsTo('Workbench\Dailies\Category', 'publication_id');
}
Category
public function story()
{
return $this->belongsTo('Workbench\Dailies\Story', 'category_id');
}
public function publications()
{
return $this->belongsTo('Workbench\Dailies\Publication', 'publication_id');
}
public function stories()
{
return $this->hasMany('Workbench\Dailies\Story', 'category_id');
}
Here is how my tables look:
Story
content
user_id
publication_id
category_id
publish_date
Publication
id
name
Category
id
name
publication_id
Here is what I currently have in my Repository.
public function compileStories($input)
{
return Category::has('stories', '>', 0)
->with('publications')
->whereHas('stories', function ($query) use ($input)
{
$query->where('publish_date', $input['publish_date']);
$query->where('publication_id', $input['publication_id']);
});
}
Am I headed in the right direction here or is there any way to improve the code above? It is not currently functioning as expected.

There are a couple of things I see here that may help straighten you out.
First - Some of the models have strange relationships without knowing more about your whole application. The Story model does not need the publication relationship since it can be retrieved through the category relationship, unless you have need of it otherwise. The Category model does not need both a story and a stories relationship, again, unless there's more to the story I don't know. In your example, you should only need the hasMany relationship. The Publication model only needs the categories relationship.
Now, after some cleanup of the models, let's look at your query. Using the category model to return your results seems completely appropriate for your desired results. You can check for the publication without having to dive into your stories, though. I haven't tested it, but you may not need the use $input line since $input is in the larger scope. You're also missing a conditional check in your where statments in the whereHas clause. The query should be able to be simplified as follows:
Category::where('publication_id', '=', $input['publication_id'])
->whereHas('stories', function($query)
{
$query->where('publish_date', '=', $input['publish_date']);
})
->get()

Related

Laravel 5.8 - How sort (orderBy) multiple relationship

I am trying to sort the serials by video views.
Relations:
The Serial has a hasMany relationship to series.
The Series has a hasMany relationship to episodes.
The Episodes has a hasOne relationship to video.
The Video has a hasMany relationship to viewcounts.
<?php
//sort method:
public function mostPopular()
{
$serials = Serial::with(['series.episodes.video' => function ($query) {
$query->withCount(['videoViews' => function($query) {
}])->orderBy('video_views_count', 'desc');
}])->get();
return $serials;
}
//Serial model:
public function series()
{
return $this->hasMany(Series::class);
}
//Series model:
public function episodes()
{
return $this->hasMany(Episode::class);
}
public function serial()
{
return $this->belongsTo(Serial::class);
}
//Episode model:
public function video()
{
return $this->hasOne(Video::class);
}
public function series()
{
return $this->belongsTo(Series::class);
}
//Video model:
public function videoViews()
{
return $this->hasMany(VideoView::class);
}
public function episode()
{
return $this->belongsTo(Episode::class);
}
?>
I expect the sorted serials by video views (series.episodes.video.videoViews), but the actual output is not sorted.
Laravel 5.8
PHP 7
This is a silly one actually but I've learnt that multiple ->sortBy on collections actually are possible with no workarounds. It's just that you need to reverse the order of them. So, to sort a catalogue of artists with their album titles this would be the solution...
Instead of :
$collection->sortBy('artist')->sortBy('title');
Do this :
$collection->sortBy('title')->sortBy('artist');
Because "With" queries run as seperate queries (not subqueries as previously suggested), exposing extrapolated fuax-columns from one query to the other gets rather tricky. I'm sure there's non-documented solution in the API docs but I've never come across it. You could try putting your with and withCount in the orderBy:
Serial::orderBy(function($query) { some combo of with and withCount })
But that too will get tricky. Since either approach will hit the database multiple times, it would be just as performant to do the separation yourself and keep your sanity at the same time. This first query uses a left join, raw group by and raw select because I don't want laravel running the with query as a separate query (the problem in the first place).
$seriesWithViewCounts = VideoView::leftJoin('episodes', 'episodes.id', '=', 'video_views.episode_id')
->groupBy(DB::raw('episodes.series_id'))
->selectRaw("episodes.series_id, count(video_views.id) as views")
->get();
$series = Series::findMany($seriesWithViewCounts->pluck('series_id'));
foreach($series as $s) {
$s->view_count = $seriesWithViewCounts->first(function($value, $key) use ($s) {
return $value->series_id = $s->id
})->video_views_count;
});
$sortedSeries = $series->sortBy('video_views_count');
This will ignore any series that has no views for all episodes, so you may want to grab those and append it to the end. Not my definition of "popular".
I'd love to see a more eloquent way of handling this, but this would do the job.

Using group by and count with laravel eloquent

I have a Forum and Forum Response Model with following database tables:
forum.id
forum_response.id
forum_response.forum_id
forum_response.user_id
forum_response.text
The Forum Model relationship is:
public function responses()
{
return $this->belongsToMany(ForumResponse::class, 'forum__responses');
}
and the Forum Response relationship:
public function Forum()
{
return $this->belongsTo(Forum::class);
}
I would like to get the number of unique responses for a specific Forum, grouped by the user_id. I have tried the following return $this->hasMany(ForumResponse::class)->groupBy('user_id')->count(); but this is returning a higher value than I'm expecting.
Even though, I feel you have something wrong in your structure. But for now, you have an error in your relationships. Just change belongsToMany to hasMany
change this
public function responses()
{
return $this->belongsToMany(ForumResponse::class, 'forum__responses');
}
to this
public function responses()
{
return $this->hasMany(ForumResponse::class, 'forum__responses');
}

Laravel belongsTo returning null when using 'with'

I'm just getting started with Laravel so please forgive any noobness.
I have a User and Order model, a user has many orders:
# Inside User model
public function orders()
{
$this->hasMany('Order');
}
# Inside Order
public function user()
{
return $this->belongsTo('User');
}
// Not sure if this is upsetting anything (also in Order)
public function products()
{
return $this->belongsToMany('Product');
}
So I think I have the above right.
But when I do this:
$users = User::with('orders')->find(1);
return $users;
I get Call to a member function addEagerConstraints() on null.
However, if I do it the other way around, it works great:
$orders = Order::with('User')->get();
return $orders;
What am I doing wrong / what don't I understand?! Or is my problem bigger than I think?
Database:
The problem is you don't have return for your orders relationship. It should be:
public function orders(){
return $this->hasMany('Order');
}
You should also use your relationships case sensitive. you showed:
$orders = Order::with('User')->get();
is working, but you should rather use
$orders = Order::with('user')->get();
to avoid extra queries to your database in future
For anyone else that runs across this, I was having the same issue, but my problem was that I had the foreign/local keys swapped. Example:
// This is correct for hasX relationships
public function user() {
return $this->hasOne('App\Models\User', 'user_id', 'local_key_user_id');
}
// This is correct for belongsTo relationships
public function user() {
return $this->belongsTo('App\Models\User', 'local_key_user_id', 'user_id');
}
Notice that for hasX relationships, the foreign key is the second parameter, and the local key is the third. However, for belongsTo relationships, these two are swapped.
Probably doesn't answer this particular question but it relates to the title. I had the same issue here is the wrong query
$offer = Offer::with([
'images:name,id,offer_id',
'offer_options:offer_option,value,id,offer_id',
'user:id,name,avatar'])
->select(['id', 'views', 'type', 'status'])
->where('id', $id)->get();
the model look like this
class Offer extends Model {
function user(): BelongsTo {
return $this->belongsTo(User::class);
}
}
The User
class User extends ..... {
function offer(): HasMany {
return $this->hasMany(Offer::class);
}
}
The issue with the query is I was not selecting user_id, i.e in my select function user_id column was not included and that is why I was getting null for user
according to Laravel docs
When using this feature, you should always include the id column and
any relevant foreign key columns in the list of columns you wish to
retrieve.
So the correct query is
$offer = Offer::with([
'images:name,id,offer_id',
'offer_options:offer_option,value,id,offer_id',
'user:id,name,avatar'])
->select(['id', 'views', 'type', 'status','user_id'])
->where('id', $id)->get();

How to set Eloquent relationship belongsTo THROUGH another model in Laravel?

I have a model Listing that inherits through its belongsTo('Model') relationship should inherently belong to the Manufacturer that its corresponding Model belongs to.
Here's from my Listing model:
public function model()
{
return $this->belongsTo('Model', 'model_id');
}
public function manufacturer()
{
return $this->belongsTo('Manufacturer', 'models.manufacturer_id');
/*
$manufacturer_id = $this->model->manufacturer_id;
return Manufacturer::find($manufacturer_id)->name;*/
}
and my Manufacturer model:
public function listings()
{
return $this->hasManyThrough('Listing', 'Model', 'manufacturer_id', 'model_id');
}
public function models()
{
return $this->hasMany('Model', 'manufacturer_id');
}
I am able to echo $listing->model->name in a view, but not $listing->manufacturer->name. That throws an error. I tried the commented out 2 lines in the Listing model just to get the effect so then I could echo $listing->manufacturer() and that would work, but that doesn't properly establish their relationship. How do I do this? Thanks.
Revised Listing model (thanks to answerer):
public function model()
{
return $this->belongsTo('Model', 'model_id');
}
public function manufacturer()
{
return $this->belongsTo('Model', 'model_id')
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id');
}
I found a solution, but it's not extremely straight forward. I've posted it below, but I posted what I think is the better solution first.
You shouldn't be able to access manufacturer directly from the listing, since manufacturer applies to the Model only. Though you can eager-load the manufacturer relationships from the listing object, see below.
class Listing extends Eloquent
{
public function model()
{
return $this->belongsTo('Model', 'model_id');
}
}
class Model extends Eloquent
{
public function manufacturer()
{
return $this->belongsTo('manufacturer');
}
}
class Manufacturer extends Eloquent
{
}
$listings = Listing::with('model.manufacturer')->all();
foreach($listings as $listing) {
echo $listing->model->name . ' by ' . $listing->model->manufacturer->name;
}
It took a bit of finagling, to get your requested solution working. The solution looks like this:
public function manufacturer()
{
$instance = new Manufacturer();
$instance->setTable('models');
$query = $instance->newQuery();
return (new BelongsTo($query, $this, 'model_id', $instance->getKeyName(), 'manufacturer'))
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id')
->select(DB::raw('manufacturers.*'));
}
I started off by working with the query and building the response from that. The query I was looking to create was something along the lines of:
SELECT * FROM manufacturers ma
JOIN models m on m.manufacturer_id = ma.id
WHERE m.id in (?)
The query that would be normally created by doing return $this->belongsTo('Manufacturer');
select * from `manufacturers` where `manufacturers`.`id` in (?)
The ? would be replaced by the value of manufacturer_id columns from the listings table. This column doesn't exist, so a single 0 would be inserted and you'd never return a manufacturer.
In the query I wanted to recreate I was constraining by models.id. I could easily access that value in my relationship by defining the foreign key. So the relationship became
return $this->belongsTo('Manufacturer', 'model_id');
This produces the same query as it did before, but populates the ? with the model_ids. So this returns results, but generally incorrect results. Then I aimed to change the base table that I was selecting from. This value is derived from the model, so I changed the passed in model to Model.
return $this->belongsTo('Model', 'model_id');
We've now mimic the model relationship, so that's great I hadn't really got anywhere. But at least now, I could make the join to the manufacturers table. So again I updated the relationship:
return $this->belongsTo('Model', 'model_id')
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id');
This got us one step closer, generating the following query:
select * from `models`
inner join `manufacturers` on `manufacturers`.`id` = `models`.`manufacturer_id`
where `models`.`id` in (?)
From here, I wanted to limit the columns I was querying for to just the manufacturer columns, to do this I added the select specification. This brought the relationship to:
return $this->belongsTo('Model', 'model_id')
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id')
->select(DB::raw('manufacturers.*'));
And got the query to
select manufacturers.* from `models`
inner join `manufacturers` on `manufacturers`.`id` = `models`.`manufacturer_id`
where `models`.`id` in (?)
Now we have a 100% valid query, but the objects being returned from the relationship are of type Model not Manufacturer. And that's where the last bit of trickery came in. I needed to return a Manufacturer, but wanted it to constrain by themodelstable in the where clause. I created a new instance of Manufacturer and set the table tomodels` and manually create the relationship.
It is important to note, that saving will not work.
$listing = Listing::find(1);
$listing->manufacturer()->associate(Manufacturer::create([]));
$listing->save();
This will create a new Manufacturer and then update listings.model_id to the new manufacturer's id.
I guess that this could help, it helped me:
class Car extends Model
{
public function mechanical()
{
return $this->belongsTo(Mechanical::class);
}
}
class CarPiece extends Model
{
public function car()
{
return $this->belongsTo(Car::class);
}
public function mechanical()
{
return $this->car->mechanical();
}
}
At least, it was this need that made me think of the existence of a belongsToThrough
You can do something like this (Student Group -> Users -> Poll results):
// poll result
public function studentGroup(): HasOneDeep
{
return $this->hasOneDeepFromRelations($this->user(), (new User())->studentGroup());
}

Laravel 4 Unable to Output Data for Complex Model Structure

First post so here goes:
I'm building a stats website with Laravel 4 and have set up relationships between several models -
Player, Game, Team, PlayerData, StatType
All of these have corresponding tables:
players: id, name, team_id
games: id, home_team_id, away_team_id, week (note 2 teams in a single game)
teams: id, name
stat_types: id, name
player_datas: id, player_id, stat_type_id, stat_value, game_id
The idea being that every player plays for a team, who plays once a week, and each stat for a player in each game will have an entry in the player datas table (e.g. player 1, stat_id 1, value 2, game 1, player 1, stat_id 2, value 10, game 1)
So what I'm looking to do is output a table when someone wants to view a player on the player show.blade.php (* represents placeholder):
****UPDATE: I've got the data i want to appear by making Watcher's suggested changes, but getting the 2nd and 3rd cells like below (in my view) seems inefficient? Think I'm missing something
#foreach($team_fixtures as $team_fixture)
<tr>
<td>{{$team_fixture->homeTeam->team_name}} vs {{$team_fixture->awayTeam->team_name}}</td>
<td>{{$team_fixture->playerData()->where('player_id', $player->id)
->where('stat_type_id', '1')
->pluck('stat_value')}}</td>
<td>{{$team_fixture->playerData()->where('player_id', $player->id)
->where('stat_type_id', '2')
->pluck('stat_value')}}</td>
</tr>
#endforeach
I can't tell whether I'm missing a pivot table (player_data_stat_types??) or have got the relationships wrong? If there's a better way to structure I'd be happy to do that, I'm just not sure where to start with this one. I started doing a for each $team_fixtures but could not get the stats to output. My problem is that the fixture is the left hand column, but the player data table has multiple entries against a game_id...
My player controller looks like:
public function show($id)
{
$player = Player::findOrFail($id);
$team_fixtures = Game::where('home_team_id', '=', $player->team_id)
->orWhere('away_team_id', '=', $player->team_id)
->get();
return View::make('site/players.show', compact('player', 'team_fixtures'));
}
And Models are linked as follows:
Team:
public function players() {
return $this->hasMany('Player');
}
public function games() {
return $this->belongsToMany('Game');
}
Player:
public function team() {
return $this->belongsTo('Team');
}
public function playerData(){
return $this->hasMany('PlayerData');
}
Game:
public function playerData() {
return $this->hasMany('PlayerData');
}
public function homeTeam()
{
return $this->hasOne('Team', 'id', 'home_team_id');
}
public function awayTeam()
{
return $this->hasOne('Team', 'id', 'away_team_id');
}
public function playerData(){
return $this->belongsTo('Player', 'player_id', 'id');
}
PlayerData:
public function statType(){
return $this->belongsTo('StatType', 'stat_id', 'id');
}
public function game(){
return $this->belongsTo('Game');
}
StatType:
public function playerData(){
return $this->hasMany('PlayerData');
}
Firs off, your relations are wrong. Here's how they should look like:
// Player
public function team()
{
return $this->belongsTo('Team');
}
public function stats()
{
return $this->belongsToMany('StatType', 'player_datas')
->withPivot('game_id', 'stat_value');
}
// Team
public function players()
{
return $this->hasMany('Player');
}
public function awayGames()
{
return $this->hasMany('Game', 'away_team_id');
}
public function homeGames()
{
return $this->hasMany('Game', 'home_team_id');
}
// Game
public function homeTeam()
{
return $this->belongsTo('Team', 'home_team_id');
}
public function awayTeam()
{
return $this->belongsTo('Team', 'away_team_id');
}
public function players()
{
return $this->belongsToMany('Player', 'player_datas')
->withPivot('stat_value', 'stat_type_id');
}
I don't think you need PlayerData model at all, and I assume your stat_types table has name instead of value field.
Then, to achieve what you wanted, ie. to show user's performance (stats values) for each game, you want something like this:
// controller
$player = Player::find($id);
$stats = $player->stats->groupBy('pivot.game_id');
// view
<ul>
#foreach ($games as $game)
<li>
{{ $game->homeTeam->name }} vs {{ $game->awayTeam->name }}
<table>
#foreach ($stats->get($game->id) as $stat)
<tr><td>{{ $stat->name }}: </td><td>{{ $stat->pivot->stat_value }}</td>
#endforeach
</table>
</li>
#endforeach
</ul>
When you are declaring a relationship in a model, you should specify what you are relating the model to. In your code I see at least one example of this:
// Game model
public function teams() {
return $this->belongsToMany('Game');
}
This should really be relating your Game model to your Team model, but you are relating games to games here. Try this out:
public function teams() {
return $this->belongsToMany('Team');
}
The method names in the relationship simply set up an attribute with which you can use on an instance of your model, it has no other special meaning. This will also work:
public function chicken() {
return $this->belongsToMany('Team');
}
With this, I could access the relationship of an instance by using $game->chicken and be able to iterate over many Team instances.
Another issue is that Eloquent relies on convention when using the other default parameters when declaring relationships. If I have two models I want to relate, say Modela and Modelb, then it assumes the table names will be modela and modelb. Furthermore, the linking column will be assumed to be modelb_id, for instance, inside the modela table.
You can override this behavior (which you will need to do at least for your Game relationships, since you have home_team_id and away_team_id). I refer you to the documentation, but here's an example:
// Game model
public function homeTeam()
{
return $this->hasOne('Team', 'home_team_id');
}
Notice I also changed your belongsToMany to a hasOne, which I think will work better for you and is more in line with what you are trying to accomplish.

Categories