Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom type resolution when creating polymorphic relationships #72

Closed
bertramakers opened this issue Jul 4, 2023 · 5 comments
Closed
Labels
enhancement New feature or request
Milestone

Comments

@bertramakers
Copy link
Contributor

bertramakers commented Jul 4, 2023

As mentioned in #68 (comment), I use simple stdClass objects to represent my data models inside the Resource classes.

I'm now trying to create a polymorphic ToOne relationship like this:

ToOne::make('example')
    ->type([
        'type1',
        'type2'
    ])
     ->required()
     ->writable()
     ->includable(),

The example types type1 and type2 are also registered as resources on the JsonApi class.

Now when I try to create a new instance of a resource with this relationship, I get:

No resource type defined to represent model stdClass

(Thrown here: https://github.com/tobyzerner/json-api-server/blob/main/src/Serializer.php#L156-L158)

It seems to me that the Serializer is trying to determine the resource class related to the included model, but because it's always stdClass it cannot differentiate between the various models being returned.

Taking a closer look at the documentation, I see that I should also define the model classes as keys in the type() call: https://tobyzerner.github.io/json-api-server/relationships.html#polymorphic-relationships

However, since all my models are plain stdClass objects, this is obviously not possible.

Is there a workaround possible, aside from creating a class for every one of my models? As mentioned in the previously linked issue I didn't do that because we already have classes for all of our models, but they're not usable in newModel() because they have some required properties that are not available there. And introducing a second class for every model is a lot of extra work.

Looking at the Serializer::addIncluded() that calls resourceForModel(), I see that $model is also available there and it already contains the type as a property (at least in our case). Edit: This was because of how we used to implement our models.

@tobyzerner
Copy link
Owner

How about if I add the ability to define a callback within a relationship definition, that resolves a related model to its resource type. Something like this:

ToOne::make('example')
    ->type(['type1', 'type2'])
    ->resolve(fn($model) => $model->foo ? 'type1' : 'type2'),

@bertramakers
Copy link
Contributor Author

I found a solution that is a bit hackish but works in our case. Instead of using stdClass for our models inside the Resource classes, we use the Resource classes themselves as models. It's a bit dirty because it combines the responsibilities of providing the methods to create, update, list and find instances of the resource together with holding properties of one specific resource, but because we use a custom base class it is all very "behind-the-scenes" stuff that does not get in the way. And it means that we don't have to create model classes specifically to use inside the Resource classes, and we also don't need to use stdClass which resulted in this issue.

I think your suggestion is good though, but maybe wait and see if anyone else runs into this issue?

@tobyzerner tobyzerner changed the title Error when creating a resource that has a polymorphic relationship Allow custom type resolution when creating polymorphic relationships Aug 19, 2023
@tobyzerner tobyzerner added the enhancement New feature or request label Aug 19, 2023
@tobyzerner
Copy link
Owner

tobyzerner commented Nov 29, 2023

I've got a more comprehensive solution for this one coming soon. Also will enable heterogeneous collection URLs so eg. you can have GET /projects which returns resources of different types (internalProjects, externalProjects).

@tobyzerner tobyzerner added this to the v1.0 milestone Nov 29, 2023
@tobyzerner
Copy link
Owner

tobyzerner commented Nov 30, 2023

With the recent addition of Collections there are now two solutions for this.

  1. Implement a Collection for the polymorphic relationship. A Collection describes which resource types are available and also implements logic to resolve a model to its resource type.
use Tobyz\JsonApiServer\Resource\CollectionInterface;

class FooCollection implements CollectionInterface
{
    public function name(): string
    {
        return 'foo';
    }

    public function resources(): array
    {
        return ['type1', 'type2'];
    }

    public function resource(object $model, Context $context): ?string
    {
        return $model->foo ? 'type1' : 'type2';
    }

    public function endpoints(): array
    {
        return [];
    }
}

$api->collection(new FooCollection());
ToOne::make('example')
     ->collection('foo')
     ->required()
     ->writable()
     ->includable()
  1. Every class that extends the base Resource class is actually also a Collection which resolves to itself. You can customise the resolution logic in each of your resources to return null if a given model does NOT correspond to the resource. Then, provide an array of the resources (collections) when defining the relationship - the model will resolve to the first non-null resource.
use Tobyz\JsonApiServer\Resource\Resource;

class Type1Resource extends Resource
{
    // ...

    public function resource(object $model, Context $context): ?string
    {
        return $model->foo ? $this->type() : null;
    }
}

class Type2Resource extends Resource
{
    // ...

    public function resource(object $model, Context $context): ?string
    {
        return $model->bar ? $this->type() : null;
    }
}
ToOne::make('example')
     ->type(['type1', 'type2']) // "type" is now just an alias for "collection"
     ->required()
     ->writable()
     ->includable()

@tobyzerner
Copy link
Owner

Docs updated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants