Laravel Polymorphic Many-to-Many relationship pivot table with relationship to another Model - laravel

I have the following table structure as shown in the diagram:
Briefly, it is composed of several many-to-many polymorphic relationships as described:
many resources can have many sources and the pivot table sourceables contains catalog_number and lot_number information to make each row in the pivot table unique. Many resources could also come from the same source or from different sources, differentiated by the catalog number and lot number on the pivot table.
many resources can also have many publications attached to it, through the publicationables table with notes on the pivot table
a resource's source could also be described in many publications.
My questions:
Since the resource's source is differentiated by the pivot table sourceables how should I save the relationship between the pivot rows of sourceables to the publications?
Can you have a custom intermediate table models between both sourceables and 'publicationables' to link to the publications?
How to retrieve a resource with all it's publications and also with the sources with all corresponding publications?

Here is my answer and I hope that I can bring some light to your problem. I already publish a GitHub repository with an example of all the code I write here. I add more information about how to replicate my scenario there.
The Database and The Relations
Here is my interpretation of the Database and its relations. You can check all the Migrations on the repository.
The Solution
Question 1:
How should I save the relationship between the pivot rows of sourceable to the publications?
Answer:
Before proceeding with the code example, I would like to explain some important concepts to understand. I'm going to use the expression tag to refer to the identifier or index Morph Relations used to relate models.
The way this works, it's by assigning the tag to any Model you want to add into a relation. Any model using these tags can be store in the Morph Pivot Table. Laravel uses the _"modelable"type column to filter the call on the relations storing the Model Name. You can "tag" your Model with a Relation creating a method into the Model that returns the morphToMany relation function.
For this specific case, here's how to proceed:
In your Resource Model, you have two methods, one related to the sourceable index and the other with the publicationable tag using morphToMany in return.
Here's how it's look the Resource Model (./app/Models/Resource.php):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Resource extends Model
{
use HasFactory;
protected $guarded = [];
public function publications()
{
return $this->morphToMany(Publication::class, 'publicationable')->withPivot('notes');
}
public function sources()
{
return $this->morphToMany(Source::class, 'sourceable')->withPivot(['catalog_number', 'lot_number']);
}
}
In your Publication Model, you have two methods, one related to the sourceable index and the other with the inverse relation with the Resource Method to the publicationable tag using morphedByMany in return.
Here's how it looks the Publication Model (./app/Models/Publication.php):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Publication extends Model
{
use HasFactory;
protected $guarded = [];
public function sources()
{
return $this->morphToMany(Source::class, 'sourceable')->withPivot(['catalog_number', 'lot_number']);
}
public function resources()
{
return $this->morphedByMany(Resource::class, 'publicationable');
}
}
With this, you can be able to accomplish your goal of relating Publications with Resources and Sources.
Question 2: Can you have an intermediate table between both sourceable and publicationable to link to the publications?
Answer:
No, you don't need to. You can use the sourceables table to accomplish this. You can always relate a Source with ANY model by creating the method that returns the morphToMany relation to the Source model. These what we do with Publications on Question 1.
Question 3: How to retrieve a resource with all its publications and the sources with all corresponding publications?
Answer:
I think Eloquent it's my favorite feature on the whole Laravel Framework. This the cherry on the cake with all we do on the Model definition.
If you check the Resource and Publication Model definition again, we add a withPivot() method with the related field we want to include on any call we do to the relation with eloquent. This method made it possible to read custom values from the Pivot table.
IMPORTANT: For this example, I'm implicitly adding the pivot values because I don't declare those columns as NULL on the migrations.
To relate (Store on the Pivot table) a publication with a resource using the relation, you just need to:
(Using artisan tinker)
Psy Shell v0.10.8 (PHP 8.0.6 — CLI) by Justin Hileman
>>> $publication = \App\Models\Publication::find(5)
>>> $resource = \App\Models\Resource::find(19)
>>> $resource->publications()->attach($publication, ["notes" => "Eureka!"]);
### Adding another Publication
>>> $publication = \App\Models\Publication::find(10)
>>> $resource->publications()->attach($publication, ["notes" => "Eureka 2!"]);
(Using a Controller)
use App\Models\Resource;
use App\Models\Publication;
...
$id_resource = 1; // This is the Resource Id you want to reach.
$id_publication = 10; // This is the Resource Id you want to reach.
$resource = Resource::find($id_resource);
$publication = Publication::find($id_publication);
$pivotData = [ "notes" => "Eureka!" ];
$resource->publications()->attach($publication, $pivotData);
To retrieve all publications from a resource, you just need to:
(Using artisan tinker)
Psy Shell v0.10.8 (PHP 8.0.6 — CLI) by Justin Hileman
>>> $resource = \App\Models\Publication::find(5)
>>> $resource->publications()->get();
Easy right? :) Eloquent POWER!
(Using a Controller)
use App\Models\Resource;
...
$id_resource = 1; // This is the Resource Id you want to reach.
$resource = Resource::find($id_resource);
$resource->publications()->get();
Just in case of any doubt, this is how you can store and retrieve from all the models:
(Using a Controller)
use App\Models\Publication;
use App\Models\Resource;
use App\Models\Source;
...
... Method ...
$id_publication = 1;
$id_resource = 1;
$id_source = 1;
$publication = Publication::find($id_resource);
$resource = Resource::find($id_resource);
$source = Source::find($id_resource);
$publicationPivotColumns = [
"notes" => "This is a note...",
];
$sourcePivotColumns = [
"catalog_number" => 100,
"lot_number" => 4903,
];
// Storing Data
// Attach (Store in the publicationables table) a Publication to a Resource
$resource->publications()->attach($publication, $publicationPivotColumns);
// Attach (Store in the sourceables table) a Source to a Resource
$resource->sources()->attach($source, $sourcePivotColumns);
// Attach (Store in the sourceables table) a Source to a Publication
$publication->sources()->attach($source, $sourcePivotColumns);
// Retraiving Data
// Get all Sources from a Resource
$resource->sources()->get();
// Get all Publications from a Resource
$resource->publications()->get();
// Get all Sources from a Publication
$publication->sources()->get();

