Repository: Lomkit/laravel-rest-api (php)

Issue:
morphTo relations with multiple types only ever return data from the related Model from fields defined in the first resource type
### Laravel Rest Api Version

2.3.3

### Laravel Version

10.35.0

### PHP Version

8.2.8

### Database Driver & Version

Postgres

### Description

When you define a `MorphTo` relation in a Resource, and you provide multiple types when performing a search and "including" the morphTo relation, the relationship data that is passed back via the API is returned only taking into account the first type Resource passed into the `MorphTo::make` method.

For example. If you have a MorphTo relationship called `subscribable` with two possible types. This is how you would register it:
```php
public function relations(RestRequest $request): array
    {
        return [
            MorphTo::make('subscribable', [
                ProspectResource::class,
                StudentResource::class,
            ]),
        ];
    }
```

If you call the search endpoint and include the `subscribable` relationship like so:
URL: http://localhost/api/v1/subscriptions/search
Body:
```json
{
    "search": {
        "includes": [
            {
                "relation": "subscribable"
            }
        ]
    }
}
```

The attributes that are returned for the `subscribable` relationship are the fields described in the first relationship. In this case `ProspectResource`.

If both Models/Resources have common columns that you will see data coming back for both relations. But you will see fields defined in the first Resource type even if it is not present in the second Resource type.
Any fields defined in the second resource type are ignored and not returned.

This probably has something to do with `Lomkit\Rest\Query\Traits\PerformSearch@include` and that on line 195 it only ever returns one Resource. And `Lomkit\Rest\Relations\Relation@resource` explicitly calls `$this->types[0]`

The expected behavior should probably be that what is returned is based on the fields defined in the Resource of what type the Model is.

### Steps To Reproduce

1. Create a MorphTo relation with multiple possible related Models
2. Ensure that the related Models have slightly different or completely different columns
3. Create API Resources for these Models and define the MorphTo relationship and provide both related Model Resources and types.
4. Perform a search and request that the MorphTo relation be included.
5. You should see only fields described in the first Relation type returned.

You can see an example of us doing so here: https://github.com/canyongbs/advisingapp/commit/6cb8e8d4ee00e55b996951f14c3381248de13bbf

Pull request:
Fix/morph to relation
closes #86 

Patch:
diff --git a/src/Concerns/Resource/Relationable.php b/src/Concerns/Resource/Relationable.php
index 8d0abc8..3b06b74 100644
--- a/src/Concerns/Resource/Relationable.php
+++ b/src/Concerns/Resource/Relationable.php
@@ -34,18 +34,6 @@ trait Relationable
         return $relation;
     }
 
-    /**
-     * Get the resource associated with a relation by name.
-     *
-     * @param string $name
-     *
-     * @return \Lomkit\Rest\Http\Resource
-     */
-    public function relationResource($name)
-    {
-        return $this->relation($name)?->resource();
-    }
-
     /**
      * The calculated relations if already done in this request.
      *
diff --git a/src/Query/Traits/PerformSearch.php b/src/Query/Traits/PerformSearch.php
index 4fd4b80..e05a9ff 100644
--- a/src/Query/Traits/PerformSearch.php
+++ b/src/Query/Traits/PerformSearch.php
@@ -192,7 +192,7 @@ trait PerformSearch
     public function include($include)
     {
         return $this->queryBuilder->with($include['relation'], function (Relation $query) use ($include) {
-            $resource = $this->resource->relationResource($include['relation']);
+            $resource = $this->resource->relation($include['relation'])?->resource();
 
             $queryBuilder = $this->newQueryBuilder(['resource' => $resource, 'query' => $query]);
 
@@ -220,7 +220,7 @@ trait PerformSearch
     public function aggregate($aggregate)
     {
         return $this->queryBuilder->withAggregate([$aggregate['relation'] => function (Builder $query) use ($aggregate) {
-            $resource = $this->resource->relationResource($aggregate['relation']);
+            $resource = $this->resource->relation($aggregate['relation'])?->resource();
 
             $queryBuilder = $this->newQueryBuilder(['resource' => $resource, 'query' => $query]);
 
diff --git a/src/Relations/MorphTo.php b/src/Relations/MorphTo.php
index 08389fe..f8bfc90 100644
--- a/src/Relations/MorphTo.php
+++ b/src/Relations/MorphTo.php
@@ -3,25 +3,11 @@
 namespace Lomkit\Rest\Relations;
 
 use Illuminate\Database\Eloquent\Model;
-use Illuminate\Validation\Rule;
 use Lomkit\Rest\Contracts\QueryBuilder;
 use Lomkit\Rest\Contracts\RelationResource;
-use Lomkit\Rest\Http\Resource;
 
 class MorphTo extends MorphRelation implements RelationResource
 {
-    /**
-     * Create a new MorphTo instance.
-     *
-     * @param string $relation The name of the relation.
-     * @param array  $types    An array of allowed types for the relation.
-     */
-    public function __construct($relation, array $types)
-    {
-        $this->relation = $relation;
-        $this->types = $types;
-    }
-
     /**
      * Perform actions before mutating the MorphTo relation.
      *
@@ -34,29 +20,8 @@ class MorphTo extends MorphRelation implements RelationResource
         $model
             ->{$relation->relation}()
             ->{$mutationRelations[$relation->relation]['operation'] === 'detach' ? 'dissociate' : 'associate'}(
-                app()->make(QueryBuilder::class, ['resource' => new $mutationRelations[$relation->relation]['type']()])
+                app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
                     ->applyMutation($mutationRelations[$relation->relation])
             );
     }
-
-    /**
-     * Define validation rules for the MorphTo relation.
-     *
-     * @param resource $resource The resource associated with the relation.
-     * @param string   $prefix   The prefix used for validation rules.
-     *
-     * @return array An array of validation rules.
-     */
-    public function rules(Resource $resource, string $prefix)
-    {
-        return [
-            ...parent::rules($resource, $prefix),
-            $prefix.'.type' => [
-                'required_with:'.$prefix,
-                Rule::in(
-                    $this->types
-                ),
-            ],
-        ];
-    }
 }
