Verify belongsToMany for update function in controller laravel 5.4 - laravel

I'm creating this sort of database with some movies information.
The logic structure is pretty simple one movie has many actors and one actor have done many movies so it's a many to many relationship.
I'm also using select2 library to have a simple input where write actors like tags, separating them with commas or spacebar (here the link to docs) and below a simple snapshot to better understand the result
In the create/store function I don't need to check if any relation exists yet because the movie it's new. So I just need to check if actors already exists in the database, if not save them.
My controller store() function looks like:
foreach ($request->input('actors') as $key => $new_actor) {
// if actors doesn't exist in the db it save it.
$check = Actor::where('name', '=', $new_actor)->count();
if ($check === 0) {
$actor = new Actor;
$actor->name = $new_actor;
$actor->save();
}
// Here it creates the relationship
$actor = Actor::where('name', '=', $new_actor)->first();
$film->actors()->attach($actor->id);
}
The problem
when I edit a movie changing the actors, for example deleting or adding a new actor. I need to check in the controller if there are new relationship or if I have to delete some. How can I do it?
This is my controller update() function and of course doesn't work at all
foreach ($request->input('actors') as $key => $new_actor) {
// if actors doesn't exist in the db it save it.
$check = Actor::where('name', '=', $new_actor)->count();
if ($check === 0) {
$actor = new Actor;
$actor->name = $new_actor;
$actor->save();
}
$actors = $film->actors()->get();
foreach ($actors as $key => $actor) {
if ($actor->name === $new_actor) {
$actor = Actor::where('name', '=', $new_actor)->first();
$film->actors()->attach($actor->id);
}
}
}
Thanks for your help.

There is not direct way to do this with using the eloquent it. But you can do with the using db facade like this
$actors_film_relation = \DB::table('actor_film')->pluck('actor_id')->unique();
By using this now you can get the list of the actors that are attached with atleast one film.
Before deleting user you can check that actor_id is not inside the $actor_film_relation list like this
if( !in_array( $id, $actors_film_realtion)) { $actor->delete(); }
So now, the actor that is related to atleast one film will be not deleted.

Ok I found my way. With the first part checks if there are new actors and if true, it creates a new row in the actors table. With the second part, check if there are changes from the input comparing the new array with the relationship saved in the db, so it can detect if some actors were deleted and then deletes the relationship too.
foreach ($request->input('actors') as $key => $new_actor) {
// if actors doesn't exist in the db it save it.
$check = Actor::where('name', '=', $new_actor)->count();
if ($check === 0) {
$actor = new Actor;
$actor->name = $new_actor;
$actor->save();
}
// If the relation doesn't exists it creates it.
if (!$film->hasActor($new_actor)) {
$actor = Actor::where('name', '=', $new_actor)->first();
$film->actors()->attach($actor->id);
}
// If the relation has been deleted, it deletes it.
$actors = $film->actors()->get();
$new_actors = $request->input('actors');
foreach ($actors as $key => $actor) {
$check = in_array($actor->name, $new_actors);
if ($check === false) {
$film->actors()->detach($actor->id);
}
}
}

Related

Laravel many to many relationship making a list with pivot table for order

I have made a list of all items using many to many relationship.
With a pivot I make sure that there is an order.
Now I am trying to remove 1 item from the list.
And the order numbers must also move, this does not work.
From the view I provide the $id of the item and the order number.
public function removefromlist($id, $nummer)
{
$user = User::find(auth()->user()->id);
$user->manytomany()->detach($id);
$nummerlist = $user->manytomany()->count();
for($i = $nummer + 1;$i <= $nummerlist;$i++){
$testt = $user->manytomany()->where('nummer', $i);
$user->manytomany()->updateExistingPivot($testt->first()->item_id , ['nummer' => $i -1]);
}
return view('welcome')->with('itemslist', $user->manytomany);
}
How can I ensure that when I delete an item, the other move up?
You can try the following
public function removefromlist($id, $nummer)
{
$user = auth()->user();
$user->manytomany()->detach($id);
$user->manytoMany()
->where('nummer', '>', $nummer)
->get()
->each(function($item) use($user) {
$user->manytomany()
->updateExistingPivot(
$item->id,
['nummer' => $item->pivot->nummer - 1]
);
});
return view('welcome')->with('itemslist', $user->manytomany);
}

Laravel, Eloquent, getting related items from a morph relationship (advice)

