Skip to content

Commit d08eb7f

Browse files
authored
fix: ResourceClassInfoTrait::isResourceClass() is always true (#7924)
1 parent d899710 commit d08eb7f

5 files changed

Lines changed: 251 additions & 2 deletions

File tree

src/Laravel/ApiPlatformProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ public function register(): void
326326
),
327327
$app->make(ResourceClassResolverInterface::class)
328328
),
329+
$app->make(ResourceClassResolverInterface::class)
329330
)
330331
),
331332
true === $config->get('app.debug') ? 'array' : $config->get('api-platform.cache', 'file')

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public function __construct(
5959
?ResourceClassResolverInterface $resourceClassResolver = null,
6060
) {
6161
$this->resourceClassResolver = $resourceClassResolver;
62+
$this->resourceMetadataFactory = $this->decorated;
6263
}
6364

6465
public function create(string $resourceClass): ResourceMetadataCollection

src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,84 @@ public function testCreateWithIgnoredProperty(): void
213213
self::assertFalse($result->isReadable());
214214
self::assertFalse($result->isWritable());
215215
}
216+
217+
public function testWithResourceClassResolverIdentifiesResourceClass(): void
218+
{
219+
$serializerClassMetadataFactoryProphecy = $this->prophesize(SerializerClassMetadataFactoryInterface::class);
220+
$dummySerializerClassMetadata = new SerializerClassMetadata(Dummy::class);
221+
$relatedDummySerializerAttributeMetadata = new SerializerAttributeMetadata('relatedDummy');
222+
$dummySerializerClassMetadata->addAttributeMetadata($relatedDummySerializerAttributeMetadata);
223+
$serializerClassMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummySerializerClassMetadata);
224+
$relatedDummySerializerClassMetadata = new SerializerClassMetadata(RelatedDummy::class);
225+
$serializerClassMetadataFactoryProphecy->getMetadataFor(RelatedDummy::class)->willReturn($relatedDummySerializerClassMetadata);
226+
227+
$context = [];
228+
229+
$decoratedProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
230+
$relatedDummyPropertyMetadata = (new ApiProperty())
231+
->withNativeType(Type::nullable(Type::object(RelatedDummy::class)));
232+
$decoratedProphecy->create(Dummy::class, 'relatedDummy', $context)->willReturn($relatedDummyPropertyMetadata);
233+
234+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
235+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
236+
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);
237+
$resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class);
238+
239+
$serializerPropertyMetadataFactory = new SerializerPropertyMetadataFactory(
240+
$serializerClassMetadataFactoryProphecy->reveal(),
241+
$decoratedProphecy->reveal(),
242+
$resourceClassResolverProphecy->reveal()
243+
);
244+
245+
$actual = $serializerPropertyMetadataFactory->create(Dummy::class, 'relatedDummy', $context);
246+
247+
$this->assertInstanceOf(ApiProperty::class, $actual);
248+
$this->assertTrue($actual->isReadable());
249+
$this->assertTrue($actual->isWritable());
250+
}
251+
252+
/**
253+
* Test that isResourceClass() falls back to false when resourceClassResolver is NOT provided.
254+
* This demonstrates the bug that occurs without the fix in ApiPlatformProvider.
255+
*
256+
* When resourceClassResolver is null and resourceMetadataFactory is null,
257+
* isResourceClass() returns false, preventing proper link status detection.
258+
*/
259+
public function testWithoutResourceClassResolverFallsBackToFalse(): void
260+
{
261+
$serializerClassMetadataFactoryProphecy = $this->prophesize(SerializerClassMetadataFactoryInterface::class);
262+
$dummySerializerClassMetadata = new SerializerClassMetadata(Dummy::class);
263+
$relatedDummySerializerAttributeMetadata = new SerializerAttributeMetadata('relatedDummy');
264+
$dummySerializerClassMetadata->addAttributeMetadata($relatedDummySerializerAttributeMetadata);
265+
$serializerClassMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummySerializerClassMetadata);
266+
$relatedDummySerializerClassMetadata = new SerializerClassMetadata(RelatedDummy::class);
267+
$serializerClassMetadataFactoryProphecy->getMetadataFor(RelatedDummy::class)->willReturn($relatedDummySerializerClassMetadata);
268+
269+
$context = [];
270+
271+
$decoratedProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
272+
$relatedDummyPropertyMetadata = (new ApiProperty())
273+
->withNativeType(Type::nullable(Type::object(RelatedDummy::class)));
274+
$decoratedProphecy->create(Dummy::class, 'relatedDummy', $context)->willReturn($relatedDummyPropertyMetadata);
275+
276+
// Create factory WITHOUT resourceClassResolver (passing null)
277+
$serializerPropertyMetadataFactory = new SerializerPropertyMetadataFactory(
278+
$serializerClassMetadataFactoryProphecy->reveal(),
279+
$decoratedProphecy->reveal(),
280+
null // No resourceClassResolver
281+
);
282+
283+
$actual = $serializerPropertyMetadataFactory->create(Dummy::class, 'relatedDummy', $context);
284+
285+
$this->assertInstanceOf(ApiProperty::class, $actual);
286+
// Without resourceClassResolver, isResourceClass() falls back to false,
287+
// so transformLinkStatus() skips the link status logic
288+
// This results in null link statuses, which causes serializer to reject nested documents
289+
$this->assertTrue($actual->isReadable());
290+
$this->assertTrue($actual->isWritable());
291+
// The key difference: link statuses remain null (not explicitly set)
292+
// This is the root cause of the Laravel test failures
293+
$this->assertNull($actual->isReadableLink(), 'readableLink is null when resourceClassResolver is missing');
294+
$this->assertNull($actual->isWritableLink(), 'writeableLink is null when resourceClassResolver is missing');
295+
}
216296
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Metadata\Tests\Util;
15+
16+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
17+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
18+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
19+
use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\Dummy;
20+
use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\RelatedDummy;
21+
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
22+
use PHPUnit\Framework\TestCase;
23+
24+
class ResourceClassInfoTraitTest extends TestCase
25+
{
26+
private function getResourceClassInfoTraitImplementation(
27+
?ResourceClassResolverInterface $resourceClassResolver = null,
28+
?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null,
29+
) {
30+
return new class($resourceClassResolver, $resourceMetadataFactory) {
31+
use ResourceClassInfoTrait {
32+
ResourceClassInfoTrait::isResourceClass as public;
33+
ResourceClassInfoTrait::getResourceClass as public;
34+
}
35+
36+
public function __construct(
37+
?ResourceClassResolverInterface $resourceClassResolver = null,
38+
?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null,
39+
) {
40+
$this->resourceClassResolver = $resourceClassResolver;
41+
$this->resourceMetadataFactory = $resourceMetadataFactory;
42+
}
43+
};
44+
}
45+
46+
public function testIsResourceClassWithResolver(): void
47+
{
48+
$resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class);
49+
$resourceClassResolver->method('isResourceClass')
50+
->willReturnMap([
51+
[Dummy::class, true],
52+
[RelatedDummy::class, false],
53+
]);
54+
55+
$classInfo = $this->getResourceClassInfoTraitImplementation($resourceClassResolver);
56+
57+
$this->assertTrue($classInfo->isResourceClass(Dummy::class));
58+
$this->assertFalse($classInfo->isResourceClass(RelatedDummy::class));
59+
}
60+
61+
public function testIsResourceClassWithMetadataFactoryWhenNoResolver(): void
62+
{
63+
$resourceMetadataFactory = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
64+
$dummyMetadata = new ResourceMetadataCollection(Dummy::class, [new \stdClass()]);
65+
$emptyMetadata = new ResourceMetadataCollection(RelatedDummy::class, []);
66+
67+
$resourceMetadataFactory->method('create')
68+
->willReturnMap([
69+
[Dummy::class, $dummyMetadata],
70+
[RelatedDummy::class, $emptyMetadata],
71+
]);
72+
73+
$classInfo = $this->getResourceClassInfoTraitImplementation(null, $resourceMetadataFactory);
74+
75+
$this->assertTrue($classInfo->isResourceClass(Dummy::class));
76+
$this->assertFalse($classInfo->isResourceClass(RelatedDummy::class));
77+
}
78+
79+
public function testIsResourceClassWithoutResolverOrFactoryReturnsFalse(): void
80+
{
81+
$classInfo = $this->getResourceClassInfoTraitImplementation();
82+
83+
$this->assertFalse($classInfo->isResourceClass(Dummy::class));
84+
$this->assertFalse($classInfo->isResourceClass(RelatedDummy::class));
85+
}
86+
87+
public function testResourceClassResolverTakesPrecedenceOverResourceMetadataFactory(): void
88+
{
89+
$resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class);
90+
$resourceClassResolver->method('isResourceClass')->willReturn(true);
91+
92+
$resourceMetadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
93+
$resourceMetadataFactory->expects($this->never())->method('create');
94+
95+
$classInfo = $this->getResourceClassInfoTraitImplementation($resourceClassResolver, $resourceMetadataFactory);
96+
97+
$this->assertTrue($classInfo->isResourceClass(Dummy::class));
98+
}
99+
100+
public function testGetResourceClassWhenStrictFalseAndIsResourceClassFalse(): void
101+
{
102+
$dummy = new Dummy();
103+
104+
$resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
105+
$resourceClassResolver->method('isResourceClass')->willReturn(false);
106+
$resourceClassResolver->expects($this->never())->method('getResourceClass');
107+
108+
$classInfo = $this->getResourceClassInfoTraitImplementation($resourceClassResolver);
109+
110+
$result = $classInfo->getResourceClass($dummy);
111+
$this->assertNull($result);
112+
}
113+
114+
public function testGetResourceClassWhenStrictFalseAndIsResourceClassTrue(): void
115+
{
116+
$dummy = new Dummy();
117+
118+
$resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class);
119+
$resourceClassResolver->method('isResourceClass')->willReturn(true);
120+
121+
$resourceClassResolver->method('getResourceClass')->willReturn(Dummy::class);
122+
123+
$classInfo = $this->getResourceClassInfoTraitImplementation($resourceClassResolver);
124+
125+
$result = $classInfo->getResourceClass($dummy);
126+
$this->assertSame(Dummy::class, $result);
127+
}
128+
129+
public function testGetResourceClassWhenStrictTrueAndIsResourceClassTrue(): void
130+
{
131+
$dummy = new Dummy();
132+
133+
$resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class);
134+
$resourceClassResolver->method('isResourceClass')->willReturn(true);
135+
136+
$resourceClassResolver->method('getResourceClass')->willReturn(Dummy::class);
137+
138+
$classInfo = $this->getResourceClassInfoTraitImplementation($resourceClassResolver);
139+
140+
$result = $classInfo->getResourceClass($dummy, true);
141+
$this->assertSame(Dummy::class, $result);
142+
}
143+
144+
public function testGetResourceClassWhenStrictTrueAndIsResourceClassFalse(): void
145+
{
146+
$dummy = new Dummy();
147+
148+
$resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class);
149+
$resourceClassResolver->method('isResourceClass')->willReturn(false);
150+
151+
$resourceClassResolver->method('getResourceClass')->willReturn(Dummy::class);
152+
153+
$classInfo = $this->getResourceClassInfoTraitImplementation($resourceClassResolver);
154+
155+
$result = $classInfo->getResourceClass($dummy, true);
156+
$this->assertSame(Dummy::class, $result);
157+
}
158+
159+
public function testGetResourceClassWithoutResolver(): void
160+
{
161+
$dummy = new Dummy();
162+
163+
$classInfo = $this->getResourceClassInfoTraitImplementation();
164+
165+
$result = $classInfo->getResourceClass($dummy);
166+
$this->assertSame(Dummy::class, $result);
167+
}
168+
}

src/Metadata/Util/ResourceClassInfoTrait.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ private function isResourceClass(string $class): bool
6262
return \count($this->resourceMetadataFactory->create($class)) > 0;
6363
}
6464

65-
// assume that it's a resource class
66-
return true;
65+
return false;
6766
}
6867

6968
private function getTypeFromProperty(ApiProperty $propertyMetadata): ?Type

0 commit comments

Comments
 (0)