diff --git a/src/Relations/Relation.php b/src/Relations/Relation.php
index 1a997e4..3f8aa32 100644
--- a/src/Relations/Relation.php
+++ b/src/Relations/Relation.php
@@ -18,7 +18,7 @@ class Relation implements \JsonSerializable
     use Mutates;
     use Constrained;
     public string $relation;
-    protected array $types;
+    protected string $type;
 
     /**
      * The displayable name of the relation.
@@ -32,7 +32,7 @@ class Relation implements \JsonSerializable
     public function __construct($relation, $type)
     {
         $this->relation = $relation;
-        $this->types = [$type];
+        $this->type = $type;
     }
 
     /**
@@ -103,7 +103,7 @@ class Relation implements \JsonSerializable
      */
     public function resource()
     {
-        $resource = $this->types[0];
+        $resource = $this->type;
 
         // If the resource isn't registered, do it
         if (!app()->has($resource)) {
@@ -164,7 +164,7 @@ class Relation implements \JsonSerializable
         $request = app(RestRequest::class);
 
         return [
-            'resources'   => $this->types,
+            'resource'    => $this->type,
             'relation'    => $this->relation,
             'constraints' => [
                 'required_on_creation'   => $this->isRequiredOnCreation($request),
diff --git a/src/Rules/AggregateField.php b/src/Rules/AggregateField.php
index 27650e3..ac39cce 100644
--- a/src/Rules/AggregateField.php
+++ b/src/Rules/AggregateField.php
@@ -86,7 +86,7 @@ class AggregateField implements Rule, DataAwareRule, ValidatorAwareRule
      */
     protected function buildValidationRules($attribute, $value)
     {
-        $relationResource = $this->resource->relationResource($value['relation']);
+        $relationResource = $this->resource->relation($value['relation'])?->resource();
 
         if (is_null($relationResource)) {
             return [];
diff --git a/src/Rules/Includable.php b/src/Rules/Includable.php
index c603e3b..4cccef4 100644
--- a/src/Rules/Includable.php
+++ b/src/Rules/Includable.php
@@ -56,20 +56,18 @@ class Includable implements ValidationRule, ValidatorAwareRule
      */
     public function validate(string $attribute, mixed $value, Closure $fail): void
     {
-        $relationResource = $this->resource->relationResource($value['relation']);
+        $resource = $this->resource->relation($value['relation'])?->resource();
 
-        if (is_null($relationResource)) {
+        if (is_null($resource)) {
             return;
         }
 
         $this
             ->validator
             ->setRules(
-                is_null($relationResource) ?
-                    [] :
-                    [
-                        $attribute => [new SearchRules($relationResource, app(RestRequest::class), false)],
-                    ]
+                [
+                    $attribute => [new SearchRules($resource, app(RestRequest::class), false)],
+                ]
             )
             ->validate();
     }
diff --git a/src/Rules/NestedRelation.php b/src/Rules/NestedRelation.php
index 0dd157b..a9ab2a5 100644
--- a/src/Rules/NestedRelation.php
+++ b/src/Rules/NestedRelation.php
@@ -60,7 +60,7 @@ class NestedRelation implements ValidationRule, ValidatorAwareRule
         $relationResource = $this->resource;
 
         do {
-            $relationResource = $relationResource->relationResource(Str::before($relation, '.'));
+            $relationResource = $relationResource->relation(Str::before($relation, '.'))?->resource();
 
             if ($relationResource === null) {
                 $fail('The relation is not allowed');


Test patch:
diff --git a/tests/Feature/Controllers/MutateCreateMorphOperationsTest.php b/tests/Feature/Controllers/MutateCreateMorphOperationsTest.php
index 3504a3c..1d321ce 100644
--- a/tests/Feature/Controllers/MutateCreateMorphOperationsTest.php
+++ b/tests/Feature/Controllers/MutateCreateMorphOperationsTest.php
@@ -17,7 +17,6 @@ use Lomkit\Rest\Tests\Support\Models\MorphOneRelation;
 use Lomkit\Rest\Tests\Support\Models\MorphToManyRelation;
 use Lomkit\Rest\Tests\Support\Models\MorphToRelation;
 use Lomkit\Rest\Tests\Support\Policies\GreenPolicy;
-use Lomkit\Rest\Tests\Support\Rest\Resources\MorphedByManyResource;
 use Lomkit\Rest\Tests\Support\Rest\Resources\MorphToResource;
 
 class MutateCreateMorphOperationsTest extends TestCase
@@ -61,45 +60,6 @@ class MutateCreateMorphOperationsTest extends TestCase
         $this->assertNotNull(Model::first()->morph_to_relation_type);
     }
 
-    public function test_creating_a_resource_with_creating_second_morph_to_relation(): void
-    {
-        $modelToCreate = ModelFactory::new()->makeOne();
-
-        Gate::policy(Model::class, GreenPolicy::class);
-        Gate::policy(MorphedByManyRelation::class, GreenPolicy::class);
-
-        $response = $this->post(
-            '/api/models/mutate',
-            [
-                'mutate' => [
-                    [
-                        'operation'  => 'create',
-                        'attributes' => [
-                            'name'   => $modelToCreate->name,
-                            'number' => $modelToCreate->number,
-                        ],
-                        'relations' => [
-                            'morphToRelation' => [
-                                'operation'  => 'create',
-                                'type'       => MorphedByManyResource::class,
-                                'attributes' => [],
-                            ],
-                        ],
-                    ],
-                ],
-            ],
-            ['Accept' => 'application/json']
-        );
-
-        $this->assertMutatedResponse(
-            $response,
-            [$modelToCreate],
-        );
-
-        $this->assertNotNull(Model::first()->morph_to_relation_id);
-        $this->assertNotNull(Model::first()->morph_to_relation_type);
-    }
-
     public function test_creating_a_resource_with_attaching_morph_to_relation(): void
     {
         $modelToCreate = ModelFactory::new()->makeOne();
diff --git a/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php b/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php
index 47433a4..ddc71bb 100644
--- a/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php
+++ b/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php
@@ -17,7 +17,6 @@ use Lomkit\Rest\Tests\Support\Models\MorphOneRelation;
 use Lomkit\Rest\Tests\Support\Models\MorphToManyRelation;
 use Lomkit\Rest\Tests\Support\Models\MorphToRelation;
 use Lomkit\Rest\Tests\Support\Policies\GreenPolicy;
-use Lomkit\Rest\Tests\Support\Rest\Resources\MorphToResource;
 
 class MutateUpdateMorphOperationsTest extends TestCase
 {
@@ -66,7 +65,6 @@ class MutateUpdateMorphOperationsTest extends TestCase
                         'relations' => [
                             'morphToRelation' => [
                                 'operation'  => 'create',
-                                'type'       => MorphToResource::class,
                                 'attributes' => [],
                             ],
                         ],
@@ -116,7 +114,6 @@ class MutateUpdateMorphOperationsTest extends TestCase
                         'relations' => [
                             'morphToRelation' => [
                                 'operation' => 'attach',
-                                'type'      => MorphToResource::class,
                                 'key'       => $morphToRelationToAttach->getKey(),
                             ],
                         ],
@@ -163,7 +160,6 @@ class MutateUpdateMorphOperationsTest extends TestCase
                         'relations' => [
                             'morphToRelation' => [
                                 'operation' => 'detach',
-                                'type'      => MorphToResource::class,
                                 'key'       => $morphToRelationToDetach->getKey(),
                             ],
                         ],
@@ -205,7 +201,6 @@ class MutateUpdateMorphOperationsTest extends TestCase
                         'relations' => [
                             'morphToRelation' => [
                                 'operation'  => 'update',
-                                'type'       => MorphToResource::class,
                                 'key'        => $morphToRelationToUpdate->getKey(),
                                 'attributes' => ['number' => 5001], // 5001 because with factory it can't exceed 5000
                             ],
diff --git a/tests/Support/Rest/Resources/ModelResource.php b/tests/Support/Rest/Resources/ModelResource.php
index 20f4f59..0c9ee60 100644
--- a/tests/Support/Rest/Resources/ModelResource.php
+++ b/tests/Support/Rest/Resources/ModelResource.php
@@ -64,7 +64,7 @@ class ModelResource extends Resource
             HasManyThrough::make('hasManyThroughRelation', HasManyThroughResource::class),
 
             // Morph relationships
-            MorphTo::make('morphToRelation', [MorphToResource::class, MorphedByManyResource::class]),
+            MorphTo::make('morphToRelation', MorphToResource::class),
             MorphOne::make('morphOneRelation', MorphOneResource::class),
             MorphOneOfMany::make('morphOneOfManyRelation', MorphOneOfManyResource::class),
             MorphMany::make('morphManyRelation', MorphManyResource::class),