1

I am building an API using Resources. I have a Product (Chocolate Cake) that can be linked to a Property (Allergies) that has Properties_property (Glutten) that need to be shown in different orders for every product.

   product    <---[many-to-many] --->   properties
     ^                                      ^
     |                                      | 
[many-to-many]-->  properties_property  --[belongs-to] 
(pivot table)
  position

The tables look like this:

products:
id
name

product_property (first pivot table)
product_id
property_id

properties:
id
name 

properties_properties
id
name

product_properties_property (this is the pivot table witht the value) 
product_id
properties_property_id
position

The aspired JSON out put of https://localhost/product would be :

{
    "product": [{
        "product_id": 1,
        "name": "Choco Cake",
        "properties": [{
            "property_id": 1,
            "name": "Allergies",
            "properties_properties": [{
                "properties_property_id": 1,
                "name": "Gluten",
                "position": 1
            }]
        }]
    }]
}

The PropertiesProperty Model has a belongs to many relation in it like so:

public function products () {
  return $this->belongsToMany(Product::class)->withPivot('position');
}

I throw all the products in there from the routes/api.php

Route::get('/product', function () {
    return new ProductCollection(Product::all());
});

I have the following Resources: ProductResource, PropertyResource and PropertiesPropertyResource. The resources link to one another like so:

return [
 'product_id' => $this->product_id,
 'name' => $this->name,
 'properties' => ProductsPropertyResource::collection($this->properties)
];

In the Resource of Properties_property I would like to access the position field of the pivot table. How do I go about this? Idealy my App\Http\Resource\PropertiesPropertyResourse.php would look something like:

return [
  'properties_property_id' => $this->id,
  'name' => $this->name,
  'position' => $this->products->pivot->position
];

But this returns an "Property[pivot] does not exist on this collection"

I can write an sql in my PropertiesPropertyResource.php to get it like so:

return [
...
'position' => $this->products->where('id', $this->product_id)->first()->pivot->position
],

This does a lot of extra loading! The problem (I think) is that I want to access the data on the pivot table in the resource from the parent Resource (ProperiesProperty) not the child(Product) like you would usually do. Is there not a more Laravel-like method to do this?

REQUESTED INFO: There are about 230 connections in the pivot table currently, so this should not be a big deal.

UPDATE:

I found this question and I tried the solution:

'position' => $this->whenPivotLoaded ('product_properties_property', function () {
  return $this->pivot->position;
}),

but here the position key didn't even show up in the Json of the /product endpoint. I am starting to suspect that you need to fill these values with the SQL that you put in the controller or (in my case) the routes/api.php file.

St. Jan
  • 284
  • 3
  • 17
  • I would like to help, but I'm afraid need to debug on Your side to understand what's going on. Cause by documentation You wrote it correct. – num8er Feb 09 '23 at 10:24
  • thats not how it works in resource, checkout the documentation https://laravel.com/docs/9.x/eloquent-resources#conditional-pivot-information – silver Feb 09 '23 at 11:52
  • 1
    `$this->products->pivot->position`. This code cannot work; what are you expecting? You are asking for the code to give you MANY products and then asking for A single Pivot on a collection of things? The code will not know which of the MANY products you want to read the pivot on. – Nicklas Kevin Frank Feb 09 '23 at 12:02
  • @silver ooo I didn't see that whenpivotloaded before... I'll dig in to it. Thanks men! If you write up and example awnser I can vote you up. – St. Jan Feb 09 '23 at 12:55
  • 1
    @St.Jan its hard to give an example as Im not sure what you are trying to do, its confusing as you seem to be trying to do a `ProductResource` which you named a `ProductCollection`, then on your resource, Im lost as to why you are calling `$this->products` ? unless its a resource of other model where product is a relationship. – silver Feb 09 '23 at 13:33
  • are you sure this is what you need `ProductCollection(Product::all());`? and not `ProductResource::collection(Product::all());` because I feel like the later is what you trying to do – silver Feb 09 '23 at 13:45
  • @silver it confused me, but I want to get the position within the Property Resource. The reason for this is that in the implementation this is a loop with 2 many-to-many relations in it product->property->properties_property->product. The position value is in the pivot table between properties_property->product. – St. Jan Feb 09 '23 at 14:20
  • @St.Jan I think its better you post an example of your desired JSON output instead, the information you have provided is giving me more confusion than clarify lol I feel like you need to re-factor the entire thing – silver Feb 09 '23 at 14:39
  • @silver I will, but I am highly confused why the original question can't be answered though. In my opinion this would still solve my problems. I am not behind my computer and I didn't look in to the whenpivotloaded function yet. But I have high hopes for it. – St. Jan Feb 09 '23 at 14:43
  • I reread your question and discovered I missed some insides related to the Resource transformation. I'll be updating the answer in a moment, adding this. – Ricardo Vargas Feb 09 '23 at 15:22
  • @St.Jan Your desired JSON it's not standard and well formatted. Please, check this output: http://jsonblob.com/1073278999157030912. This is how I understand it's supposed to be your answer. Maybe changing the keys, but that's the main idea. – Ricardo Vargas Feb 09 '23 at 16:29
  • okay, apperently my explanation was very bad. Sorry. I adjusted it and how it makes more sense now. Hope I didn;t waist to much of your time, I should have been clearer. – St. Jan Feb 09 '23 at 22:20
  • @silver I checked this whenpivotLoaded out, I think it made it a lot more clear to me, but its not the solution. As I understand it now the problem is that I want to access a field in the pivot table from the parent resource of a many-to-many relationship, not the child. Probably you are right and it is a design error on my side. Thanks for your help anyway. – St. Jan Feb 10 '23 at 07:24
  • I think you are missing the `->withPivot` somewhere, that is why I was asking about the `product` relationship that I did not see – matiaslauriti Feb 10 '23 at 14:14
  • @St.Jan, I think you can simplify even more your database structure. You don't need two tables to do what you are trying to do. You can accomplish this by using only one property table but including an extra column; let's call it Parent Column. Then, create the relation base on your requirement. I will reply with my suggestion and implementation. – Ricardo Vargas Feb 11 '23 at 07:20
  • @RicardoVargas this part is not really clear to me: "You can accomplish this by using only one property table but including an extra column; let's call it Parent Column. Then, create the relation base on your requirement." I will wait for your answer, thx again! – St. Jan Feb 11 '23 at 11:47
  • Hi @St.Jan! I pushed and updated a few changes on the repository. Here's how it looks so far: https://jsonblob.com/1073278999157030912. I'm pending to include the position base in your requirement. Could you please check the repo and validate if this scheme works for you? Please check the database structure. – Ricardo Vargas Feb 14 '23 at 16:34
  • @RicardoVargas the Json misses the "position" thing, but the database looks good! – St. Jan Feb 16 '23 at 20:06