Related

Laravel: Saving a relationship when instanciating an Eloquent model rises this SQL error: "Integrity constraint violation"

Summary
Context and Needs
Minimal, Testable and Executable sources (with instructions for testing)
Actual Results and Expected Results
What I've tried
The Question
Context and Needs
The relationship between both Eloquent models GalleryImage and GalleryGroup is: GalleryImage * <-> 1 GalleryGroup. I want to save an instance of GalleryGroup, then of GalleryImage.
Minimal, Testable and Executable sources
Instructions to test
I wanted to show you how to test my code in the case you really want to do it ;-) . However, I think you don't actually need to test. Indeed, the code is very simple. By reading it, if you know more than me Laravel, maybe you will find the problem and be able to bring me some help. I let you reading the following contents but I think you'll agree with me.
Create the tables for GalleryGroup and GalleryImage (out of topic). The fields to create and the name of the tables are contained in the following sources.
Copy/Paste the Eloquent models and the script that instanciates them and tries to save them in DB.
Creates the routes of your choice to run the script and then, run the script (ie.: access the Web page or use a REST client)
The Eloquent models
-- GalleryGroup.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class GalleryGroup extends Model
{
use HasFactory;
protected $primaryKey = 'group_id';
private $name;
public function images() {
return $this->hasMany(GalleryImage::class);
}
}
-- GalleryImage.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class GalleryImage extends Model
{
use HasFactory;
protected $primaryKey = 'image_id';
public function group() {
return $this->hasOne(GalleryGroup::class, 'group_id', 'image_id');
}
}
Instanciations and concretization of the relationship
The Eloquent model GalleryGroup is instanciated and saved in db; then, the Eloquent model GalleryImage is instanciated and should be saved in db:
$img_group = new GalleryGroup();
$img_group->name = 'foobar';
$img_group->save();
$image = new GalleryImage();
var_dump($img_group->group_id); // It exists and it's not empty
$image->group()->save($img_group);
$image->save();
Actual Results and Expected Results
The last line is never executed because this error is raised at the line $image->group()->save($img_group);:
Illuminate\Database\QueryException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'group_id' cannot be null (SQL: update gallery_groups set group_id = ?, gallery_groups.updated_at = 2021-01-09 10:16:44 where group_id = 24) in file /var/www/html/api/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 671
I don't understand why it tries to update the group entry, and I don't understand why group_id is NULL or empty, because $img_group actually has a non-empty group_id (cf.: the line var_dump($img_group->group_id);).
The actual results are: 1) the model GalleryGroup is correctly instanciated and correctly saved in db and 2) the model GalleryImage is correctly instanciated and not saved in db because of the above SQL error is raised.
The expected results are: 1) the model GalleryGroup is correctly instanciated and correctly saved in db and 2) the model GalleryImage is correctly instanciated and saved in db.
What I've tried
I've tried to var_dump several times several variables but did not found any relevant information to help debugging this issue.
I've read and re-read the docs https://laravel.com/docs/8.x/eloquent-relationships#the-save-method and https://laravel.com/docs/8.x/eloquent#primary-keys but did not found any relevant information to help debugging this issue.
The Question
Why is this error raised and how to fix it?
One of these relationships needs to be a belongsTo as one of these tables has the foreign key on it that relates to the other table. I would assume a GalleryImage belongs to a GalleryGroup:
GalleryGroup
images
hasMany GalleryImage
GalleryImage
gallery
belongsTo GalleryGroup
Once those are setup correctly you should be able to do this to save the relationship:
$img_group->images()->save($image);

