Applying hasManyThrough to deeper relationships - php

The Laravel docs seem to indicate that the hasManyThrough declaration can only be used for relationships that are two levels "deep". What about more complex relationships? For example, a User has many Subjects, each of which has many Decks, each of which has many Cards. It's simple to get all Decks belonging to a User using the hasManyThrough declaration, but what about all Cards belonging to a User?

I created a HasManyThrough relationship with unlimited levels: Repository on GitHub
After the installation, you can use it like this:
class User extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function cards() {
return $this->hasManyDeep(Card::class, [Subject::class, Deck::class]);
}
}

As stated in the comments, hasManyThrough doesn't support this level of specificity. One of the things you can do is return a query builder instance going the opposite direction:
//App\User;
public function cards()
{
Card::whereHas('decks', function($q){
return $q->whereHas('subjects', function($q){
return $q->where('user_id', $this->id);
});
});
}
We're going from Cards -> Decks -> Subjects. The subjects should have a user_id column that we can then latch onto.
When called from the user model, it would be done thussly:
$user->cards()->get();

Well, actually the best solution will be put the extra column to Card table - user_id, if you have so frequent needs to get all cards for the user.
Laravel provides Has-Many-Through relations for 2-depth relation because this is very widely often used relation.
For the relations Laravel does not support, you need to figure out the best table relationship yourself.
Any way, for your purpose, you can use following code snap to grab all cards for the user, with your current relation model.
Assumption
User has hasManyThough relationship to Deck,
So Project model will have following code:
public function decks()
{
return $this->hasManyThrough('Deck', 'Subject');
}
Deck has hasMany relationship to Card
Code
$deck_with_cards = $user->decks()->with("cards")->get();
$cards = [];
foreach($deck_with_cards AS $deck) {
foreach ($deck->cards as $c) {
$cards[] = $c->toArray();
}
}
Now $cards has all cards for the $user.

Related

Laravel polymorphic relationship with pivot

