Eloquent relationship structure for league app - php

I've been working on an Angular app with a Laravel rest API and have realised my relationships aren't quite as they should be.
I have 3 entities - seasons, divisions and teams.
Season:
A season has many divisions, and many teams.
Division:
A division belongs to many seasons, and has many teams.
Team:
A team belongs to many divisions, and belongs to many seasons.
This is because a division may not be used for every season, and a team may change divisions each season, or may not play in all seasons.
I'm struggling to understand how to implement the relationship logic.
For example, I'd like to get divisions for a season and the teams present in that division for that particular season, be it the current one or when the user is viewing an old season.
Here's what I've got at the moment:
Season model
class Season extends Model
{
protected $guarded = [];
protected $hidden = ['pivot'];
/**
* Return divisions for this season
*
* #return BelongsToMany
*/
public function divisions()
{
return $this->belongsToMany('App\Division');
}
public function teams()
{
return $this->belongsToMany('App\Team');
}
}
Division Model
class Division extends Model
{
protected $guarded = [];
public function matches()
{
return $this->hasMany('App\Fixture');
}
public function seasons()
{
return $this->belongsToMany('App\Season');
}
public function teams() {
return $this->hasMany('App\Team');
}
}
Team Model
class Team extends Model
{
protected $guarded = [];
protected $hidden = ['pivot'];
public function division() {
return $this->belongsTo('App\Division');
// should be belongsToMany
}
public function seasons()
{
return $this->belongsToMany('App\Season')->select('season_id');
}
}
With the following tables:
seasons
divisions
teams
division_season
season_team
But this doesn't enable me to have a team belonging to a different division per season.
If I change the division() method on the team model to be divisions() and with a belongsToMany() and have a new table - division_team (is this the right approach?) how would I then query all teams by their division on a per season basis?
Eg:
Get all divisions and their teams by season id
Bearing in mind teams have the potential to change divisions each season.
EDIT
As per answer from Thomas Van Der Veen's answer below, I have added a table division_season_team and used the relations in his answer.
Trying to get divisions with their teams based on a season id though is proving difficult - the below returns the correct divisions, but the teams aren't necessarily part of the current season!
DivisionsController
if ($request->query('seasonId')) {
$seasonId = $request->query('seasonId');
return $this->respond(new DivisionCollection(Division::with('teams')->whereHas(
'seasons', function($q) use ($seasonId) {
$q->where('season_id', '=', $seasonId);
})->get()
));
}

By reading your desired outcome a suggestion would be to create a single table that holds al those three relationships.
season_division_team // Or whatever you want to call it.
id
season_id
division_id
team_id
This allows you to easily create and update relations. All models can belong to many others.
Relations you now can have are:
// Season
public function divisions()
{
return $this->belongsToMany(Division::class, 'season_division_team');
}
public function teams()
{
return $this->belongsToMany(Team::class, 'season_division_team');
}
// Division
public function seasons()
{
return $this->belongsToMany(Season::class, 'season_division_team');
}
public function teams()
{
return $this->belongsToMany(Team::class, 'season_division_team');
}
// Team
public function seasons()
{
return $this->belongsToMany(Season::class, 'season_division_team');
}
public function divisions()
{
return $this->belongsToMany(Division::class, 'season_division_team');
}

You just don't use the correct query. Your query should be:
if ($seasonId = $request->input('seasonId')) {
$season = Season::findOrFail($seasonId);
$divisionIdsQuery = $season->divisions()
->select('divisions.id')
->groupBy('divisions.id')
->getQuery(); // can't just ->get(), because MYSQL FULL GROUP BY.
$divisions = Division::whereIn('id', $divisionIdsQuery)
->with(['teams' => function ($query) use ($season) {
$query->where('season_id', $season->id);
}])
->get();
return $this->respond(new DivisionCollection($divisions));
}