hasManyThrough with an intermediate pivot table

Description:
My application is structured as follows:
Property has a Many-Many relationship with Manager, and a Unit has a One-Many relationship with a Property, i.e. A manager can manage multiple properties, one property can have multiple manager accounts and one property can have multiple units.
I would like to have a HasManyThrough relationship on the manager to get all his units, so ideally it would look something like: $manager->units instead of having through loop through each property and call $property->units on it. Is this possible with the current version of laravel?
Tables:
managers:
id
properties:
id
managers_properties:
manager_id
property_id
units:
id
property_id
Eloquent currently does not have methods for chained relations, other than the hasManyThrough, that is only applicable to 2 chained hasMany relations. You should create your own implementation to fetch the related resources. The simplest way is to define an accessor on the Manager model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;
/**
* #property-read Collection|\App\Models\Property[] $properties
* #property-read Collection|\App\Models\Unit[] $units
*/
class Manager extends Model
{
public function properties(): BelongsToMany
{
return $this->belongsToMany(Property::class);
}
public function getUnitsAttribute(): Collection
{
return $this->properties
->pluck('units')
->flatten(1)
->unique('id')
->sortBy('id');
}
}
You should now be able to access the related units with $manager->units assuming $manager instanceof App\Models\Manager.
Note
Calling $manager->units does perform at most n + 1 database queries: 1 for fetching n related properties, and another n for fetching related units for each returned property. "At most" because the resources might have been loaded already because of previous calls to the accessor.
Note
Calling $manager->units returns you a Collection of Unit models, a format that's equivalent to what you'd get from the magic accessor of a to-many relationship method. However getUnitsAttribute() is not an actual relationship method (it does not return a relationship object), so it can not be treated as such, whereas Manager::properties() can be.

Eloquent join with where clause

