Can I create a double-morphed many-to-many relationship in Laravel? - laravel

I have two models: Product and Group, which I want to both give a many-to-many 'shown' and a 'recommended' relation with itself and the other (so both have has to be morphed in both ways).
So a 'shown' resource of either type has a set of 'recommended' resources of either type (the Groups are actually acting as a container for grouped Products).
The table should be pretty straightforward:
Schema::create('recommendables', function (Blueprint $table) {
$table->id();
$table->morphs('shown');
$table->morphs('recommendable');
$table->timestamps();
});
Then relations are defined in the trait 'RecommendableTrait' which is then given to both classes. As far as these relationships are concerned both classes are then handled pretty much the same later on (even to be merged into a single collection), so there is merit in trying to do it this way.
The problem is that the following relation definitions won't work, because it will look for group_id if I call the morphed relations on Group, similarly with `morphedByMany'.
trait RecommendableTrait {
// RECOMMENDED GROUPS
public function recommendedGroups()
{
return $this->morphToMany(Group::class, 'recommendable', 'recommendables')
->withTimestamps();
}
// RECOMMENDED PRODUCTS
public function recommendedProducts()
{
return $this->morphToMany(Product::class, 'recommendable', 'recommendables')
->withTimestamps();
}
// SHOWN GROUPS
public function shownGroups()
{
return $this->morphToMany(Group::class, 'shown', 'recommendables')
->withTimestamps();
}
// SHOWN PRODUCTS
public function shownProducts()
{
return $this->morphToMany(Product::class, 'shown', 'recommendables')
->withTimestamps();
}
}
Now I know, I'm looking to create a bit of a weird relation tangle here. But I'm hoping defining a double-morphed many-to-many is possible in Laravel?
If not, I guess I'll have to either split it up in two single-morphed many-to-Many relationship sets for both classes. Which would be annoying because of the code that goes on top of these relations would have to be needlessly duplicated. Or I'll have to forego relations and just DB query the results directly. Thus giving up some of Eloquence's convenience, but at least allowing me to keep the code confined to a single shared trait.

Related

Scope by custom attribute or excluding data by scope

I have a Model User that has a polymorphic relation to table Relationships with Pivot column relationship_level.
public function activities()
{
return $this->morphedByMany('App\Activity', 'relationship')->withPivot('relationship_level')->withTimestamps();
}
public function relationships()
{
return $this->hasMany('App\Relationship');
}
I want one the following:
I want scopeNoCitizens which to exclude everyone who has a relationship_level Citizen no matter what other relationships he has.
I've tried:
scopeNoCitizens($query) {
$query->whereHas('relationship', function($query) {
$query->where('relationship_level', '!=', 'Citizen');
});
}
This doesn't work, maybe because all users have many levels and I want everyone who has even one record among many that has level citizen to be excluded from the collection.
I also have a custom attribute getHighestRoleAttribute, but as far as I understand you can't filter by accessor/custom attribute in a scope.
What I currently do is "filter" the collection after get(), but I have to manually paginate after that and do that on numerous places in the app.

Laravel Eloquent Relation belongsTo update

I am trying to update/delete/create in belongsTo relations.
Company has many sports
sports is belonging to Company
Here is two models.
class CompanySports
{
public function company()
{
return $this->belongsTo(Company::class, "company_id","id");
}
class Company
public function sports()
{
return $this->hasMany(CompanySports::class,"company_id","id");
}
}
at controller, when sports is added or modified or remove, what is the best practice to update?
i know that many to many, sync can be used. In this, what is the best solution? Should i compare everytime after loading all from database which is not good practice i believe.
From your code, I would first recommend putting your models in separate files, and ensuring they are singular. If you use the artisan make:model command to generate the stubs, it should do this for you.
// app/CompanySport.php // <-- NOTE singular
class CompanySport // <-- NOTE singular
{
public function company()
{
return $this->belongsTo(Company::class, "company_id","id");
}
}
// app/Company.php
class Company {
public function sports()
{
return $this->hasMany(CompanySport::class,"company_id","id"); // singular
}
}
From there, I find it helpful to build helper methods in the various classes so that the grammar sounds natural and more importantly, belongs to the model. For example:
// app/Company.php
class Company
{
...
public function addSport(CompanySport $sport)
{
$this->sports()->save($sport);
}
public function removeSport(CompanySport $sport)
{
$this->sports()->find($sport->id)->delete();
}
}
These helper functions can then be easily called from anywhere, e.g. controller:
// CompanySportsController.php
public function store(Company $company, CompanySport $sport)
{
$company->addSport($sport);
return redirect('/company/' . $company->id);
}
If you are using these helpers, there is no comparing or sync to be done since you are only using a one to many relationship. Eloquent does everything for you.
Also, I've found this cheatsheet particularly helpful when building out the initial relationships and scaffolding of a new app.
While adding new record of Company Model, you need not to do anything as there is no child for it yet.
While updating an instance of a Company model, again you need not to update anything on its children. As relationship are based on id(primary key) which I believe you don't change while updating.
And now for deleting there are some questions. Do you want to delete the children when the parent is deleting? If so, you can use ON DELETE CASCADE which you can set up in migration like
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
in your spors table.
Well you can make your own function too like answered in here
Well if you don't want to delete the children, you can use softdelete on your Model. set up the relations then like
CompanySports
public function company()
{
return $this->belongsTo(Company::class, "company_id","id")->withTrashed();
}
This way you can get the parent of a children without any error though the parent is deleted.