Assume I have posts and videos that can be seen by multiple users.
- users
- id
- posts
- id
- videos
- id
- user_accessables (pivot)
- id
- user_id
- accessable_id
- accessable_type
In an example like that, I have set my User relationship like so but something feels wrong
class User extends Model {
public function posts() {
return $this->morphedByMany(
Post::class,
'accessable',
'user_accessables'
);
}
public function videos() {
return $this->morphedByMany(
Video::class,
'accessable',
'user_accessables'
);
}
public function allowedEntities() {
return ($this->posts)->merge($this->videos);
}
}
With the allowedEntities() I can get a collection of both models joined together.
However, I think the use of polymorphic relationship is returning a collection of entities through relationship rather than needing a combiner relationship, right?
I am having problems with understanding polymorphic with pivot table (the tag example in documentation doesn't seem like same scenario).
Because now I can't do:
$collection = collect(); // multiple models of Video & Post
$user->allowedEntities()->sync($collection);
As #Jonas Staudenmeir said is not possible to have a relationship that returns all related model, BUT you can define a method on the model that returns a query builder object with all entities you need (search with on the docs).

Schema.org + Laravel = way too complicated?

I recently completed my first Laravel project (I really liked the look of the framework and wanted to give it a try) and was reflecting on what I think I did well, what I did poorly, and what I want to do differently next time.
One of the biggest issues I noticed I had was that in a gross effort to avoid duplication of data I had some pretty hideous work to do in order to extract meaningful information from my database.
At the outset of the project I used schema.org as a rough basis for designing my models. For instance, when I wanted to store Employees and Customers:
Create a Person model
A Person hasMany Roles
A Customer is a (morphs) Role
An Employee is a (morphs) Role
An Organization hasMany Employees
An Organization hasMany Customers
At first this seems nice, because a person can be both an employee and a customer (you can shop at WalMart and work for WalMart). Also since a person can have many roles, they can be multiple customers and multiple employees -- if they shop at more than one store or if they have two jobs.
However I ran into issues when I wanted to do something like "find all employees for organization X whose name is John":
$employees = $organization->employees;
$employeesNamedJohn = new Collection();
foreach( $employees as $employee ) {
if( $employee->role->person->name == 'John' )
$employeesNamedJohn->add( $employee );
}
This seems awfully complicated (not to mention inefficient), especially considering if I hadn't invented the messed up schema it would just be a one-liner:
$employees = $organization->employees()->where('name', 'John')->get();
So am I just doing something wrong? Does Laravel have a simple way to handle complex relationships like this, or is the answer to simply never let your relationships get this complicated?
Adding query scopes on your models could really help:
class Role extends Model
{
public function scopeNamed($query, $name)
{
$query->whereHas('person', function ($query) use ($name) {
$query->where('name', $name);
});
}
}
class Employee extends Model
{
public function scopeNamed($query, $name)
{
$query->whereHas('role', function ($query) use ($name) {
$query->named($name);
});
}
}
Then you could do this:
$employees = $organization->employees()->named('John')->get();
Much better.
As an extra,
I did implement Josheph Silber his answer in
https://github.com/noud/seo
Look at the Entity-Relationship Diagram below in the readme.md
Entity role having fields
roleable_id (the id to where the foreign key points)
roleable_type (the entity the foreign key points to)
In Laravel this is
Role model
trait for Employee, Customer, ...
so you can fetch Employees named $name

Laravel : Sum with related models

I'm a trying to get the total distance of an activity which has multiple steps (with distance) ? For now, I'm doing like this :
Controller :
$total_distance = 0;
foreach (\Auth::user()->activities as $key => $activity) {
$total_distance += $activity->getTotalDistance();
}
Activity Model :
public function steps()
{
return $this->hasMany('App\Step');
}
public function getTotalDistance()
{
return $this->steps->sum('distance');
}
Is there a proper solution to do that ?
Thanks for your help
The way you do it requires quite a lot of DB queries to be run:
1 to fetch user activities
1 per activity to fetch its steps
You can get the number you need with just one query using Eloquent's aggregate function sum():
$total_distance = Step::join('activities', 'activity_id', '=', 'activities.id')->where('activities.user_id', Auth::id())->sum('distance');
You can read about other aggregate methods that Eloquent offers here: http://laravel.com/docs/5.1/queries#aggregates
Just like #Jeemusu said, your approach seems fine. But if you want to do it in some other way you can try to do it in several ways.
Look at Laravel's Has Many Through relationship.
The "has many through" relation provides a convenient short-cut for accessing distant relations via an intermediate relation. For example, a Country model might have many Post through a User model.
So using has many through you can access steps directly something like
$user->steps->sum('distance');
You need to make relationship that user has many steps through activities.
Something like:
class User extends Eloquent {
public function steps() {
$this->hasManyThrough('Steps','Activity','user_id','activity_id');
}
}
The other solution is to put foreign key in steps table so you can directly access it from user model like:
$user->steps->sum('distance');
class User extends Eloquent {
public function steps() {
$this->HasMany('Steps','user_id');
}
}
Hope this can help you.

Laravel: How do I create a custom pivot model?

I'm working on a project that has a many to many relationship between User and Club. This relationship works and I can get the respective objects with: $user->clubs. The pivot table I've named memberships. I can get the pivot data with $club->pivot. Foreign keys are defined for the memberships table in migrations.
However, I'd like the pivot table to be represented by a model so that I can easily update attributes of Membership such as role (Or even add a Role model to Membership!) or status.
I've looked at "Defining A Custom Pivot Model" in the docs, but what it says doesn't work for me, I get:
ErrorException
Argument 1 passed to Illuminate\Database\Eloquent\Model::__construct() must be of the type array, object given
I've also looked at this description of how to do it, but it's more or less the same as above.
Membership model:
class Membership extends Eloquent {
protected $table = 'memberships';
public function user()
{
return $this->belongsTo('User');
}
public function club()
{
return $this->belongsTo('Club');
}
}
Has anyone done this before?
This has been solved by a suggestion by a reddit user to extend Pivot in my Membership model. I also had to add ->withPivot('status', 'role')->withTimestamps() to the relationship declarations in the User and Club models.
You can do this by adding some manual code.
Your Membership model looks ok. You can get all clubs of a $user easily if you define method $user->clubs() where you get all the clubs manually.
clubs() {
$memberships=$this->memberships;
$clubs=new \Illuminate\Database\Eloquent\Collection();
foreach($memberships as $m) {
$clubs=$clubs->merge($m->clubs);
}
return $clubs;
}

Laravel, How to use where conditions for relation's column?

I'm using Laravel and having a small problem with Eloquent ORM.. I can get this working simply with SQL query using a JOIN but I can't seem to get it working with Eloquent!
This is what I want, I have two tabels. one is 'Restaurants' and other is 'Restaurant_Facilities'.
The tables are simple.. and One-To-One relations. like there is a restaurant table with id, name, slug, etc and another table called restaurant_facilities with id, restaurant_id, wifi, parking, etc
Now what I want to do is.. load all restaurants which have wifi = 1 or wifi = 0..
How can i do that with Eloquent ? I have tried eager loading, pivot tables, with(), collections() and nothing seems to work!
The same problem I have for a Many-To-Many relation for cuisines!
I have the same restaurant table and a cuisine table and a restaurant_cuisine_connection table..
but how do I load all restaurants inside a specific cuisine using it's ID ?
This works.
Cuisine::find(6)->restaurants()->get();
but I wanna load this from Restaurant:: model not from cuisines.. because I have many conditions chained together.. its for a search and filtering / browse page.
Any ideas or ways ? I've been struggling with this for 3 days and still no answer.
Example Models :
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facilities() {
return $this->hasOne('Facilities');
}
}
class Facilities extends Eloquent {
protected $table = 'restaurants_facilities';
public function restaurant() {
return $this->belongsTo('Restaurant');
}
}
PS :
This seems to be working.. but this is not Eloquent way right ?
Restaurant::leftJoin(
'cuisine_restaurant',
'cuisine_restaurant.restaurant_id',
'=', 'restaurants.id'
)
->where('cuisine_id', 16)
->get();
Also what is the best method to find a count of restaurants which have specific column value without another query ? like.. i have to find the total of restaurants which have parking = 1 and wifi = 1 ?
Please help on this.
Thank you.
I don't see anything wrong with doing the left join here, if you have to load from the Restaurant model. I might abstract it away to a method on my Restaurant model, like so:
class Restaurant extends Eloquent {
protected $table = 'restaurants'; // will be default in latest L4 beta
public function facility()
{
return $this->hasOne('Facility');
}
// Or, better, make public, and inject instance to controller.
public static function withWifi()
{
return static::leftJoin(
'restaurant_facilities',
'restaurants.id', '=', 'restaurant_facilities.restaurant_id'
)->where('wifi', '=', 1);
}
}
And then, from your routes:
Route::get('/', function()
{
return Restaurant::withWifi()->get();
});
On the go - haven't tested that code, but I think it should work. You could instead use eager loading with a constraint, but that will only specify whether the facility object is null or not. It would still return all restaurants, unless you specify a where clause.
(P.S. I'd stick with the singular form of Facility. Notice how hasOne('Facilities') doesn't read correctly?)
I stumbled across this post while trying to improve my REST API methodology when building a new sharing paradigm. You want to use Eager Loading Constraints. Let's say you have an api route where your loading a shared item and it's collection of subitems such as this:
/api/shared/{share_id}/subitem/{subitem_id}
When hitting this route with a GET request, you want to load that specific subitem. Granted you could just load that model by that id, but what if we need to validate if the user has access to that shared item in the first place? One answer recommended loading the inversed relationship, but this could lead to a confusing and muddled controller very quickly. Using constraints on the eager load is a more 'eloquent' approach. So we'd load it like this:
$shared = Shared::where('id', $share_id)
->with([ 'subitems' => function($query) use ($subitem_id) {
$query->where('subitem_id', $subitem_id)
}]);
So where only want the subitem that has that id. Now we can check if it was found or not by doing something like this:
if ($shared->subitems->isEmpty())
Since subitems is a collection (array of subitems) we return the subitem[0] with this:
return $shared->subitems[0];
Use whereHas to filter by any relationship. It won't join the relation but it will filter the current model by a related property. Also look into local scopes to help with situations like this https://laravel.com/docs/5.3/eloquent#local-scopes
Your example would be:
Restaurant::whereHas('facilities', function($query) {
return $query->where('wifi', true);
})->get();
Restaurant::whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
})->get();
To achieve the same thing with local scopes:
class Restaurant extends Eloquent
{
// Relations here
public function scopeHasWifi($query)
{
return $query->whereHas('facilities', function($query) {
return $query->where('wifi', true);
});
}
public function scopeHasCuisine($query, $cuisineId)
{
return $query->whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
});
}
}
For local scopes you DO NOT want to define them as static methods on your model as this creates a new instance of the query builder and would prevent you from chaining the methods. Using a local scope will injects and returns the current instance of the query builder so you can chain as many scopes as you want like:
Restaurant::hasWifi()->hasCuisine(6)->get();
Local Scopes are defined with the prefix scope in the method name and called without scope in the method name as in the example abover.
Another solution starring whereHas() function:
$with_wifi = function ($query) {
$query->where('wifi', 1);
};
Facilities::whereHas('restaurant', $with_wifi)
Nice and tidy.
Do you absolutely have to load it from the Restaurant model? In order to solve the problem, I usually approach it inversely.
Facilities::with('restaurant')->where('wifi' ,'=', 0)->get();
This will get all the restaurant facilities that match your conditions, and eager load the restaurant.
You can chain more conditions and count the total like this..
Facilities::with('restaurant')
->where('wifi' ,'=', 1)
->where('parking','=', 1)
->count();
This will work with cuisine as well
Cuisine::with('restaurant')->where('id','=',1)->get();
This grabs the cuisine object with the id of 1 eager loaded with all the restaurants that have this cuisine

Categories