I have problems to build a relationship with eloquent.
I have two models created, Spielplan and Verein. In model Spielplan I have the fields Team_ID and Spiel_ID. In model Verein I have the field V_ID and Name. Now I need to join this two tables about Team_ID = V_ID.
This is my model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Spielplan extends Model
{
protected $table = 'Spielplan';
public function vereinFunction(){
return $this->hasOne('App\Verein', 'V_ID');
}
}
And this is a function in my web route where I want to get Spiel_ID and Name.
Route::get('test', function(){
$spieleT = App\Spielplan::where('Spiel_ID', '=', 30)->get();
foreach($spieleT as $da){
echo $da->Spiel_ID;
echo $da->vereinFunction->Name;
}
});
The first echo works and I get back Spiel_ID but the second echo gives back ErrorException Trying to get property of non-object.
What is wrong with my code?
Try editing this line:
$spieleT = App\Spielplan::with('vereInFunction')->where('Spiel_ID', '=', 30)->get();.
The with() allows you to fetch the association at the time you use get(). After using get(), you're working with a collection, and can't query the database again.
Try specifying the model primary key as a third argument, because if not, Laravel will assume it is named id, which is not the case.
Allow me to suggest you something: I used to name the tables and fields like you do (in the days I use Codeigniter) but since I started using Laravel around three years ago, I follow Laravel convention (which is recommended, but not imposed). I now name the tables in lowercase, (snakecase) plural, table fields also snakecasm lowercase. Models singular, camelcase similar corresponding table, relation function names as related model, being singular if relation is to one, plural if to many, etc. The advantage of this is among other reflected in model relationship declaration, which is a lot simpler and easier to define.
For instance (only as demonstration of stated above),
tables (with relation one to many:
managers (primarykey: id, name, ......)
technicians (primary key: id, foreingkey: manager_id (related table name in singular plus underscore plus id), name, .....)
models:
Manager:
/* relationships */
public function technicians () // see name as related table, plural due to as many relationship)
{
return $this->hasMany(Technician::class); // as naming convention has followed, you don't need to specify any extra parameters;
}
Techician:
/* relationship */
public function manager() // named as related table, singular due to to one relationship
{
$this->belongsToOne(Manager::class); // again, as naming convention has followed, you don't need to specify any extra parameters;
}
Therefore you can do this:
$manager::find(1);
echo $manager->technicians->first()->name,
or
foreach ($manager->technicians as $technician) {
echo $technician->name;
}
as well as:
$technician->manager->name;
Remember, a proper model relationship definition will save a lot of headache along the way, like the one you have
Hope this help in anyway

Laravel assumes the database table is the plural form of the model name

By default, Laravel is assuming that the database table is the plural form of the model name. But what if my table name is "news" and i still want to use this feature? should i change it to "newses" or should i use "new" for the model name?
You may specify a custom table by defining a table property on your model as below
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'my_flights';
}
Ref:https://laravel.com/docs/5.1/eloquent
If you have a model ending with the letter 's', it will keep the table name the same. In your case, your model will name your table news by default.
If you want to use another name though, you can use:
protected $table = 'tablename';
inside of your model.
EDIT: I tested this in my application. I made a model named News. Then I made a new instance of News and retrieved the table name:
$news = new News();
dd($news->getTable());
It returns: news
Inside your eloquent model you have to define table name. For example if my model is named user and table in database is named user_of_application then i do it this way
class user extends Model
{
protected $table = 'user_of_application';
}
Laravel uses a "standard set" rule that defines:
A Model is a single instance of a record
A Collection is one or more records
Therefore it assumes that a table is a collection of records.
The nomenclature has a problem when it clashes with various features / gotchas of human languages. For example, what if I have a Model called Sheep? That does mean my Database Table should be called "Sheeps"?
It's up to the developer to avoid "Gollum/Smeagol" syntax. Indeed, you wouldn't want a table called "Newses" as much I'd like to end up with a table called "Sheeps".
Ultimately, I construct Migrations with:
sudo php artisan make:migration create_sheep_table --create=sheep
As for Models, you'll notice in the documentation that they have a different table name for "Flights" called "my_flights"
https://laravel.com/docs/master/eloquent#defining-models
Again, it's up to the developer / DB manager to make decisions on naming conventions that make sense in an application context.

what to do with an inverted polymorphic relation?

I have been trying to get my head around these polymorphic relationships all day. I might be over complicating/thinking it but. Can Laravel handle inverse polymorphic relationships? I have a registration flow that can have two types of field Models- normal field and customField.
When I loop through all the fields available it could pull the attributes from either NormalField or CustomField.
<?php
foreach($registrationFlow->fields->get() as $field)
{
echo $field->name; // could be custom field or could be normal field
}
?>
My difficulty is that, the example given in the docs works if you want to assign a photo to either staff or orders, but i want to assign either a customField or a normalField to a registrationFlow
*Edit
If you follow the example for the polymorphic many to many relationship, The tag class contains posts and videos- while i would want just a simple fields() method that relates to customField or normalField dependent on the type
First of all, you should take a look at the updated docs for Laravel 5.1: https://laravel.com/docs/5.1/eloquent-relationships#polymorphic-relations.
I think the difficulty with the example they provide is that the relationship between Photo and Staff/Product are "has-a" relationships, whereas you are trying to model an "is-a" relationship. However, you can model "is-a" essentially the same way. Take a look at this article: http://richardbagshaw.co.uk/laravel-user-types-and-polymorphic-relationships/.
Basically, the strategy is to define a generic model (and a generic table), perhaps in your case Field, that relates to your RegistrationFlow. You then have two subtype models, NormalField and CustomField, that have one-to-one relationships with Field. (there's your "is-a"). Thus, RegistrationFlow is indirectly related to your field subtypes.
Polymorphism comes in when you want to access the specific subtypes:
class Field extends Model {
public function fieldable()
{
return $this->morphTo();
}
}
Your base field table should have fieldable_id and fieldable_type columns defined (see the Eloquent docs).
You can then add methods to NormalField and CustomField that let you access the base model (your "inverse relationship"):
class NormalField {
public function field()
{
return $this->morphOne('Field', 'fieldable');
}
}
class CustomField {
public function field()
{
return $this->morphOne('Field', 'fieldable');
}
}
Usage:
$field = Field::find(1);
// Gets the specific subtype
$fieldable = $field->fieldable;

Resources