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

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);
}
}
}

Related

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();

Verify belongsToMany for update function in controller laravel 5.4

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);
}
}
}

Get all championships that are teams in Eloquent

I have a tournament, a tournament can have many >
public function championships()
{
return $this->hasMany(Championship::class);
}
and a Championship hasOne Category. In Category, I have the isTeam attribute.
Now I need a function that get me all the championships that have the isTeam = 1 in Category table.
public function teamChampionships()
{
}
Of course, I have defined : $tournament->championships, $championship->category
In my controller, I get all of them:
$tournament = Tournament::with('championship.category')->find($tournament->id);
Any idea???
Try
$tournament = Tournament::with(['championships' => function ($query) {
$query->whereHas('category', function($subquery) {
$subquery->where('isTeam', '=', 1);
});
}])->get();
If the above doesn't work, try a different approach. Define isTeam() scope in Category model
public function scopeIsTeam($query) {
return $query->where('isTeam', 1);
}
Then you can use it like this
$tournament = Tournament::with('championships.categoryIsTeam')
->find($tournament->id);
Even better, create another scope in Championship that loads only teams
public function categoryTeam() {
return $this->hasOne(Category::class)->isTeam();
}
Sorry for too much information. One of those should do the job.

Optimising eloquent query

I have a need to attach a custom select on an eloquent query, but I find the whole thing a bit hard to understand. This regards a forum system where each Forum object needs to know how many Topic relations and Comment relations are registered. This is what I've got so far:
in Forum.php
public function getNumTopics () {
return Topic::where('forum_id', '=', $this->id)->count();
}
public function getNumComments () {
return Comment::wherein('topic_id', Topic::where('forum_id', '=', 1)->lists('id'));
}
Controller returning json
public function getCategories () {
$categories = ForumCategory::with('forums')->get();
foreach ($categories as $cat) {
if ($cat->forums->count() > 0) {
foreach ($cat->forums as $forum) {
/* #var $forum Forum */
$forum->num_topics = $forum->getNumTopics();
$forum->num_posts = $forum->getNumComments();
}
}
}
return Response::json($categories, 200);
}
The call takes approx 1300ms to return 5 Forums in three categories. I suspect this is because this executes around 16 queries instead of one. Is there a way to attach the "num_topics" an "num_posts" as properties on select, so that I only execute one query?
Edit
What I basically want is for Eloquent to produce something like this when I ask for Forum::all():
select f.*,
ifnull(count(t.id), 0) num_topics,
ifnull(count(c.id), 0) num_posts
from forums f left join topics t on t.forum_id = f.id
left join comments c on c.topic_id = t.id
group by f.id
You will have to query those tables either way so your best bet should be to just eager load everything and count it after it's been retrieved instead of making more calls to the database just to find a count.
public function getCategories () {
$categories = ForumCategory::with('forums.topics.comments')->get();
foreach($categories as $category) {
foreach($category->forums as &$forum) {
$forum->num_topics = $forum->topics()->count();
$forum->num_comments = 0;
foreach($forum->topics as $topic) {
$forum->num_comments += $topic->comments()->count();
}
}
}
return Response::json($categories, 200);
}
This will attach num_topics and num_comments to each Forum object.
It's fewer calls but it's grabbing more information in one sweep so it may be faster or slower, will have to do some testing. The good news is that because it's grabbing everything, no additional queries should be needed.

how to make "follow" button change to "following" social network codeigniter

I am making a social network using Codeigniter and I'm trying to make the users view show a button when you are already following a person how do I do that??
Model
<?php
class User_model extends CI_Model {
function get_All(){
$this->db->select('*');
$this->db->from('membership');
// $this->db->join('following', 'following.id = membership.id');
$q = $this->db->get();
if($q->num_rows() > 0) {
foreach ($q->result() as $rows) {
$data[] = $rows;
}
return $data;
}
}
It's a little bit of work. Assuming that all users can be searched, you might have a list of all users, and of the 'friends' of all users. Then you run through the two comparing them. The following is an idea:
Database Members: All members |ID_User|...other user stuff...
Database Followers: Lists all friendship |ID_User|ID_Follower|
You need two queries for this: One for all, and one for Followers where ID_User=$UserID. As you run through all members as you suggest, then you can
function get_Followers(){
$friends= $this->db->query('SELECT * FROM Followers WHERE ID_User=$UserID');
$follow_array=$row->ID_Follower;
}
Now:
$follow_array //A list of all friends.
$q //Your original list of members
Then you can use:
if($q->num_rows() > 0) {
foreach ($q->result() as $rows) {
if(in_array($rows->ID_Follower, $Follower_array))
{
$data=true;
}
else
{
$data=false;
}
}
return $data;
}
Beware however, this may be a very server-heavy operation.
I'm going to assume you're talking about visiting a persons profile and if you are following that person you see a button. That only requires one query and logic in the view itself.
Going on this assumption you're already passing the person who's profile you're viewing to the view so all you need is a list of people the viewer is following to compare that id to.
I'm not really sure what your query in your code is doing since there's no description of your database so I'll give the code the way the tables should be laid out, which is a user table and a following table that is just a join table on the users. Assuming the following table has 3 columns id, userId, followingId where userId is you and followingId is the person you are following.
Controller
$data['following'] = $this->modelName->getFollowing($userId); //pass current userId
$this->load->view('viewName',$data);
Model:
public function getFollowing($userId)//$userId to be passed as the person viewing.
{
$this->db->where('userId',$userId);
$data = $this->db->get('following');
if($this->db->num_rows() > 0)
{
return $data;
} else {
$data=array();
$return $data;
}
Then in the view it's a simple if statement.
if(in_array($profileId,$following))
{
echo button;
}

Resources