Skip to content

Commit 4a22ba9

Browse files
feat(doctrine): add requirements
1 parent 15ea6d8 commit 4a22ba9

File tree

4 files changed

+138
-6
lines changed

4 files changed

+138
-6
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,11 +18,15 @@
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\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2325
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2426
use ApiPlatform\State\Util\StateOptionsTrait;
2527
use Doctrine\ORM\EntityManagerInterface;
28+
use Doctrine\ORM\Mapping\ClassMetadata;
29+
use Doctrine\ORM\Mapping\FieldMapping;
2630
use Doctrine\Persistence\ManagerRegistry;
2731

2832
final class DoctrineOrmResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface
@@ -83,6 +87,10 @@ private function addDefaults(Operation $operation): Operation
8387
{
8488
if (null === $operation->getProvider()) {
8589
$operation = $operation->withProvider($this->getProvider($operation));
90+
91+
if ($operation instanceof HttpOperation) {
92+
$operation = $operation->withRequirements($this->getRequirements($operation));
93+
}
8694
}
8795

8896
$options = $operation->getStateOptions() ?: new Options();
@@ -98,6 +106,59 @@ private function addDefaults(Operation $operation): Operation
98106
return $operation;
99107
}
100108

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

0 commit comments

Comments
 (0)