The title of the question may seem weird, but I'll explain it briefly. I'm not experiencing any issues yet, but considering any advice regarding the following approach.
I have the following tables:
articles
hasMany(tags)
movies
hasMany(tags)
series
hasMany(tags)
subs (morph)
media_id
media_type
user_id
All of the first three tables have many tags, where the subs table have a Morph relation between them all.
The concept is simple, the user can subscribe to movies and/or series and NOT articles, let's say, opt-in a record from movies.
The following record will be inserting:
media_id => 1, media_type => movie, user_id => 1
Let's say we have 10K records like this in subs table. Now, I create a record in articles with specific tags. I want to notify all users in subs, who are opted-in movies and/or series that have exactly the same tags as the record I'm targeting from articles.
TL;DR: Notifying users that are interested in specific tags from movies or series.
To achieve this, I added the morphTo in the subs model:
public function media(){
return $this->morphTo();
}
The logic is straightforward:
Get the article.
Get the tags() relationship and save it into a variable.
Check if tags are not empty, if not, continue
Get all the subs.
Get the morphTo relationship and get its tags.
Compare and continue.
$article = MediaArticle::find($notificationable_id);
$article_tags = $article->tags;
$subbed_users = array();
if (empty($article_tags) === false) {
$subs = NotificationMediaSub::all();
foreach ($subs as $sub) {
$sub_tags = $sub->media->tags;
foreach ($sub_tags as $sub_tag) {
foreach ($article_tags as $article_tag_title) {
if (strcasecmp($sub_tag->title, $article_tag_title->title) === 0) {
array_push($subbed_users, $sub->user_id);
}
}
}
}
// continue here
}
I find this approach really intensive and may cause the server to slow down a lot. Is there a better approach to achieve my goal?
=========================
Approach
I defined the media_tags_relationship as a Model, inside it:
public function media() {
if ($this->taggable_type === AdminHelper::MORPH_MEDIA_TRANSLATION_MOVIE['original']) {
return $this->belongsTo(MediaMovie::class, 'taggable_id', 'id');
} elseif ($this->taggable_type === AdminHelper::MORPH_MEDIA_TRANSLATION_SERIES['original']) {
return $this->belongsTo(MediaSeries::class, 'taggable_id', 'id');
}
return $this->belongsTo(MediaMovie::class, 'taggable_id', 'id')->where('id', 0);
}
Now, I'm fetching the subs like this:
$article = MediaArticle::find($notificationable_id);
$article_tags = $article->tags;
$subbed_users_tokens = array();
if (empty($article_tags) === false) {
$article_tags = $article_tags->map(function ($tag) {
return $tag->title;
});
$related_tags = MediaTag::whereIn("title", $article_tags)->get();
$related_tags = $related_tags->map(function ($tag) {
return $tag->id;
});
$tags_relationship = MediaTagsRelationship::whereIn("tag_id", $related_tags)->where("taggable_type", "movie")->orWhere("taggable_type", "series")->get();
foreach ($tags_relationship as $tag_relationship) {
$media = $tag_relationship->media()->with('subs')->get();
if (empty($media) === false) {
$media = $media[0];
$subs = $media->subs->map(function ($sub) {
return $sub->firebase_token;
});
array_push($subbed_users_tokens, $subs);
}
}
}

Eager load for collections on the fly