After read your edit I think that you can get this with a join inside the teams with.
Tables:
seasons
divisions
teams
season_divisions_teams
- season_id
- division_id
- team_id
In your controller:
if ($request->query('seasonId')) {
$seasonId = $request->query('seasonId');
$result = Division::with(['teams' => function (Builder $query) use ($seasonId) {
$query->select(["teams.*"]);
$query->join('season_divisions_teams', 'season_divisions_teams.team_id', '=', 'teams.id');
$query->where('season_divisions_teams.season_id', '=', $seasonId);
}])
->whereHas('seasons', function(Builder $query) use ($seasonId) {
$query->where('season_id', '=', $seasonId);
})->get();
return $this->respond(new DivisionCollection($result));
}
Explanation:
Like we are calling the relation between divisions and teams when we calling with('teams'), the teams that isn't in that division are excluded. After, we use a join between teams and season_divisions_teams to get the information about what teams are in the seasons, and we use a where to filter them by season id.

Related

Laravel: How to count data in database?

I want to make a list of students to know how many products each student buys. I have two tables : Member and Orders. In Orders table have column member_id and product_id. I want to count how many products each student buys. I can get list of student but i can't count how many products each student buys.
public function index()
{
$students = Member::getStudents();
$order = Order::where('member_id', $students->id)->count();
return view('admin.student.index', compact('students'));
}
But it appears an error:
Property [id] does not exist on this collection instance.
function getStudents()
public static function getStudents()
{
$members = Member::where('member_type_id', BaseModel::$student)->get();
for ($idxMember = 0; $idxMember < count($members); $idxMember++) {
if ( $members[$idxMember]->user_id ) {
$members[$idxMember]->username = User::find($members[$idxMember]->user_id)->username;
}
}
return $members;
}
I think it is because you get a collection on id.
so you should foreach your collection and get specific ids.
public function index() {
$students = Member::getStudents();
foreach($students as $student ) {
$order = Order::where('member_id', $student->id)->count();
}
return view('admin.student.index', compact('students'));
}
I suggest to use relationships in Database. It would be more easy and simple.
In orders table, there is a column called member_id which reference to id column in members table. (Keep attention to singular plural usage)
Refer Laravel Documentation for foreign key design and implementation.
You need 2 models, Member and Order. You should define the relationship as below.
Since this is One To Many Relationship,
In Member Model,
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Member extends Model
{
/**
* Get the orders for the member.
*/
public function orders()
{
return $this->hasMany('App\Order');
}
}
?>
In Order Model,
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
/**
* Get the member that owns the order.
*/
public function member()
{
return $this->belongsTo('App\Member');
}
}
?>
Now you can get order count per user using this code.
Member::find(MEMBER_ID)->orders->count();
This will return order count of the selected member.
Please refer the Laravel Documentation here.
Relationships | One To Many | One To Many (Inverse)
You can achieve this by simply using laravel relationship
In order to do this please follow below steps
First, create two relation in members model
public function orders()
{
return $this->hasMany('App\Models\Order', 'member_id', 'id);
}
Then retrieve the students details with orders
public function getStudents()
{
return Member::where('member_type_id', BaseModel::$student)->withCount('orders)->get();
}
Hope this helps
Thank you.
Sharing solution with explanation.
Objects are
Member (Student is also a member. But the Type is different)
Order (One order can have multiple products)
Product (Product will allocate with Orders and Members can place Orders)
Now If you are using Laravel. So the best way is to use Eloquent Relationships.
Model Member :
1 Define Query Scope
public function scopeStudents($query)
{
return $query->where('member_type_id', 1);
}
2 Define One to Many relation between Order and Member (One Student can have multiple orders. And I hope that other members can also have orders. So i am going to relate direct Member to Order)
public function orders()
{
return $this->hasMany('App\Order');
}
and
public function products()
{
return $this->hasManyThrough('App\Product', 'App\Order');
}
Model Order :
1 Define relation with Member Model for One to Many
public function member()
{
return $this->belongsTo('App\Member');
}
2 Define one to many between Order and Product
public function products()
{
return $this->hasMany('App\Product');
}
Model Product :
1 Define Order Product Relation
public function order()
{
return $this->belongsTo('App\Order');
}
Now the Model work is done. Only data fetching is remaining. Tell me in comments if you think below code is less and useful and easy to understand.
Controller Member :
1 Index Function :
// this will fetch all students with their orders and products
$data = Member::students()->with('orders', 'products')->get();
// this will fetch all students with their orders and order's products
$data = Member::students()->with('orders.products')->get();
// this will fetch all students with their orders count and products count
$data = Member::students()->withCount('orders', 'products')->get();

Laravel whereHas count on Many-to-Many relationship

In my current project, a User may join many Organisations and vice versa - An example of a many to many relationship. I'm trying to count the number of users who are currently unverified (where the Verified column on the user table is equal to 0).
My User model:
/**
* Get the organisations that the user is a part of.
*/
public function organisation()
{
return $this->belongsToMany(
Organisation::class, 'organisation_users', 'user_id', 'organisation_id'
)->withPivot(['role'])->orderBy('name', 'asc');
}
My Organisation model:
/**
* Get all of the users that belong to the organisation.
*/
public function users()
{
return $this->belongsToMany(
User::class, 'organisation_users', 'organisation_id', 'user_id'
)->withPivot('role');
}
So if I want to count the number of unverified users I have the following method on the Organisation model:
/**
* An organisation may have unverified users attached.
*/
public function unverifiedUsers()
{
return $this->whereHas('users', function($query) {
$query->where('verified', 0);
})->get();
}
However, running dd(\App\Organisation::find($org->id)->unverifiedUsers()->count()); only shows 1 when in fact there should be 10. Am I structuring my relationships incorrectly?
whereHas() will return 0 or 1. It just tells you if such a user exists.
The sollution is much simpler:
public function unverifiedUsers()
{
return $this->users()->where('verified', 0)->get();
}
If you only need the count:
public function unverifiedUsersCount()
{
return $this->users()->where('verified', 0)->count();
}

Laravel 4 Many to Many triangle with Eloquent, retrieve "suggestions"-like data

This might be very simple, but I can't find the solution... Any help will be appreciated.
I have 3 tables: users, skills and projects, all connected to each other with Many to Many relationship.
users <-- MtM --> skills <-- MtM --> projects <-- MtM --> users
User model
public function skills()
{
return $this->belongsToMany('Skill')->withPivot('level');
}
public function projects(){
return $this->belongsToMany('Project')->withPivot('user_id', 'project_id');
}
Skill model
public function users() {
return $this->belongsToMany('User')->withPivot('level');
}
public function projects() {
return $this->belongsToMany('Project')->withPivot('level');
}
Project model
public function users(){
return $this->belongsToMany('User')->withPivot('user_id', 'project_id');
}
public function skills(){
return $this->belongsToMany('Skill')->withPivot('level');
}
So, a Many to Many triangle. I want to get all the projects, the skills of which are subset of specific users skills. So basically "suggested projects" for user.
$projects = Project::whereHas('skills', function($q) use($array)
{
$q->thatAreSubsetOf($array);
//$array contains names or ids of users skills
})->orderBy('created_at', 'DESC')->get();
If some $user->skills are 'PHP', 'C' and 'Javascript', all the returned projects skills should only be subset of those (e.g. {'PHP', 'C'} or {'PHP', 'Javascript'} or {'Javascript', 'C'} or {'PHP', 'C', 'Javascript'} or {'PHP'}, or {'Javascript'} or {'C'})
I've tried many thing, researched a lot in Laravel Docs/here but can't seem to get what I want..
Basically your requirement can achieve Many To Many Polymorphic Relations. Your requirement is to use skills with User and Project, so its would be easy to handle with MTMPR.
Example below shows how I have implemented sharing Skill with both Project and User.
DB Schema
Skill
id integer
name string
skillable
id integer
skillable_id integer
skillable_type string
Models
Skill
class Skill extends Eloquent{
public function projects(){
return $this->morphedByMany('Project', 'skillable');
}
public function users(){
return $this->morphedByMany('User', 'skillable');
}
}
Project
class Project extends Eloquent{
public function skills(){
return $this->morphToMany('Skill', 'skillable');
}
}
User
class User extends Eloquent{
public function skills(){
return $this->morphToMany('Skill', 'skillable');
}
}
How to get skill from project
$project = Project::find($id);
$projectsSkill = $project->skills()->all();
And how you can get Project from skills
$skill = Skill::find(1);
$projects = $skill->projects();
If you still find something unclear to handle these situation, let me know.

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