Laravel polymorphic-relationship many to many - php

I'm having some trouble figuring out the polymorphic relationships.
I've read the documentation but for me it is quite confusing.
Hope anyone has the time to help me a bit to understanding it.
What I'm trying to do is to have a very simple tag system for some wallpapers.
I started a new test project just to get this working.
I have 3 models: Wallpaper, Tag and WallpaperTag
class Wallpaper extends Model
{
protected $primaryKey = 'wallpaper_id';
protected $table = 'wallpapers';
protected $guarded = ['wallpaper_id'];
/**
* Get all the tags assigned to this wallpaper
*/
public function tags()
{
//
}
}
class Tag extends Model
{
protected $primaryKey = 'tag_id';
protected $table = 'tags';
protected $guarded = ['tag_id'];
/**
* Get all wallpapers that have this given tag
*/
public function wallpapers()
{
//
}
}
class WallpaperTag extends Model
{
protected $primaryKey = 'wallpaper_tag_id';
protected $table = 'wallpaper_tags';
protected $guarded = ['wallpaper_tag_id'];
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
* Wallpaper relation
*/
public function wallpaper()
{
return $this->belongsTo('App\Wallpaper','wallpaper_id');
}
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
* Tag relation
*/
public function tag()
{
return $this->belongsTo('App\Tag','tag_id');
}
}
The wallpapers table in this test project contains only wallpaper_id
The tags table contanis a tag_id and a tag
The wallpaper_tags table contains a foreign key for both tags.tag_id and wallpapers.wallpaper_id
I've set it up like this so wallpapers can share tags without duplicating them. The problem is that I really dont understand the polymorphic relations and the example in the documentation.
Can anyone here 'spoonfeed' how this would work? :') Thanks in advance for all help.

So you are trying to create a relationship with ManyToMany between 2 tables, which in the DB needs a 3rd table to allow you to create such relationship.
This is due to the fact that one Wallpaper can have many Tag and vice versa! For such you need a 3rd table that holds that information accordingly.
The 3rd table is only holding ids in relationship to your 2 main tables. This allows the flexibility you are looking for, while your Object tables can actually hold information specific to them, without you having to duplicate it.
If you were to store the relationship ids on both tables you would be forced to duplicate your data and that is just something you do not wish on databases! Imagine having to update 1000 rows because it is basically the same wallpaper but with so many different tags.
Anyway, below is the code that should be get you going:
You do need to create a class to represent your relationship table (Kudos on the WallpaperTag class! That is the one!);
You do not touch that class anymore, do not add belongs or any other function!
You create the relationships on the main classes Wallpaper and Tag;
class Wallpaper extends Model
{
...
public function tags()
{
return $this->belongsToMany('App\Tag', 'wallpaper_tag', 'tag_id', 'wallpaper_id');
}
}
class Tag extends Model
{
...
public function wallpapers()
{
return $this->belongsToMany('App\Wallpaper', 'wallpaper_tag', 'wallpaper_id', 'tag_id');
}
}
class WallpaperTag extends Model
{
}
Laravel should create a relationship between your classes and map it accordingly to the correct 3rd table to sort the search for you.
If you follow the semantics all you needed was the class name. If ids are to change, then you will need to start telling Laravel what id column names it should be looking for as you deviate from the normal behaviour. It still finds it, just needs some guidance on the names! Hence why we start adding more parameters to the relationships belongsTo or hasMany etc :)
Pivot Table Migration
You do not need an id for your pivot table since your primary key is a combination of the two foreign keys from the other tables.
$table->bigInteger('wallpaper_id')->unsigned()->nullable();
$table->foreign('wallpaper_id')->references('wallpaper_id')
->on('wallpaper')->onDelete('cascade');
$table->bigInteger('tag_id')->unsigned()->nullable();
$table->foreign('tag_id')->references('tag_id')
->on('tags')->onDelete('cascade');
Let me know if it helped! :3

Related

Extending the Spatie Role Model but use a different database table

