I'm starting to learn how to use Vue and Laravel. So I'm writting some Vue components to replace some parts of the blades.
In my application I have a Sugerencia (suggest) model. Of course I have users models and many others, but I need those two for this question
My SugerenciaController#index is quite simple:
public function index()
{
$sugerencias = Sugerencia::orderBy('created_at', 'desc')
->with('user')
->paginate(10);
return view('sugerencias.index', compact('sugerencias'));
}
In my sugerencias/index.blade.php I "send" the $sugerencia to Vue with:
#foreach ($sugerencias as $sugerencia)
...
<user-info :sugerencia="{{ $sugerencia }}"></user-info>
...
#endforeach
And works as intended. user-info is a Vue component that formats and displays the user's info. In Vue, to access the user's name, I use {{ sugerencia.user.name }} in my template. So far, so good. But this works only in the collection.
Then I have my SugerenciaController#show method, which is as simple as
public function show(Sugerencia $sugerencia) {
return view('sugerencias.show', compact('sugerencia'));
}
I assumed that in my sugerencias\show.blade.php I could use the same call for the user-info Vue component as:
...
<user-info :sugerencia="{{ $sugerencia}}"></user-info>
...
...but I can't, since it's not "sending" the user relationship. I assume its because I'm not using the ->with('user') as I did in the collection.
In the mounted() method I placed a console.log(this.sugerencia) and I get all sugerencia's data, but not the user data (just the user_id foreign key)
So question is, how do I "append" the user relationship to the sugerencia model, kinda like eager loading. I want to do something that as trivial as $sugerencia->user->name in the ORM
Please forgive me if my English isn't clear enough and feel free to edit
Honestly, I would decouple the logic here and load the page and vue component with a null value and then do ajax requests to get the required data. Or implement a standardized way of getting data from the controller to the vue components without having specific variables in my view blade templates.
However, for your particular question, you could use Lazy eagar loading like this:
public function show(Sugerencia $sugerencia) {
$sugerencia->load('user');
return view('sugerencias.show', compact('sugerencia'));
}
From the docs:
Sometimes you may need to eager load a relationship after the parent model has already been retrieved. For example, this may be useful if you need to dynamically decide whether to load related models:
Related
I've started using Filament PHP for creating a Laravel based intranet application but stumbled across a question I couldn't answer myself using the official documentation:
What's the easiest way to show relational data inside the view page of a resource?
I have two resources ClientResource and ProjectResource which results in two Laravel relationships:
Client model:
public function projects(): HasMany
{
return $this->hasMany(Project::class);
}
Project model:
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
I have implemented a BelongsToSelect field inside project resource to assign a client:
Components\BelongsToSelect::make('client')
->relationship('client', 'first_name')
->required(),
Everything works fine so far, but (obviously) all I can see on the project's view page is the disabled select field showing the customer's first name. I'd like to have all related fields listed. Have I missed something crucial in the documentation or what's the best way to approach this?
I've had a look into the RelationManager but it seems there is only a belongsToMany relationship (no belongsTo).
This can also be done with a select component by specifying a relation like this:
Select::make('client_id')
->relationship('client', 'first_name');
You also have access to the eloquent query builder instance to play with like so:
Select::make('client_id')
->relationship('client', 'first_name',
fn (Builder $query) => $query->where('status', 'actif'))
);
Document reference: https://filamentphp.com/docs/2.x/forms/fields#populating-automatically-from-a-relationship
Here is my current situation:
I have a Task model.
Tasks have owners (a belongsTo relationship)
Owners have accounts (yet another belongsTo relationship)
I'd like to set up a "belongsToThrough" relationship from Tasks to Accounts.
My first solution was to define a relationship in the Tasks model, like this:
public function account(): BelongsTo
{
return $this->owner->account();
}
With it I could call $task->account and retrieve a task's account easily. The problem is that this doesn't work with load/with, which in turn causes problems because I can't refresh() a task that has had the account loaded in (because refresh uses load). The error just states Trying to call account() on null which was honestly expected.
My second solution was to change the relationship method to:
public function account(): BelongsTo
{
return $this->owner()->first()->account();
}
With this, I can also simply call $task->account and retrieve the model, and when loading, it doesn't work (returns null), but also doesn't throw any errors. I don't need to load this relationship in, it just happens that sometimes I need to refresh models and having the load method throw an error is not ok.
In summary
What I'm looking for is kind of a BelongsToThrough, as a Task would BelongTo an Account through an Owner (User). Is there a way to do this that works using both $task->account and $task->load('account'). Before you tell me I can load it using owner.account, I know that, but refresh() will do it automatically with load('account') so I need it to work like that, not with the dot notation.
To get it working with load(), you'll need to define an account relationship on the owner model, if you haven't done so already. Like this:
public function account() :BelongsTo
{
return $this->belongsTo(AccountsTable);
}
Then use dot notation when calling load() on your task model like:
$task->load('owner.account');
You can do that using eager loading
public function account()
{
return $this->belongsTo('App\ParentModel', 'foreignkey', 'localkey');
}
After that you can easily fetch relation data with load/with.
Thanks,
Is it possible to use eager loading using the with method but giving it another name? Something like:
->with('documents as product', 'documents.documents as categories')
I have a documents table that can be product or categories, eager loading is working but not that friendly to retrieve the documents by just the document name instead of what it really is.
This feature is currently not supported in any Laravel version. Your best bet is to duplicate your relations and name them according to your needs. E.g.:
class Post extends Model
public function documents() {
return $this->hasMany(Document::class);
}
public function products() {
return $this->hasMany(Document::class)
->where('type', 'product'); // Scope the query, if necessary
}
public function categories() {
return $this->hasMany(Document::class)
->where('type', 'category'); // Would a Document really be of type 'category', though? Looks like a code smell.
}
}
$postsWithAllDocs = Post::with('documents')->get();
$postsWithProductsOnly = Post::with('products')->get(); // Only eager load Documents of type 'product'
On a side note, you mention that a Document can be a product or category, which logically doesn't seem to make much sense. Part of the issue could probably be resolved by rethinking the structure of your database and relations.
Eager loading tells "load also this relationship data", so next you can access subject->relation without further queries
if you want to rename the relationship maybe you should do it renaming the relationshp in the model, not in the eager loading
you can also bypass this by adding virtual attributes:
function getProductAttribute(){
return $this->document;
}
leaving eager loading on original document
resulting in product attribute that is the same as document:
$subject->product === $subject->document
I asked myself the same question, and since I didn't find a satisfying answer online, here is what I did.
I had:
$o->load('X');
but I wanted the $o object to have attribute Y with the value of X relation. Since I already had the Y relation defined for $o, I couldn't rename X to Y and finish the job. So I did
$o['Y'] = $o->X();
I know this is not the best solution, but it works for my case :)
Note: load and with produce exactly the same number of sql queries - you need to choose the one which is more appropriate for your situation.
I am fairly new to laravel and I built a little "similar posts" section. So every post has a tag and I query all the id's from the current tag. And then I find all the posts with thoses id's. Now my problem is that the current post is always included. Is there an easy way to exclude the current id when querying?
I can't seem to find anything in the helper function on the laravel docs site
this is my function:
public function show($id)
{
$project = Project::findOrFail($id);
foreach ($project->tags as $tag){
$theTag = $tag->name;
}
$tag_ids = DB::table('tags')
->where('name', "=", $theTag)
->value('id');
$similarProjects = Tag::find($tag_ids)->projects;
return view('projects.show', ['project' => $project, 'similarProjects' => $similarProjects]);
}
An easy way to solve your issue would be to use the Relationship method directly instead of referring to it by property, which you can add additional filters just like any eloquent transaction.
In other words, you would need to replace this:
Tag::find($tag_ids)->projects
With this:
Tag::find($tag_ids)->projects()->where('id', '!=', $id)->get()
Where $id is the current project's id. The reason behind this is that by using the method projects(), you are referring your model's defined Relationship directly (most probably a BelongsToMany, judging by your code) which can be used as a Query Builder (just as any model instance extending laravel's own Eloquent\Model).
You can find more information about laravel relationships and how the Query Builder works here:
https://laravel.com/docs/5.1/eloquent-relationships
https://laravel.com/docs/5.1/queries
However, the way you are handling it might cause some issues along the way.
From your code i can assume that the relationship between Project and Tag is a many to many relationship, which can cause duplicate results for projects sharing more than 1 tag (just as stated by user Ohgodwhy).
In this type of cases is better to use laravel's whereHas() method, which lets you filter your results based on a condition from your model's relation directly (you can find more info on how it works on the link i provided for eloquent-relationships). You would have to do the following:
// Array containing the current post tags
$tagIds = [...];
// Fetch all Projects that have tags corresponding to the defined array
Project::whereHas('tags', function($query) use ($tagIds) {
$query->whereIn('id', $tagIds);
})->where('id', !=, $postId)->get();
That way you can exclude your current Project while avoiding any duplicates in your result.
I don't think that Tag::find($tag_ids)->projects is a good way to go about this. The reason being is that multiple tags may belong to a project and you will end up getting back tons of project queries that are duplicates, resulting in poor performance.
Instead, you should be finding all projects that are not the existing project. That's easy.
$related_projects = Project::whereNotIn('id', [$project->id])->with('tags')->get();
Also you could improve your code by using Dependency Injection and Route Model Binding to ensure that the Model is provided to you automagically, instead of querying for it yourself.
public function show(Project $project)
Then change your route to something like this (replacing your controller name with whatever your controller is:
Route::get('/projects/{project}', 'ProjectController#show');
Now your $project will always be available within the show function and you only need to include tags (which was performed in the "with" statement above)
I'm working on a side project in which we are trying to implement a "like" functionality for user's posts. We are using Laravel's ORM and we would like to use eager loading to make things easier, I'll outline the issue below. Some information first, the Post.php Model contains this:
public function likes()
{
return $this->hasMany('App\Models\PostLike', 'post_id', 'post_id');
}
The PostController.php implementing the API call to load the posts and their likes looked like:
$posts = Post::with("likes")->where("group_id", "=", $group_id)->get();
An example JSON response from the posts API might look like this:
[
{
"post_id":1,
"group_id":1,
"author_id":1,
"text":"Some text here.",
"created_at":"2015-08-13 00:15:08",
"updated_at":"2015-08-13 00:15:08",
"likes":[
{"post_id":1,"user_id":1,"updated_at":"2015-08-14 03:05:48"}
]
}
]
The issue with this is that the "likes" are not indexed by the user_id, so you have to search through the array to determine if a user has liked the post or not, it would be much easier to have it indexed by the unique user_id key. That would yield something like:
[
{
"post_id":1,
"group_id":1,
"author_id":1,
"text":"Some text here.",
"created_at":"2015-08-13 00:15:08",
"updated_at":"2015-08-13 00:15:08",
"likes":{
"1": {"post_id":1,"user_id":1,"updated_at":"2015-08-14 03:05:48"}
}
}
]
So the ultimate question is how do we index the eager loading response by one of the columns it returns?
In order to achieve that, you'll need to add extra post-processing step to your code that would index the array by id. This is very easy using Collection's helper method keyBy().
What you need is a accessor that would load the relation if needed and reindex the array. This accessor can be used in different scenarios, even without eagerly loading the relation, that's why it needs to handle relation loading if needed.
Adding that method to your Post model should do the trick:
public function getLikesAttribute() {
return $this->getRelationValue('likes')->keyBy('user_id');
}
Custom accessors take priority above relation definitions in cases like that, when botu likes() and getLikesAttribute() exist. That's why your accessor will be called when you do $post->likes and will reindex the table.