I have this, user has many to many property via pivot property_users.
I am making somehow reusable classes in my webapp.
These are the models with their eager loading functions:
//User model
public function properties()
{
return $this->belongsToMany(Property::class, 'property_users', 'user_id', 'property_id');
}
//Property model
public function property_users()
{
return $this->hasMany(PropertyUser::class, 'property_id', 'id');
}
//PropertyUser model
public function user()
{
return $this->belongsTo(User::class);
}
//GetProperties class
public function handle()
{
return auth()->user()->properties()->get();
}
//somewhere in a feature
$properties = $this->run(GetProperties::class);
//this returns valid properties under the logged in user
I now need to get the chat_username in property_users that belongs to this user
I manage to make it work if I loop through the properties and then doing it on the fly.
$properties = $properties->map(function($property) {
$propertyUsers = $property->property_users()->get();
$chatUsername = null;
foreach($propertyUsers as $propertyUser) {
if($propertyUser->property_id == $property->id) {
$chatUsername = $propertyUser->chat_username;
}
}
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $chatUsername
];
});
But I am trying to reduce query on loop to reduce hits especially when they are on multiple properties in the database.
The other way is that I can add the property_users in the eager loading under GetProperties class by updating it to:
$query = Property::query();
$query->with(['property_users']);
$query->whereHas('property_users', function($qry) {
$qry->where('user_id', Auth::user()->id);
});
$properties = $query->get();
return $properties;
But I do not want to rely on adding more eager loading to the original GetProperties class as the GetProperties will get fat and I do not really need those data (let's say adding property_invoices, property_schedules, etc but not really needing it in some area).
Rather, I want to do the eager loading on the fly but with a twist! This is how I would imagine it:
Collect all the ids from the properties, do the fetch using wherein and apply all the users to the properties in a single query. This way it will be even more beautiful.
Maybe something like this: (using the original GetProperties class)
$properties = $this->run(GetProperties::class);
//this does not work. The error is: Method Illuminate\Database\Eloquent\Collection::property_users does not exist.
$property->property_users = $properties->property_users()->get();
Would be great if someone can show me how to do it.
What about eager loading only the fields you actually need?
$query->with('property_users:id,user_id');
The model will not get fat, and you will not need to do separate queries in the loop.
It is documented in the official documentation: https://laravel.com/docs/5.8/eloquent-relationships#eager-loading , see Eager Loading Specific Columns
Edit: if you want to perform the query after the GetProperties class, you will need to collect all the ids and perform a second query. I honestly don't like this second approach, because it is far slower, less performant and I consider it less elegant then adding a single line in the GetProperties class, but it is gonna work:
$properties = $this->run(GetProperties::class);
$ids = $properties->pluck('id'); // Get all the ids as an array
$propertyUsers = propertyUsers::whereIn('property_id', $ids)->get(); // Get the propertyUsers model
foreach($properties as $property) {
$property->property_users = $propertyUsers->where('property_id', $property->id); // Not even sure you can do that, proerty_users may not been writable
}
Alright, after reading here and there.
I found this article: https://laravel-news.com/eloquent-eager-loading
The solution is really nice. Simply:
$properties = $this->run(GetProperties::class);
//lazy load
$properties->load('property_users');
$properties = $properties->map(function($property) {
$user = $property->property_users->first(function($user) {
if($user->user_id == Auth::user()->id) {
return $user;
}
})->only('chat_username');
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $user['chat_username']
];
});
After checking query logs:
//auth query done by middleware
[2019-05-21 07:59:11] local.INFO: select * from `users` where `auth_token` = ? or `temporary_auth_token` = ? limit 1 ["token_removed_for_security_purpose","token_removed_for_security_purpose"]
//These are the queries made:
[2019-05-21 07:59:11] local.INFO: select `properties`.*, `property_users`.`user_id` as `pivot_user_id`, `property_users`.`property_id` as `pivot_property_id` from `properties` inner join `property_users` on `properties`.`id` = `property_users`.`property_id` where `property_users`.`user_id` = ? [8]
[2019-05-21 07:59:11] local.INFO: select * from `property_users` where `property_users`.`property_id` in (2, 4)
This way, I can keep my GetProperties as small as possible, then just lazy load it whereever I need it.

Laravel include relationship result if one exists

I am trying to write a query where all items are returned (products) and if a relationship exists for that particular item (many to many) then that information is included too. When I include the relationship at the moment on the query it only returns items that have that relationship rather thatn every single item, regardless of whether that relationship exists or not.
Here is my query at the moment:
public static function filterProduct($vars) {
$query = Product::query();
if((array_key_exists('order_by', $vars)) && (array_key_exists('order', $vars))) {
$query = $query->orderBy($vars['order_by'], $vars['order']);
}
if(array_key_exists('category_id', $vars) && $vars['category_id'] != 0) {
$query = $query->whereHas('categories', function($q) use ($vars) {
return $q->where('id', $vars['category_id']);
});
}
if(array_key_exists('manufacturer_id', $vars)) {
$query = $query->whereHas('manufacturer', function($q) use ($vars) {
return $q->where('id', $vars['manufacturer_id']);
});
}
$query = $query->whereHas('options', function($q) use ($vars) {
});
As you can see, when an item has the 'options' relationship I need to have that particular row include details of that relationship in the returned date. With the code as it is though it is only returning items that have this relationship rather than every single item.
Can someone advise me as to how this is achieved please?
Thanks!
I feel a bit stupid as it was so simple but it was solved by adding this:
$query = $query->with('options');

How to use withTrashed when I'm querying using eager loading?

I have some tables, one of which is a table named Users where there are soft deleted rows.
I have code like:
$appointments = Appointment::with('user')->get();
$return = array();
foreach ($appointments as $key => $appointment) {
$return[] = array(
$appointment->customer_name,
$appointment->date->format('d/m/Y'),
$appointment->time,
$appointment->status,
$appointment->user->full_name,
);
}
Because the row with the user is deleted, I get an error on the line with:
$appointment->user->full_name
because of course there is no match for this user.
I tried adding withTrashed() to the first line, both before and after with('user') but this didn't help.
How do I ensure that this query really does return all appointments with all users, even deleted ones?
withTrashed() can be applied to the query that retrieves the users like so:
$appointments = Appointment::with(array('user' => function($query) {
$query->withTrashed();
}))->get();
You can also apply withTrashed() to both the appointment and their users:
$appointments = Appointment::with(array('user' => function($query) {
$query->withTrashed();
}))->withTrashed()->get();
Alternatively, you can add withTrashed() to the association method to apply it whenever the association is loaded:
public function user() {
return $this->hasOne('User')->withTrashed();
}

Resources