What I need to do is extend all of the functionality of the Spatie permissions package Role model, but use a different table for the derived model.
Right now I have a model SubscriptionPackage that I want to emulate the behavior of a Role such that it can be assigned permissions and in turn this model can be assigned to users. But I wanna keep the Role model intact too.
I have tried extending Yes, but when I create a new SubscriptionPackage, the new record is created in the roles tables instead of subscription_packages table despite specifying the table in my derived Model. As shown below
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\Permission; // This extends from Spatie\Permission\Models\Permission
use Spatie\Permission\Models\Role as SpatieRole;
class SubscriptionPackage extends SpatieRole
{
//
protected $guarded = ['id'];
protected $table = 'subscription_packages';
/**
* The permissions that belong to the package.
*/
public function packagePermissions()
{
return $this->belongsToMany(Permission::class);
}
}
With the code above I expect when I create a new SubscriptionPackage, the record should be inserted into the subscription_packages table but in this case it goes to the roles table.
Any pointers on how to go about this will be highly appreciated.
If you have a look at the Role source code you will this inside the __construct method:
public function __construct(array $attributes = [])
{
$attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');
parent::__construct($attributes);
$this->setTable(config('permission.table_names.roles')); // <-- HERE IS THE PROBLEM!
}
So, if you want that your SubscriptionPackage to write its records in the right table you have to override this behaviour like this:
public function __construct(array $attributes = [])
{
parent::__construct($attributes)
$this->setTable('your_table_name'); // <-- HERE THE SOLUTION!
}
I don't think you can. Spatie already have 5 tables and fetched data from those only. But still if you want to make the change you have make the changes with table and column name in the model

Populating parameters with default value using relationships in Laravel 5.2

Okay, so I have a question. I'm programming a really complex report and the interface uses Laravel 5.2. Now the thing is that, depending on certain conditions, the user does not always need all parameters to be filled. However, for simplicity purposes, I made it so that the report always receives the complete set of parameters no matter what. So I have three tables:
tblReportParam
ID
ParamName
DefaultValue
tblReportParamValue
ParamID
ReportID
Value
tblReport
ID
UserName
Now, I have a solution that works, but for some reason, it just feels like I should be able to make better use of models and relationships. I basically have just my models and controllers and solved the whole thing using SQL.
It feels somewhat close to this but not quite. So basically, you need to always load/save all parameters. If parameter x is actually defined by the user then you use his definition otherwise you go with the default defined in tblReportParam. Anyone has any idea how to do this?
EDIT:
Okay, so I checked Eddy's answer and tried to work it in our system, but another colleague of mine started implementing a many-to-many relationship between the tblReport and the tblReportParam table with the tblReportParamValue acting as the pivot so I'm having some difficulty adapting this solution for our system. Here's the two models:
class ReportParam extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblReportParam';
protected $primaryKey = 'ID';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['ID', 'NomParam', 'DefaultValue'];
public function renourapports()
{
return $this->belongsToMany('App\Report');
}
}
class Report extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblReport';
protected $primaryKey = 'ID';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['ID', 'NoEmploye', 'NoClient', 'NoPolice', 'DateCreation', 'DateModification', 'runable', 'DernierEditeur'];
public $timestamps = false;
public function params()
{
return $this->belongsToMany('App\ReportParam ', 'tblReportParamValue', 'ReportID', 'ParamID')->withPivot('Valeur');
}
}
Now this actually is a pretty neat solution, but it only works if the parameter is actually in the pivot table (i.e. the relationship actually exists). What we want is that for the parameters that aren't in the pivot table, we simply want their default value. Can Eddy's solution work in this case?
Using Eloquent models
class ReportParam extends Model
{
public function paramValue() {
return $this->hasOne('App\ReportParamValue', 'ParamID');
}
public function getDefaultValueAttribute($value) {
if ( $this->paramValue ) return $this->paramValue->Value; //relationship exists
return $this->DefaultValue;
}
}
$reportParam->value; // return the relationship value or the default value;
UPDATE
Now that tblReportParamValue is a pivot table you should redefine your relationships. In ReportParam model add
public function reports() {
return $this->belongsToMany('App\Report', 'tblReportParamValue', 'ParamID', 'ReportID')->withPivot('Value');
}
And in Report model, defined the opposite
public function params() {
return $this->belongsToMany('App\ReportParam', 'tblReportParamValue', 'ReportID', 'ParamID')->withPivot('Value');
}
Now getting the default value from ReportParam becomes too complicated because it will one ReportParam has Many Reports. So doing $reportParam->reports() will bring back every single report that uses that paramID in the pivot table. Therefore looking for a value would mean going through all the reports. We could avoid that by changind the function definition.
public function getDefaultValue($reportID) {
$reportValue = $this->reports()->wherePivot('ReportID', $reportID)->first();
return $reportValue ? $this->reportValue->Value : $this->DefaultValue;
}
//In Controller
$report = Report::find(1);
$reportParam = ReportParam::find(1);
$reportParam->getDefaultValue($report->ID);
Ok I think this might work. If it doesnt, I am really sorry, I don't know any better.