2 Answers2

3

Here is my answer, and I hope I can bring some light to your problem. I already published a GitHub repository with an example of all the code I write here. I added more information about replicating my scenario there in the README.md file.

The Question:

How to load Pivot data from a Belongs To Many relations?

My Answer and Recommendations:

Here is my interpretation of your database and its relations. You can review all the migrations into the repository. My intent here is not to define your structure as is but to help you understand how Laravel works. I hope after this, you can change, adapt or update your project values as needed. database_design

I expect to provide you with information about how to interact with your Pivot tables and how to manipulate them to hide and transform data by casting the pivot table properties. Laravel uses the relational Pivot table as an add-on. If you need this information, you need to attach the details of the required pivot data in the model.

First, you can define the relationships on any model, but I choose to present the pivot data on the Product model. Here's the model definition (All the dots represent hidden data to simplify the class definition, but you can review the whole model here):

<?php
...
class Product extends Model
{
...

    protected $with = ['properties']; // 6

    public function properties(): BelongsToMany // 1
    {
        return $this->belongsToMany(Property::class) // 2
            ->as('attributes') // 3
            ->using(ProductProperty::class) // 4
            ->withPivot('position'); // 5
    }
}

Let's explain what's happened here:

  1. The properties methods represent the relation between the Product and their Attributes.
  2. As Product is the parent object, you could represent this relationship with a hasMany relation, but it disallows you from relating the Pivot table. To connect the Pivot table, you must define this relation as a BelongsToMany relation. This unlocks all the other methods specified in the example.
  3. The as method allows you to rename the relation. By default, Laravel calls any pivot-loaded data by the pivot keyword. In this case, we rename this with the attributes value.
  4. The using method allows you to use a defined Pivot model to cast and modify the behavior of the data retrieved from the Pivot table.
  5. The withPivot method defines all the extra required fields from the Pivot relation. By default, Laravel loads the primary key columns from the related base models. In this case, I'm adding the position column from the Pivot table.
  6. The $with protected variable defines which relations should be loaded when you call the Product model. In this case, I'm adding all the Properties of a Product when you load any product.

Next, let's explain what happens on the ProductProperty Model (You can review the whole model here):

<?php
...
class ProductProperty extends Pivot // 1
{
    protected $casts = [ // 2
        'position' => 'integer',
    ];

    protected $hidden = [ // 3
        'product_id',
        'property_id',
    ];
}

