Skip to content

Commit ddee8be

Browse files
Merge 76d0e52 into 63fba2a
2 parents 63fba2a + 76d0e52 commit ddee8be

File tree

6 files changed

+143
-9
lines changed

6 files changed

+143
-9
lines changed

src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
use ApiPlatform\Doctrine\Odm\State\Options;
1919
use ApiPlatform\Metadata\CollectionOperationInterface;
2020
use ApiPlatform\Metadata\DeleteOperationInterface;
21+
use ApiPlatform\Metadata\HttpOperation;
22+
use ApiPlatform\Metadata\Link;
2123
use ApiPlatform\Metadata\Operation;
2224
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2325
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2426
use ApiPlatform\State\Util\StateOptionsTrait;
2527
use Doctrine\ODM\MongoDB\DocumentManager;
28+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
2629
use Doctrine\Persistence\ManagerRegistry;
2730

2831
final class DoctrineMongoDbOdmResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface
@@ -86,6 +89,10 @@ private function addDefaults(Operation $operation): Operation
8689

8790
if (null === $operation->getProvider()) {
8891
$operation = $operation->withProvider($this->getProvider($operation));
92+
93+
if ($operation instanceof HttpOperation) {
94+
$operation = $operation->withRequirements($this->getRequirements($operation));
95+
}
8996
}
9097

9198
if (null === $operation->getProcessor()) {
@@ -95,6 +102,54 @@ private function addDefaults(Operation $operation): Operation
95102
return $operation;
96103
}
97104