Laravel relationships: hasManyThrough a model with on an pivot table

I have 3 models in Laravel:
Item
ModifierGroup
Modifier
An Item can have many ModifierGroups via an intermediate table:
public function modifierGroups()
{
return $this->belongsToMany(
'App\ModifierGroup',
'menu_item_modifiers',
'item_id',
'group_id'
)->using('App\MenuItemModifier')
->orderBy('position', 'ASC')
->withPivot('position');
}
A ModifierGroup has many Modifiers:
public function modifiers()
{
return $this->hasMany(
'App\Modifier',
'group_id',
'id'
)->orderBy('position', 'ASC');
}
My question is whether it's possible to have a function on the Item that gets to the Modifiers, going through the ModifierGroup (and its pivot)? HasManyThrough doesn't seem to fit with an pivot table involved or does it?
It's possible with a BelongsToMany relationship by "skipping" the ModifierGroup table:
public function modifiers() {
return $this->belongsToMany(
'App\Modifier',
'menu_item_modifiers',
'item_id',
'group_id',
null,
'group_id'
);
}
This problem can be solved using pivot tables, but not in the way that you are using them. As I assume you have three tables - each associated with your models (Item, ModifierGroup, Modifier), you haven't set up pivot functionality as you don't actually have pivot tables. To do this you would require one table to link Item to ModifierGroups and another to link ModifierGroups to Modifier.
To start, lets create the tables using Artisan:
php artisan generate:pivot Item ModifierGroup
now, we have a table called item_modifier_group (I believe?). This table should have two columns, item_id and modifier_group_id. These are the keys Eloquent will use to connect to the items and modifier_groups tables. Now, in our query we can access and item's modifier groups by using the following query:
public function modifierGroups()
{
return $this->belongsToMany('App\ModifierGroup');
}
This means that when you call $item->modifierGroups() you will get a collection of all modifier groups having an id of $item->id.
This process can then be repeated for the ModifierGroups to Modifiers relationship:
php artisan generate:pivot ModifierGroup Modifiers
Now define the modifiers method in the ModifierGroup model:
public function modifiers()
{
return $this->belongsToMany('App\Modifier');
}
Now we have our pivot tables set up, and our relationships defined in one direction (just add similar methods to models to get Items via Modifiers)
The final piece of the pie is Eager Loading (it's amazing). Get item, with modifiers through the appropriate groups like so:
Item::with('modifierGroups.modifiers')->findOrFail('id');
Now there are other ways you could have gone about this problem, and some may even be more valid, however the huge benefit to doing it this way is the flexibility. You now have a way to connect Items, ModifierGroup and Modifiers with pivot tables and with a simple eager loaded query, can get any collection combination you deem necessary. Hope this helped! This is definitely the longest solution I've ever written on here...
For future reading I recommend:
Eager Loading - Nested Eager Loading
Defining Relationships - Many-to-Many

Laravel 5 - defining relationships

everything was working fine with a single Model, but now I am implementing more, I have noticed an issue.
I have several document Models which represent a different type of document. For now, let's say I have DocumentA and DocumentB.
Each Document allows file uploads, so I have created a FileUpload Model. A Document can have many FileUploads.
So, seems pretty straight forward at this point. My FileUpload table has a documentId field, which is a reference to the id field of the Document that is using it.
In DocumentA, I have something like so
public function uploadFile()
{
return $this->hasMany('App\UploadFile', 'documentId');
}
So DocumentA can have many UploadFiles, linked by the documentId.
DocumentB has the same function within its Model.
My problem lies with the UploadFiles model. Firstly, this model now has two belongTo events e.g.
public function documentA()
{
return $this->belongsTo('App\DocumentA', 'documentId');
}
public function documentB()
{
return $this->belongsTo('App\DocumentB', 'documentId');
}
This could be the problem, not sure if I can have multiple belongs to? My immediate problem however is to do with the migration of the doc_file table. At the moment I have this
Schema::table('doc_file', function (Blueprint $table) {
$table->integer('documentId')->unsigned()->default(0);
$table->foreign('documentId')->references('id')->on('document_a')->onDelete('cascade');
});
Schema::table('doc_file', function (Blueprint $table) {
$table->integer('documentId')->unsigned()->default(0);
$table->foreign('documentId')->references('id')->on('document_b')->onDelete('cascade');
});
So I am trying to provide foreign keys to my documents. When I try to migrate, it tells me that
Column already exists: 1060 Duplicate column name documentId
Am I handling my relationships correctly? Essentially, I have many documents, and each document can have many files.
Any assistance with my database relationships appreciated.
Many thanks
Looking at your problem at first glance, it seems that there is a little confusion for you regarding the concept model.
The Model Concept
A model is in fact a conceptualization of a real-world object as it is used to represent a real-world entity.
In other words, it represents a whole class of objects with similar properties. For instance a Car model would represent all cars, whether they are of type Lamborghini or Mercedez. The fact is that they all come under the Car classification.
Same concept goes in Eloquent, and with your use case; therefore a Document model is sufficient to represent both of your documents (DocumentA and DocumentB).
Eloquent Relationships
To refine what you've achieved so far, your models' relationships can be refactored as such:
Document Model
public function fileUploads(){
return $this->hasMany('App\FileUpload');
}
FileUpload Model
public function document(){
return $this->belongsTo('App\Document');
}
Based on the relationship "EACH document has MANY file uploads", and the inverse "EACH file upload BELONGS to exactly one document", as you can see, there is only one belongsTo() method in the FileUpload model to define the latter part of the relationship.
Similarly, the schema for the tables defining the above relationship are as follows:
// Schema for Document table
Schema::table('document', function (Blueprint $table) {
$table->increment('id');
$table->string('name', 100);
});
// Schema for FileUpload table
Schema::table('doc_file', function (Blueprint $table) { // file_uploads would have been a more friendly name in my opinion
$table->integer('documentId')->unsigned()->default(0); // note that `documentId` is interpreted as `documentid` in MySQL
$table->foreign('documentId')->references('id')->on('document')->onDelete('cascade');
});

Laravel / Eloquent - custom relation method

I have a class Report which has a belongsToMany relation to Metric. Report also additionally has a belongsTo relation to Metric.
Normally, the model returned by the belongsTo relation is the same as one of the models in the belongsToMany relation. When this is true I'd like it to be the case that each of the two relations actually looks at the same object instance (this also saves an extra trip to the db).
So, in basic terms - is there a way to get one relation to check another first, to see if a model has already been loaded, and if so, point to that object rather than creating a new one.
I tried putting some code in the belongsTo relation method for Metric but I can't get round the fact it needs to return an instance of belongsTo, which needs various things passed as constructor arguments (ie. a query object), which aren't relevant in that case that the model has already been loaded in the belongsToMany relation.
I thought of ditching the belongsTo relation and adding data horizontally in the pivot table for the belongsToMany relation, but it isn't a many-to-many relation required so that seems a bit wrong.
Thanks!
Geoff
The idea here is to write a function which would check if a relationship is loaded and return that relationship, otherwise it will return the belongsToMany. This would go in your Report class. This is also for Laravel 5. If you have 4, just remove the namespaces from the model names.
public function metric()
{
return $this->belongsTo('App\Metric');
}
public function metrics()
{
return $this->belongsToMany('App\Metric');
}
public function getMetric()
{
if(array_key_exists('metric', $this->getRelations())) {
return $this->metric;
}
return $this->metrics()->first();
}
If you do decide to just go with a belongsToMany only, I'd suggest putting a unique key on your pivot table for both ID's to keep from getting any duplicates in the pivot table.

Resources