laravel-json-api / laravel

JSON:API for Laravel applications
MIT License
541 stars 41 forks source link

[Question] How to include nested polymorphic relationships #255

Closed CLL-GTA closed 1 year ago

CLL-GTA commented 1 year ago

I have a uses case that I don't think it's too unrealistic but I can't figure out how to set it up.

First of all, apologies for raising this as an issue, it's most likely me not understanding the package, but I wouldn't know where else to ask. StackOverflow is too generic for this IMO. Also, there is a similar issue already raised (#222) but I think my case is different because it's a nested relationship. And it may be related to #59 but I don't know for sure.

Anyhow, this is my use case.

I have a few different "type" of users in my system, let's say clients and suppliers, for example. They all have a 1:1 relationship with a person, where we store the personal details like first and last name. Each person can have multiple addresses (but one address is associated with one person only).

But I also have organisations, which can have multiple addresses, which is why I have a polymorphic relationship.

So, in Laravel/Eloquent terms

class Client extend Models
{
  \\...

  public function person(): BelongsTo
  {
    return $this->belongsTo(Person::class);
  }
}

class Person extends Model
{
  \\...

  public function addresses(): MorphMany
  {
    return $this->morphMany(Address::class, 'addressable');
  }
}

class Organisation extends Model
{
  \\...

  public function addresses(): MorphMany
  {
    return $this->morphMany(Address::class, 'addressable');
  }
}

What I would like is to hide the fact of how the personal details and addresses are stored. If I call GET /api/v1/clients I should get their personal details (even if they are stored in the people table). And similarly, if I want their addresses to, I want to be able to call GET /api/v1/clients?include=addresses, and get something like this:

{
  "data": [
    {
        "type": "clients",
        "id": "1",
        "attributes": {
          "firstName": "Paul",
          "lastName": "Winston"
        }
        "relationships": {
          "address": {
            "links": {
              "related": "http://ocalhost/api/v1/clients/1/addresses",
              "self": "http://localhost/api/v1/clients/1/relationships/addresses"
            },
            "data": [
              {
                "type": "addresses",
                "id": "1"
              },
              {
                "type": "addresses",
                "id": "2"
              }
            ]
          }
        }
    }
  ],
  "included": [
    {
      "type": "addresses",
      "id": "1",
      "attributes": {
        "name": "Home"
      }
    },
    {
      "type": "addresses",
      "id": "2",
      "attributes": {
        "name": "Work"
      }
    }
  ]
}

My schema is

class ClientSchema extends Schema
{
  public static string $model = Client::class;

  public function fields(): array
  {
    return [
      ID::make()->uuid(),
      Str::make('firstName')->on('person'),
      Str::make('lastName')->on('person'),
    ];
  }
}

This works well for the personal detail, i.e. GET /api/v1/clients returns the correct data. But I cannot find the right way to define my relationship for the address. I've tried many things, probably going in circle too, so I'm a bit lost.

I could maybe have it GET /api/v1/clients?include=person.addresses but to me the fact that the addresses are associated with people (rather than clients) is an implementation issue, which should be transparent to my API consumer (i.e. they want the client's addresses).

Can anybody help?

lindyhopchris commented 1 year ago

So the starting point with this is to forget you're doing this for Laravel JSON:API, and instead think in terms of Eloquent.

How would you load this data in Eloquent? If you can write the Eloquent query that produces the result you want, then this package should be able to do it (and if it can't, then something is potentially missing). However, if you can't load it in Eloquent in the way that you want, that's going to make it exceptionally difficult for this package to do it. Not least because every relationship has to be supported for eager loading, so that we can avoid N+1 problems.

CLL-GTA commented 1 year ago

That's a good point @lindyhopchris

I would probably write something like

$client = Client::query()->with(['person.addresses'])->where('id', 1)->get()

and that would give something like

Client {
  id: 1,
  person_id: 1,
  person: Person {
    id: 1,
    first_name: "Paul",
    last_name: "Winston",
    addresses: Collection {
      all: [
        Address: {
          id: 1,
          name: "home
        },
        Address: {
          id: 2,
          name: "work
        }
      ]
    }
  }
}

So, yeah, it's not in the format I want it.

I'm now thinking that maybe I should have a api/v1/people endpoint with a filter for clients, something like api/v1/people?filter[type][]=client.

CLL-GTA commented 1 year ago

I have ended up creating a non-eloquent resource as explained in the documentation, where the repository class you need to create actually uses the Eloquent query builder.