Polymorphic relationshoip in Laravel

I'm trying to understand polymorphic relationship in Laravel. I know how it works in principle, but the choice of wording in Laravel is not intuitive in this part. Given the exanple,
namespace App;
use Illuminate\Database\Eloquent\Model;
class Like extends Model
{
/**
* Get all of the owning likeable models.
*/
public function likeable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get all of the product's likes.
*/
public function likes()
{
return $this->morphMany('App\Like', 'likeable');
}
}
class Comment extends Model
{
/**
* Get all of the comment's likes.
*/
public function likes()
{
return $this->morphMany('App\Like', 'likeable');
}
}
How do yo put in plain English sentence morphTo for instance? It is "belongsto"? and morphmany, hasMany? going further,
$post = App\Post::find(1);
foreach ($post->likes as $like) {
//
}
$likeable = $like->likeable;
morphToMany and morphByMany
How do you describe in plain english?
A polymorphic relationship means an object can have a relationship to more than one type of object. This is determined by two fields in the database rather the typical one foreign key field you would normally see.
Using the code you included in your question any type of object extending the Model class can have a relationship with a Like object. So you could have Comments and Posts that can have Likes associated to them. In your likes table you may have rows where 'likable_type' = 'post' and 'likable_id' = 1 or 'likable_type' = 'comment' and 'likable_id' = 4 for example.

Laravel Removing Pivot data in many to many relationship

Not sure if I set this up correctly. In Laravel I'm creating two models with a many-to-may relationship
The models are Item and Tags. Each one contains a belongsTo to the other.
When I run a query like so:
Item::with('tags')->get();
It returns the collection of items, with each item containing a tags collection. However the each tag in the collection also contains pivot data which I don't need. Here it is in json format:
[{
"id":"49",
"slug":"test",
"order":"0","tags":[
{"id":"3","name":"Blah","pivot":{"item_id":"49","tag_id":"3"}},
{"id":"13","name":"Moo","pivot":{"item_id":"49","tag_id":"13"}}
]
}]
Is there anyway to prevent this data from getting at
you can just add the name of the field in the hidden part in your model like this:
protected $hidden = ['pivot'];
that's it , it works fine with me.
You have asked and you shall receive your answer. But first a few words to sum up the comment section. I personally don't know why you would want / need to do this. I understand if you want to hide it from the output but not selecting it from the DB really has no real benefit. Sure, less data will be transferred and the DB server has a tiny tiny bit less work to do, but you won't notice that in any way.
However it is possible. It's not very pretty though, since you have to override the belongsToMany class.
First, the new relation class:
class BelongsToManyPivotless extends BelongsToMany {
/**
* Hydrate the pivot table relationship on the models.
*
* #param array $models
* #return void
*/
protected function hydratePivotRelation(array $models)
{
// do nothing
}
/**
* Get the pivot columns for the relation.
*
* #return array
*/
protected function getAliasedPivotColumns()
{
return array();
}
}
As you can see this class is overriding two methods. hydratePivotRelation would normally create the pivot model and fill it with data. getAliasedPivotColumns would return an array of all columns to select from the pivot table.
Now we need to get this integrated into our model. I suggest you use a BaseModel class for this but it also works in the model directly.
class BaseModel extends Eloquent {
public function belongsToManyPivotless($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null){
if (is_null($relation))
{
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table))
{
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new BelongsToManyPivotless($query, $this, $table, $foreignKey, $otherKey, $relation);
}
}
I edited the comments out for brevity but otherwise the method is just like belongsToMany from Illuminate\Database\Eloquent\Model. Of course except the relation class that gets created. Here we use our own BelongsToManyPivotless.
And finally, this is how you use it:
class Item extends BaseModel {
public function tags(){
return $this->belongsToManyPivotless('Tag');
}
}
If you want to remove pivot data then you can use as protected $hidden = ['pivot']; #Amine_Dev suggested, so i have used it but it was not working for me,
but the problem really was that i was using it in wrong model so i want to give more detail in it that where to use it, so you guys don't struggle with the problem which i have struggled.
So if you are fetching the data as :
Item::with('tags')->get();
then you have to assign pivot to hidden array like below
But keep in mind that you have to define it in Tag model not in Item model
class Tag extends Model {
protected $hidden = ['pivot'];
}
Two possible ways to do this
1. using makeHidden method on resulting model
$items = Item::with('tags')->get();
return $items->makeHidden(['pivot_col1', 'pivot_col2']...)
2. using array_column function of PHP
$items = Item::with('tags')->get()->toArray();
return array_column($items, 'tags');