Let's explain as before:

  1. As you can see, the ProductProperty class extends the Pivot class. This is REALLY important. This is how Laravel identifies this as a Pivot relation and not a base model. (And that's why you can use this in the Product model with the using method).
  2. The $cast protected property allows you to define column types to take advantage of data transformation. More info here: Eloquent: Mutators & Casting
  3. The $hidden protected property allows you to define the list of columns you don't want to present when you see a relationship that includes this Pivot table Model definition.

Ok. Now you know how to define the Pivot relation. But, How can I use it? Let's check the ProductController definition (You can review the whole controller here):

<?php
...
class ProductController extends Controller
{
    public function index(Request $request)
    {
        $products = Product::all(); // 1
        return new ProductCollection($products); // 2
    }
}

// Returned data:
/**
{
   "data":[
      {
         "id":1,
         "name":"Costa Rica",
         "properties":[
            {
               "id":1,
               "name":"DeepSkyBlue",
               "attributes":{ // By default Laravel use 'pivot' here, but we replace this with the 'attributes' name.
                  "position":8
               }
            },
            {
               "id":2,
               "name":"Azure",
               "attributes":{
                  "position":8
               }
            },
            {
               "id":3,
               "name":"LavenderBlush",
               "attributes":{
                  "position":8
               }
            }
         ]
      },
    ... // Sample of 1 Product with 3 Properties
   ]
}
 */

What's happening here:

  1. I'm calling all the existing products from the Database. As we have the $with property defined on the Product model, all Products are returned with their Properties.
  2. I'm returning the Products wrapped on API Resources to allow the transformation of the Endpoint structure if needed in the future. Here is more information: Eloquent: API Resources

Well, that's it! Here I covered the basics as simply as I can. I hope this information could help.


For this example, I used Laravel Blueprint to accelerate the project scaffolding. You can read more about it here: Laravel Blueprint

Ricardo Vargas
  • 721
  • 4
  • 14
  • 1
    Thx so much for all your effort, I can see you really put in the work. Only thing is that I am asking how to do this in a resource. You are only linking to the description of where I can find about resources (a page I have been reading and reading to find my answer). My question is: “What is the Laravel-way to access the data in the pivot table of a many-to-many relationship from a resource?” – St. Jan Feb 09 '23 at 21:58
  • Sorry for all the time you invested in this, I adjusted the question and hope this is clearer now. – St. Jan Feb 09 '23 at 22:21
  • 1
    I think I located the origin of the problem. Since it is a circular connection and all I need it the data of the pivot field, position, not the data from product again I am trying to access the data of the pivot table from the child(PropertiesProperty) of the many-to-many relationship not the parent as you would usually do. – St. Jan Feb 10 '23 at 07:38
  • Hello @St.Jan, So, sorry I couldn't reply to you yesterday. Let me check the updates. I want to confirm if you could validate this output: https://jsonblob.com/1073278999157030912. I want to update the answer after confirming If the production it's right. BTW, Please download the repository; I am pushing all changes there, including the Resource changes. – Ricardo Vargas Feb 11 '23 at 07:10
  • I checked your repro through the web interface, not always on a machine with shell/git access ;-) Really thank you for you work and time! really nice. You can check my answer and if you have any comments or improvements on it I would love to hear about them! – St. Jan Feb 11 '23 at 11:45
0

You can't reach the pivot table product_properties_property in the PropertiesPropertyResource since it is not yet known there which relation is loaded. (as @Nicklas Kevin Frank pointed out)

Best solution in this case would be to rename your pivot to something like "attributes" in the model(thanks @Ricardo Vargas for that idea):

public function properties_properties()
{
   return $this->belongsToMany(Product::class) 
       ->as('attributes') 
       ->withPivot('position'); 
}

Then make a PropertiesPropertyAttributeResource that returns the sought after attribute(s):

class PropertiesPropertyAttributeResource extends JsonResource
{
    public function toArray($request)
    {
      return [
          'position' => $this->attributes->position,
        ];
    }
}

And load this in the PropertiesPropertyResources:

class ProductsPropertiesPropertyResource extends JsonResource
{
    public function toArray($request)
    {
      return [
        'properties_property_id' => $this->id,
        'name' => $this->name,
        'attributes' => 
  PropertiesPropertyAttributeResource::collection(
    $this->products
      ->where('id',
        //this is a hack i am using to get access to the current product_id
        $request->get('my_name')['product_id])
      ];
    }
}

This way the pivot table values will show up in your JSON like so:

{
    "product": [{
        "product_id": 1,
        "name": "Choco Cake",
        "properties": [{
            "property_id": 1,
            "name": "Allergies",
            "properties_properties": [{
                "properties_property_id": 1,
                "name": "Gluten",
                "attributes" [{
                    "position": 1
                }]
            }]
        }]
    }]
}

NOTE: In the case I asked about, where there are 2 many-to-many relationships between the source and eventual record, you need to create and Eloquent object that contains the right information for generating the output using the API Resources. There is no default way for _PropertiesProperty to know what Product originated the request. My hack displayed here only works on small data sets, as soon as the collection of $this->products becomes to big this will be a major memory eater.

St. Jan
  • 284
  • 3
  • 17