105+
/**
106+
* @return array<string, string>
107+
*/
108+
private function getRequirements(HttpOperation $operation): array
109+
{
110+
$requirements = $operation->getRequirements() ?? [];
111+
$uriVariables = (array) ($operation->getUriVariables() ?? []);
112+
113+
foreach ($uriVariables as $paramName => $uriVariable) {
114+
if (isset($requirements[$paramName])) {
115+
continue;
116+
}
117+
118+
if (!$uriVariable instanceof Link) {
119+
continue;
120+
}
121+
122+
$identifiers = $uriVariable->getIdentifiers();
123+
if (1 !== \count($identifiers)) {
124+
continue;
125+
}
126+
$fieldName = $identifiers[0];
127+
128+
$fromClass = $uriVariable->getFromClass();
129+
if (null === $fromClass) {
130+
continue;
131+
}
132+
$classMetadata = $this->managerRegistry->getManagerForClass($fromClass)?->getClassMetadata($fromClass);
133+
134+
$requirement = null;
135+
if ($classMetadata instanceof ClassMetadata && $classMetadata->hasField($fieldName)) {
136+
$fieldMapping = $classMetadata->getFieldMapping($fieldName);
137+
$requirement = match ($fieldMapping['type']) {
138+
'uuid', 'guid' => '^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$',
139+
'ulid' => '^[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}$',
140+
'smallint', 'integer', 'bigint' => '^-?[0-9]+$',
141+
default => null,
142+
};
143+
}
144+
145+
if (null !== $requirement) {
146+
$requirements[$paramName] = $requirement;
147+
}
148+
}
149+
150+
return $requirements;
151+
}
152+
98153
private function getProvider(Operation $operation): string
99154
{
100155
if ($operation instanceof CollectionOperationInterface) {

src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
use ApiPlatform\Doctrine\Orm\State\Options;
1919
use ApiPlatform\Metadata\CollectionOperationInterface;
2020
use ApiPlatform\Metadata\DeleteOperationInterface;
21+
use ApiPlatform\Metadata\HttpOperation;
22+
use ApiPlatform\Metadata\Link;
2123
use ApiPlatform\Metadata\Operation;
2224
use ApiPlatform\Metadata\Patch;
2325
use ApiPlatform\Metadata\Put;
2426
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2527
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2628
use ApiPlatform\State\Util\StateOptionsTrait;
2729
use Doctrine\ORM\EntityManagerInterface;
30+
use Doctrine\ORM\Mapping\ClassMetadata;
31+
use Doctrine\ORM\Mapping\FieldMapping;
2832
use Doctrine\Persistence\ManagerRegistry;
2933

3034
final class DoctrineOrmResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface
@@ -94,6 +98,10 @@ private function addDefaults(Operation $operation): Operation
9498
{
9599
if (null === $operation->getProvider()) {
96100
$operation = $operation->withProvider($this->getProvider($operation));
101+
102+
if ($operation instanceof HttpOperation) {
103+
$operation = $operation->withRequirements($this->getRequirements($operation));
104+
}
97105
}
98106

99107
$options = $operation->getStateOptions() ?: new Options();
@@ -109,6 +117,59 @@ private function addDefaults(Operation $operation): Operation
109117
return $operation;
110118
}
111119

120+
/**
121+
* @return array<string, string>
122+
*/
123+
private function getRequirements(HttpOperation $operation): array
124+
{
125+
$requirements = $operation->getRequirements() ?? [];
126+
$uriVariables = (array) ($operation->getUriVariables() ?? []);
127+
128+
foreach ($uriVariables as $paramName => $uriVariable) {
129+
if (isset($requirements[$paramName])) {
130+
continue;
131+
}
132+
133+
if (!$uriVariable instanceof Link) {
134+
continue;
135+
}
136+
$identifiers = $uriVariable->getIdentifiers();
137+
if (1 !== \count($identifiers)) {
138+
continue;
139+
}
140+
$fieldName = $identifiers[0];
141+
142+
$fromClass = $uriVariable->getFromClass();
143+
if (null === $fromClass) {
144+
continue;
145+
}
146+
$classMetadata = $this->managerRegistry->getManagerForClass($fromClass)?->getClassMetadata($fromClass);
147+
148+
$requirement = null;
149+
if ($classMetadata instanceof ClassMetadata && $classMetadata->hasField($fieldName)) {
150+
$fieldMapping = $classMetadata->getFieldMapping($fieldName);
151+
if (class_exists(FieldMapping::class)) {
152+
$type = $fieldMapping->type;
153+
} else {
154+
$type = $fieldMapping['type'];
155+
}
156+
157+
$requirement = match ($type) {
158+
'uuid', 'guid' => '^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$',
159+
'ulid' => '^[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}$',
160+
'smallint', 'integer', 'bigint' => '^-?[0-9]+$',
161+
default => null,
162+
};
163+
}
164+
165+
if (null !== $requirement) {
166+
$requirements[$paramName] = $requirement;
167+
}
168+
}
169+
170+
return $requirements;
171+
}
172+
112173
private function getProvider(Operation $operation): string
113174
{
114175
if ($operation instanceof CollectionOperationInterface) {

src/Metadata/Link.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
2020
final class Link extends Parameter
2121
{
22+
/**
23+
* @param class-string|null $fromClass
24+
* @param class-string|null $toClass
25+
*/
2226
public function __construct(
2327
private ?string $parameterName = null,
2428
private ?string $fromProperty = null,
@@ -87,11 +91,17 @@ public function withParameterName(string $parameterName): self
8791
return $self;
8892
}
8993

94+
/**
95+
* @return class-string|null
96+
*/
9097
public function getFromClass(): ?string
9198
{
9299
return $this->fromClass;
93100
}
94101

102+
/**
103+
* @param class-string $fromClass
104+
*/
95105
public function withFromClass(string $fromClass): self
96106
{
97107
$self = clone $this;
@@ -100,11 +110,17 @@ public function withFromClass(string $fromClass): self
100110
return $self;
101111
}
102112

113+
/**
114+
* @return class-string|null
115+
*/
103116
public function getToClass(): ?string
104117
{
105118
return $this->toClass;
106119
}
107120

121+
/**
122+
* @param class-string $toClass
123+
*/
108124
public function withToClass(string $toClass): self
109125
{
110126
$self = clone $this;

src/State/Tests/Provider/SecurityParameterProviderTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function testIsGrantedLink(): void
3030
$obj = new \stdClass();
3131
$barObj = new \stdClass();
3232
$operation = new GetCollection(uriVariables: [
33-
'barId' => new Link(toProperty: 'bar', fromClass: 'Bar', security: 'is_granted("some_voter", "bar")'),
33+
'barId' => new Link(toProperty: 'bar', fromClass: $barObj::class, security: 'is_granted("some_voter", "bar")'),
3434
], class: \stdClass::class);
3535
$decorated = $this->createMock(ProviderInterface::class);
3636
$decorated->method('provide')->willReturn($obj);
@@ -39,7 +39,7 @@ public function testIsGrantedLink(): void
3939
$request->attributes = $parameterBag;
4040
$request->attributes->set('bar', $barObj);
4141
$resourceAccessChecker = $this->createMock(ResourceAccessCheckerInterface::class);
42-
$resourceAccessChecker->expects($this->once())->method('isGranted')->with('Bar', 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(true);
42+
$resourceAccessChecker->expects($this->once())->method('isGranted')->with($barObj::class, 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(true);
4343
$accessChecker = new SecurityParameterProvider($decorated, $resourceAccessChecker);
4444
$accessChecker->provide($operation, ['barId' => 1], ['request' => $request]);
4545
}
@@ -51,7 +51,7 @@ public function testIsNotGrantedLink(): void
5151
$obj = new \stdClass();
5252
$barObj = new \stdClass();
5353
$operation = new GetCollection(uriVariables: [
54-
'barId' => new Link(toProperty: 'bar', fromClass: 'Bar', security: 'is_granted("some_voter", "bar")'),
54+
'barId' => new Link(toProperty: 'bar', fromClass: $barObj::class, security: 'is_granted("some_voter", "bar")'),
5555
], class: \stdClass::class);
5656
$decorated = $this->createMock(ProviderInterface::class);
5757
$decorated->method('provide')->willReturn($obj);
@@ -60,7 +60,7 @@ public function testIsNotGrantedLink(): void
6060
$request->attributes = $parameterBag;
6161
$request->attributes->set('bar', $barObj);
6262
$resourceAccessChecker = $this->createMock(ResourceAccessCheckerInterface::class);
63-
$resourceAccessChecker->expects($this->once())->method('isGranted')->with('Bar', 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(false);
63+
$resourceAccessChecker->expects($this->once())->method('isGranted')->with($barObj::class, 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(false);
6464
$accessChecker = new SecurityParameterProvider($decorated, $resourceAccessChecker);
6565
$accessChecker->provide($operation, ['barId' => 1], ['request' => $request]);
6666
}
@@ -73,7 +73,7 @@ public function testSecurityMessageLink(): void
7373
$obj = new \stdClass();
7474
$barObj = new \stdClass();
7575
$operation = new GetCollection(uriVariables: [
76-
'barId' => new Link(toProperty: 'bar', fromClass: 'Bar', security: 'is_granted("some_voter", "bar")', securityMessage: 'You are not admin.'),
76+
'barId' => new Link(toProperty: 'bar', fromClass: $barObj::class, security: 'is_granted("some_voter", "bar")', securityMessage: 'You are not admin.'),
7777
], class: \stdClass::class);
7878
$decorated = $this->createMock(ProviderInterface::class);
7979
$decorated->method('provide')->willReturn($obj);
@@ -82,7 +82,7 @@ public function testSecurityMessageLink(): void
8282
$request->attributes = $parameterBag;
8383
$request->attributes->set('bar', $barObj);
8484
$resourceAccessChecker = $this->createMock(ResourceAccessCheckerInterface::class);
85-
$resourceAccessChecker->expects($this->once())->method('isGranted')->with('Bar', 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(false);
85+
$resourceAccessChecker->expects($this->once())->method('isGranted')->with($barObj::class, 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(false);
8686
$accessChecker = new SecurityParameterProvider($decorated, $resourceAccessChecker);
8787
$accessChecker->provide($operation, ['barId' => 1], ['request' => $request]);
8888
}

tests/Fixtures/TestBundle/Entity/ContainNonResource.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
#[ORM\Entity]
2828
class ContainNonResource
2929
{
30-
#[ORM\Column(type: 'integer')]
30+
/**
31+
* @var string
32+
*/
33+
#[ORM\Column(type: 'string')]
3134
#[ORM\Id]
32-
#[ORM\GeneratedValue(strategy: 'AUTO')]
3335
#[Groups('contain_non_resource')]
3436
public $id;
3537
/**

tests/Fixtures/TestBundle/State/ContainNonResourceProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
3535
$resourceClass = $operation->getClass();
3636
// Retrieve the blog post item from somewhere
3737
$cnr = new $resourceClass();
38-
$cnr->id = $id;
38+
$cnr->id = (string) $id;
3939
$cnr->notAResource = new NotAResource('f1', 'b1');
4040
$cnr->nested = new $resourceClass();
4141
$cnr->nested->id = "$id-nested";

0 commit comments

Comments
 (0)