Laravel Eloquent Join

I'm fairly new with Laravel. I'm still trying to learn it. My question is:
I have 3 tables named
games
game_options
game_platforms
I have 3 Models for those tables
Game Model
class Game extends Eloquent
{
protected $table = 'games';
public function platforms()
{
return $this->hasManyThrough('GamePlatform','GameOptions','id','game_id');
}
}
GamePlatform Model
class GamePlatform extends Eloquent
{
protected $table = 'game_platform';
}
GameOption Model
class GameOptions extends Eloquent
{
protected $table = 'game_options';
}
So when I do
$game = Game::find(1)->platforms;
It only shows,
{"id":1,"platform_id":20,"game_id":1}
{"id":1,"platform_id":21,"game_id":1}
{"id":1,"platform_id":22,"game_id":1}
{"id":1,"platform_id":23,"game_id":1}
{"id":1,"platform_id":24,"game_id":1}
But I need game name and platform names with those ID's. The thing is, I want to do this with eloquent only. I could go with "DB" or oldschool SQL but I want to learn if this way is possible or not.
Also I'm looking for better documentation/books for laravel. Most of what I read were only introduce laravel or far too advanced for me.
I left a comment earlier about this but now I'm pretty sure it's the answer you're looking for: you should use belongsToMany rather than hasManyThrough. So first, might I suggest you rename your tables and models to follow Laravel's conventions (plural snake_case table names, singular snake_case alphabetical order pivot table names, singular StudlyCaps model names), that way you'll have the following situation:
Tables:
games
id
name
game_option
id
game_id
option_id
options
id
option
name
Now you can rewrite your models to conform to the new structure, and use a belongsToMany relationship too:
class Game extends Eloquent
{
public function platforms()
{
return $this->belongsToMany('Option');
}
}
class Option extends Eloquent
{
public function platforms()
{
return $this->belongsToMany('Game');
}
}
Note: you don't have to model the pivot table (game_option) unless you store extra data on the pivot.
Now you should be good to get all options for a given game:
$options = Game::find(1)->options;
Or if you need to get all platforms (though I am trying to infer meaning of your code here regarding options and platforms):
$platforms = Game::find(1)->options()->whereOption('platform')->get();
you can use the with method with eloquent
$game = Game::where('id',1)->with('platforms')->get();
Should return you the game and platforms
For documentation I would first start with the documentation provided (find it to be about 50% complete) and with the api everything else is covered
You would have to model your tables like:
**games**
id
name
**game_options**
id
game_id
name
**game_platform**
id
game_options_id
platform_id /* which i assume you get from a platform master table */
Then in your Game Class:
class Game extends Eloquent
{
protected $table = 'games';
public function platforms()
{
return $this->hasManyThrough('GamePlatform','GameOptions','game_id','game_options_id');
}
}
Now, this would be assuming that Game Platform belongs to Games through Game Options.

Categories