Try taking this line of code out, and it should work fine:
protected $morphClass = 'product';
Or change that to the full namespace path like 'App\Product'. When you are eager loading the results, it runs this method:
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
$this->query->where($this->morphType, $this->morphClass);
}
It gets the morphClass, but since yours was not properly named, it wasn't eagerloading the results properly.
Hey thanks for the response! That $morphClass is actually necessary for me. I can explain that in a second, but I forgot to mention that I am aliasing my Product model to the word 'product' in config/app.php like so:
'aliases' => [
//all other aliases here
'product' => App\Product::class
]
So addEagerConstraints() should be able to alias that into the correct class. Unless the Builder does not have access to the config files in which case I could be out of luck.
The reason that I need to do this is because my Note database table has a different convention for storing the type for our polymorphic relation. So if I were building this all from scratch, the note_type field would just have the full namespace for the model in the field (i.e. 'App\Product'), but because we are using our own convention, it just has the word 'product'. That is the whole reason I declared a custom $morphClass and aliased it to the model - I need a way to map our custom way of storing data with the way Laravel expects the relationship to be.
Surely there must be a way to use polymorphic relations without storing the full namespaced model string in the database field? I feel like this would be a somewhat common use case.
Also, I'd like to reiterate the fact that when I did a DB::getQueryLog(); during the eager loading statement and printed out what queries were actually being run, it printed out the correct query for getting the notes, which was:
select * from `note` where `note`.`note_ref_id` in ("123") and `note`.`note_type` = "product"
This suggests to me that it actually is grabbing the correct data but somewhere on its way from returning the results to making them into an Eloquent collection it all gets lost.
Hm...I'm not seeing what the problem is with just storing the full namespace.
But in any case, I don't think aliases work that way. I guess one solution you can do is override the getMorphClass()
method like this for your Product model.
public function getMorphClass()
{
return config("app.aliases.{$this->morphClass}");
}
Oh believe me, I would love to be able to convert all our database field values to just use the full namespace :)
The issue is that we are making a Laravel json API for a database/codebase that is many years old. There are hundreds of thousands of records that already exist and a lot of other legacy code that depends on the fields having our convention without the namespaces. I'm forced into this.
I tried your accessor method, but that didn't work. I tried a few variations of it, including just hard-coding my namespaced model but it still didn't work. In fact, when I did that, then I couldn't even get the notes after I had first instantiated my model, which worked before. The only way I could get that working was to return the string 'product' back, but that is essentially the same thing as just setting $morphClass property to the string. I also tried all of those variations with declaring that property and without declaring that property.
I still feel like there must be some way to map what is in the database to what Laravel's eager loading expects for the class name. I tried my original way of doing things after reading this : http://stackoverflow.com/questions/19881963/polymorphic-eloquent-relationships-with-namespaces
I think I have a better understanding of the real issue. By declaring my custom $morphClass property, Laravel is able to successfully build out the query as it should be (and as I have been seeing it). The issue is that after it has gotten all of the correct data, it does not know how to associate the results from the notes table with any model (most likely it is searching for a 'product' model when it actually needs 'App\Product'). So I need that custom $morphClass in order that the query is correct, but it will cause Laravel to be unable to associate the results with any model. If I change it so that I pass the full namespaced name, Laravel will be able to associate the results with the right model, but the query will always return back empty because I don't have any database fields with the namespaced name.
The addEagerConstraints() method you showed me above is still at the point where Laravel is creating the query. That part is working as expected. What I really need to see is how Laravel turns those database results into the Model (with its relation Collections) that it returns back to me. thomastkim, you seem much much more familiar with the Laravel source than I am - do you know where I would find that?
Interesting. So I thought, "There has to be a simpler way", and I actually decided to copy/paste your original code without any fancy overrides, and it worked for me.
Are you sure that it's not getting the relationship? If you dd($product)
, what do you get?
Hmm, I suspect that your database might not be structured in the same way as mine which could explain why your test worked. But when you say that you did it without all of the fancy overrides, what exactly do you mean? Could you post your models?
An example row from my 'note' table looks like this:
note_id | note_type | note_ref_id |
----------------------------------------------------
2203 | product | 1234
Remember, I don't have the namespaced model name in the database field, just the string 'product'.
So when I currently dd my product, I get this:
Collection {#206 ▼
#items: array:1 [▼
0 => Product {#201 ▼
#table: "product"
#primaryKey: "product_id"
#morphClass: "product"
#connection: null
#perPage: 15
+incrementing: true
+timestamps: true
#attributes: array:50 [▶]
#original: array:50 [▶]
#relations: array:1 [▼
"notes" => Collection {#200 ▼
#items: []
}
]
#hidden: []
#visible: []
#appends: []
#fillable: []
#guarded: array:1 [▶]
#dates: []
#dateFormat: null
#casts: []
#touches: []
#observables: []
#with: []
+exists: true
+wasRecentlyCreated: false
}
]
}
As you can see, it attaches a notes Collection to the relations field, but it is just always empty. If I take this exact same model, instantiate an instance of the same product, and then after that query its 'notes' property, it will return with the actual notes. It is just when I am eager loading it.
I have been delving pretty deep into the Eloquent Builder class and getting more confused, because I can see where it actually does associate the notes with its corresponding model. It just somehow seems to lose its data along the way when it attaches the collection of Note models to its parent Product model. Yet it retains the name of the relation. This is so weird.
Here's what I did to try to duplicate your problem. But for me, it's working perfectly fine.
Database:
note_id | note_ref_id | note_type
1 | 1 | product
product_id
1
As you can see, I have only one entry for both. Neither entry has any extra data. Just the bare minimum.
This is what the models look like:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model {
protected $table = 'product';
protected $primaryKey = 'product_id';
protected $morphClass = 'product';
public $timestamps = false;
public function notes()
{
return $this->morphMany('App\Note','noteable','note_type','note_ref_id');
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Note extends Model {
protected $table = 'note';
protected $primaryKey = 'note_id';
public $timestamps = false;
public function noteable()
{
return $this->morphTo(null,'note_type','note_ref_id');
}
}
This is my query:
$product = Product::where('product_id',1)->with('notes')->first();
This is the output:
Product {#182 ▼
#table: "product"
#primaryKey: "product_id"
#morphClass: "product"
+timestamps: false
#connection: null
#perPage: 15
+incrementing: true
#attributes: array:1 [▶]
#original: array:1 [▶]
#relations: array:1 [▼
"notes" => Collection {#185 ▼
#items: array:1 [▼
0 => Note {#188 ▶}
]
}
]
#hidden: []
#visible: []
#appends: []
#fillable: []
#guarded: array:1 [▶]
#dates: []
#dateFormat: null
#casts: []
#touches: []
#observables: []
#with: []
+exists: true
+wasRecentlyCreated: false
}
Wow, I am so very confused by this. It really does look like you have an identical setup to mine. The only thing that I noticed was different was the timestamps, so I changed that but that made no difference. My table rows do have some extra fields that I'm not putting here for simplicity sake (like timestamps) although I don't think those should make any difference.
I've removed the config alias. I've even made a different model that also has notes and tried it fresh and I am still seeing the same results (eager loading doesn't work, but if I get the object first and then get the relation it will work).
The only thing I can even think of is there is some difference in our Laravel versions. What version are you on? I'm using 5.1.19.
Otherwise, maybe there is just some boneheaded thing lurking in my code that is somehow obstructing this. I didn't start the project very long ago, so my app is not really bogged down with a ton of stuff yet. I think I will have to look at this with fresh eyes tomorrow. Thank you so much for all of your help! You have no idea how much this is all helping me!
I'm running version 5.1.19 as well.
No problem. Hopefully, you can figure out what's going on! I bet someone will come along soon and point out something very minor that we are missing.
Wow, I have finally figured out what the problem was and yes, it was something unrelated to Laravel (sort of). Because this is an old database, at some point an old developer had converted the data type of 'note_ref_id' field to a decimal (for obscure reasons). So for instance, a product with an id of 1234 would get listed in the notes table as having an id of 1234.00. This works fine for mysql queries because mysql always does a conversion anyway so we have never run into an issue. This explains why it was getting the notes after I had already instantiated the model - at that point Laravel is just doing a basic PDO query so it will get the correct results.
However when you are eager loading, Laravel builds out a dictionary mapping of your database results to the model. To do this it turns all of your results into strings and maps them to your model's key. In this case, it would try to find a model with an id of '1234.00' and since it doesn't exist, would never map the notes to the corresponding model. Now that we know what the problem is, we have changed that data type back to an integer and things work perfectly.
thomstkim, thank you so much for all of your help! I wish there was some better way to thank you than just writing in a forum post, but really, your level of effort in helping out has been incredible. I really appreciate it!
Sign in to participate in this thread!
The Laravel portal for problem solving, knowledge sharing and community building.
The community