Skip to content

Commit ab71ee5

Browse files
committed
feat(laravel): graphql policies
1 parent 0b7761e commit ab71ee5

File tree

14 files changed

+324
-82
lines changed

14 files changed

+324
-82
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 106 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,10 @@ public function register(): void
927927
);
928928
});
929929

930+
if (interface_exists(FieldsBuilderEnumInterface::class)) {
931+
$this->registerGraphQl($this->app);
932+
}
933+
930934
$this->app->singleton(JsonApiObjectNormalizer::class, function (Application $app) {
931935
return new JsonApiObjectNormalizer(
932936
$app->make(ObjectNormalizer::class),
@@ -936,51 +940,6 @@ public function register(): void
936940
);
937941
});
938942

939-
if ($this->app['config']->get('api-platform.graphql.enabled')) {
940-
$this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) {
941-
return new GraphQlItemNormalizer(
942-
$app->make(PropertyNameCollectionFactoryInterface::class),
943-
$app->make(PropertyMetadataFactoryInterface::class),
944-
$app->make(IriConverterInterface::class),
945-
$app->make(IdentifiersExtractorInterface::class),
946-
$app->make(ResourceClassResolverInterface::class),
947-
$app->make(PropertyAccessorInterface::class),
948-
$app->make(NameConverterInterface::class),
949-
$app->make(SerializerClassMetadataFactory::class),
950-
null,
951-
$app->make(ResourceMetadataCollectionFactoryInterface::class),
952-
$app->make(ResourceAccessCheckerInterface::class)
953-
);
954-
});
955-
956-
$this->app->singleton(GraphQlObjectNormalizer::class, function (Application $app) {
957-
return new GraphQlObjectNormalizer(
958-
$app->make(ObjectNormalizer::class),
959-
$app->make(IriConverterInterface::class),
960-
$app->make(IdentifiersExtractorInterface::class),
961-
);
962-
});
963-
}
964-
965-
$this->app->singleton(GraphQlErrorNormalizer::class, function () {
966-
return new GraphQlErrorNormalizer();
967-
});
968-
969-
$this->app->singleton(GraphQlValidationExceptionNormalizer::class, function (Application $app) {
970-
/** @var ConfigRepository */
971-
$config = $app['config'];
972-
973-
return new GraphQlValidationExceptionNormalizer($config->get('api-platform.exception_to_status'));
974-
});
975-
976-
$this->app->singleton(GraphQlHttpExceptionNormalizer::class, function () {
977-
return new GraphQlHttpExceptionNormalizer();
978-
});
979-
980-
$this->app->singleton(GraphQlRuntimeExceptionNormalizer::class, function () {
981-
return new GraphQlHttpExceptionNormalizer();
982-
});
983-
984943
$this->app->bind(SerializerInterface::class, Serializer::class);
985944
$this->app->bind(NormalizerInterface::class, Serializer::class);
986945
$this->app->singleton(Serializer::class, function (Application $app) {
@@ -1009,7 +968,7 @@ public function register(): void
1009968
$list->insert($app->make(JsonApiItemNormalizer::class), -890);
1010969
$list->insert($app->make(JsonApiObjectNormalizer::class), -995);
1011970

1012-
if ($config->get('api-platform.graphql.enabled')) {
971+
if (interface_exists(FieldsBuilderEnumInterface::class)) {
1013972
$list->insert($app->make(GraphQlItemNormalizer::class), -890);
1014973
$list->insert($app->make(GraphQlObjectNormalizer::class), -995);
1015974
$list->insert($app->make(GraphQlErrorNormalizer::class), -790);
@@ -1033,7 +992,8 @@ public function register(): void
1033992
new JsonEncoder('jsonapi'),
1034993
new JsonEncoder('jsonhal'),
1035994
new CsvEncoder(),
1036-
]);
995+
]
996+
);
1037997
});
1038998

1039999
$this->app->singleton(JsonLdItemNormalizer::class, function (Application $app) {
@@ -1078,17 +1038,56 @@ function (Application $app) {
10781038
return new Inflector();
10791039
});
10801040

1081-
if ($this->app['config']->get('api-platform.graphql.enabled')) {
1082-
$this->registerGraphQl($this->app);
1083-
}
1084-
10851041
if ($this->app->runningInConsole()) {
10861042
$this->commands([Console\InstallCommand::class]);
10871043
}
10881044
}
10891045

10901046
private function registerGraphQl(Application $app): void
10911047
{
1048+
$this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) {
1049+
return new GraphQlItemNormalizer(
1050+
$app->make(PropertyNameCollectionFactoryInterface::class),
1051+
$app->make(PropertyMetadataFactoryInterface::class),
1052+
$app->make(IriConverterInterface::class),
1053+
$app->make(IdentifiersExtractorInterface::class),
1054+
$app->make(ResourceClassResolverInterface::class),
1055+
$app->make(PropertyAccessorInterface::class),
1056+
$app->make(NameConverterInterface::class),
1057+
$app->make(SerializerClassMetadataFactory::class),
1058+
null,
1059+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
1060+
$app->make(ResourceAccessCheckerInterface::class)
1061+
);
1062+
});
1063+
1064+
$this->app->singleton(GraphQlObjectNormalizer::class, function (Application $app) {
1065+
return new GraphQlObjectNormalizer(
1066+
$app->make(ObjectNormalizer::class),
1067+
$app->make(IriConverterInterface::class),
1068+
$app->make(IdentifiersExtractorInterface::class),
1069+
);
1070+
});
1071+
1072+
$this->app->singleton(GraphQlErrorNormalizer::class, function () {
1073+
return new GraphQlErrorNormalizer();
1074+
});
1075+
1076+
$this->app->singleton(GraphQlValidationExceptionNormalizer::class, function (Application $app) {
1077+
/** @var ConfigRepository */
1078+
$config = $app['config'];
1079+
1080+
return new GraphQlValidationExceptionNormalizer($config->get('api-platform.exception_to_status'));
1081+
});
1082+
1083+
$this->app->singleton(GraphQlHttpExceptionNormalizer::class, function () {
1084+
return new GraphQlHttpExceptionNormalizer();
1085+
});
1086+
1087+
$this->app->singleton(GraphQlRuntimeExceptionNormalizer::class, function () {
1088+
return new GraphQlHttpExceptionNormalizer();
1089+
});
1090+
10921091
$app->singleton('api_platform.graphql.type_locator', function (Application $app) {
10931092
$tagged = iterator_to_array($app->tagged('api_platform.graphql.type'));
10941093

@@ -1130,44 +1129,78 @@ private function registerGraphQl(Application $app): void
11301129
return new GraphQlSerializerContextBuilder($app->make(NameConverterInterface::class));
11311130
});
11321131

1133-
$app->singleton('api_platform.graphql.state_provider', function (Application $app) {
1132+
$app->singleton(GraphQlReadProvider::class, function (Application $app) {
11341133
/** @var ConfigRepository */
11351134
$config = $app['config'];
1136-
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));
1137-
$resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver'));
11381135

11391136
return new GraphQlReadProvider(
1140-
new GraphQlDenormalizeProvider(
1141-
new ResolverProvider(
1142-
new ParameterProvider(
1143-
$app->make(CallableProvider::class),
1144-
new ServiceLocator($tagged)
1145-
),
1146-
new ServiceLocator($resolvers),
1147-
),
1148-
$app->make(SerializerInterface::class),
1149-
$app->make(GraphQlSerializerContextBuilder::class)
1150-
),
1137+
$this->app->make(CallableProvider::class),
11511138
$app->make(IriConverterInterface::class),
11521139
$app->make(GraphQlSerializerContextBuilder::class),
11531140
$config->get('api-platform.graphql.nesting_separator') ?? '__'
11541141
);
11551142
});
1143+
$app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read');
1144+
1145+
$app->singleton(ResolverProvider::class, function (Application $app) {
1146+
$resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver'));
1147+
1148+
return new ResolverProvider(
1149+
$app->make(GraphQlReadProvider::class),
1150+
new ServiceLocator($resolvers),
1151+
);
1152+
});
1153+
1154+
$app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver');
1155+
1156+
$app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) {
1157+
return new GraphQlDenormalizeProvider(
1158+
$this->app->make(ResolverProvider::class),
1159+
$app->make(SerializerInterface::class),
1160+
$app->make(GraphQlSerializerContextBuilder::class)
1161+
);
1162+
});
1163+
1164+
$app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize');
1165+
1166+
$app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) {
1167+
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));
1168+
$tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class);
1169+
1170+
return new ParameterProvider(
1171+
new ParameterValidatorProvider(
1172+
new SecurityParameterProvider(
1173+
$app->make(GraphQlDenormalizeProvider::class),
1174+
$app->make(ResourceAccessCheckerInterface::class)
1175+
),
1176+
),
1177+
new ServiceLocator($tagged)
1178+
);
1179+
});
1180+
1181+
$app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) {
1182+
return new AccessCheckerProvider($app->make('api_platform.graphql.state_provider.parameter'), $app->make(ResourceAccessCheckerInterface::class));
1183+
});
1184+
1185+
$app->singleton(NormalizeProcessor::class, function (Application $app) {
1186+
return new NormalizeProcessor(
1187+
$app->make(SerializerInterface::class),
1188+
$app->make(GraphQlSerializerContextBuilder::class),
1189+
$app->make(Pagination::class)
1190+
);
1191+
});
1192+
$app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize');
11561193

11571194
$app->singleton('api_platform.graphql.state_processor', function (Application $app) {
11581195
return new WriteProcessor(
1159-
new NormalizeProcessor(
1160-
$app->make(SerializerInterface::class),
1161-
$app->make(GraphQlSerializerContextBuilder::class),
1162-
$app->make(Pagination::class)
1163-
),
1196+
$app->make('api_platform.graphql.state_processor.normalize'),
11641197
$app->make(CallableProcessor::class),
11651198
);
11661199
});
11671200

11681201
$app->singleton(ResolverFactoryInterface::class, function (Application $app) {
11691202
return new ResolverFactory(
1170-
$app->make('api_platform.graphql.state_provider'),
1203+
$app->make('api_platform.graphql.state_provider.access_checker'),
11711204
$app->make('api_platform.graphql.state_processor')
11721205
);
11731206
});
@@ -1227,7 +1260,8 @@ private function registerGraphQl(Application $app): void
12271260
$app->make(SerializerInterface::class),
12281261
$app->make(ErrorHandlerInterface::class),
12291262
debug: $config->get('app.debug'),
1230-
negotiator: $app->make(Negotiator::class)
1263+
negotiator: $app->make(Negotiator::class),
1264+
formats: $config->get('api-platform.formats')
12311265
);
12321266
});
12331267
}

src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
use ApiPlatform\Metadata\DeleteOperationInterface;
2323
use ApiPlatform\Metadata\Get;
2424
use ApiPlatform\Metadata\GetCollection;
25+
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
26+
use ApiPlatform\Metadata\GraphQl\Mutation;
27+
use ApiPlatform\Metadata\GraphQl\Query;
28+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
29+
use ApiPlatform\Metadata\GraphQl\Subscription;
2530
use ApiPlatform\Metadata\Patch;
2631
use ApiPlatform\Metadata\Post;
2732
use ApiPlatform\Metadata\Put;
@@ -39,6 +44,12 @@ final class EloquentResourceCollectionMetadataFactory implements ResourceMetadat
3944
GetCollection::class => 'viewAny',
4045
Delete::class => 'delete',
4146
Patch::class => 'update',
47+
48+
Query::class => 'view',
49+
QueryCollection::class => 'viewAny',
50+
Mutation::class => 'update',
51+
DeleteMutation::class => 'delete',
52+
Subscription::class => 'viewAny',
4253
];
4354

4455
public function __construct(
@@ -94,6 +105,12 @@ public function create(string $resourceClass): ResourceMetadataCollection
94105
$graphQlOperations = $resourceMetadata->getGraphQlOperations();
95106

96107
foreach ($graphQlOperations ?? [] as $operationName => $graphQlOperation) {
108+
if (!$graphQlOperation->getPolicy() && ($policy = Gate::getPolicyFor($model))) {
109+
if (($policyMethod = self::POLICY_METHODS[$graphQlOperation::class] ?? null) && method_exists($policy, $policyMethod)) {
110+
$graphQlOperation = $graphQlOperation->withPolicy($policyMethod);
111+
}
112+
}
113+
97114
if (!$graphQlOperation->getProvider()) {
98115
$graphQlOperation = $graphQlOperation->withProvider($graphQlOperation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class);
99116
}

src/Laravel/GraphQl/Controller/EntrypointController.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ final class EntrypointController
3232
use ContentNegotiationTrait;
3333
private int $debug;
3434

35+
/**
36+
* @param array<string, string[]> $formats
37+
*/
3538
public function __construct(
3639
private readonly SchemaBuilderInterface $schemaBuilder,
3740
private readonly ExecutorInterface $executor,
@@ -40,6 +43,7 @@ public function __construct(
4043
private readonly ErrorHandlerInterface $errorHandler,
4144
bool $debug = false,
4245
?Negotiator $negotiator = null,
46+
private readonly array $formats = [],
4347
) {
4448
$this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE;
4549
$this->negotiator = $negotiator ?? new Negotiator();
@@ -48,14 +52,23 @@ public function __construct(
4852
public function __invoke(Request $request): Response
4953
{
5054
$formats = ['json' => ['application/json'], 'html' => ['text/html']];
55+
56+
foreach ($this->formats as $k => $f) {
57+
if (!isset($formats[$k])) {
58+
$formats[$k] = $f;
59+
}
60+
}
61+
62+
$this->addRequestFormats($request, $formats);
5163
$format = $this->getRequestFormat($request, $formats, false);
64+
$request->setRequestFormat($format);
5265

5366
try {
5467
if ($request->isMethod('GET') && 'html' === $format) {
5568
return ($this->graphiQlAction)();
5669
}
5770

58-
[$query, $operationName, $variables] = $this->parseRequest($request);
71+
[$query, $operationName, $variables] = $this->parseRequest($request, $format);
5972
if (null === $query) {
6073
throw new BadRequestHttpException('GraphQL query is not valid.');
6174
}
@@ -78,7 +91,7 @@ public function __invoke(Request $request): Response
7891
*
7992
* @return array{0: array<string, mixed>|null, 1: string, 2: array<string, mixed>}
8093
*/
81-
private function parseRequest(Request $request): array
94+
private function parseRequest(Request $request, string $format): array
8295
{
8396
$queryParameters = $request->query->all();
8497
$query = $queryParameters['query'] ?? null;
@@ -91,16 +104,15 @@ private function parseRequest(Request $request): array
91104
return [$query, $operationName, $variables];
92105
}
93106

94-
$contentType = method_exists(Request::class, 'getContentTypeFormat') ? $request->getContentTypeFormat() : $request->getContentType();
95-
if ('json' === $contentType) {
107+
if ('json' === $format) {
96108
return $this->parseData($query, $operationName, $variables, $request->getContent());
97109
}
98110

99-
if ('graphql' === $contentType) {
111+
if ('graphql' === $format) {
100112
$query = $request->getContent();
101113
}
102114

103-
if (\in_array($contentType, ['multipart', 'form'], true)) {
115+
if ('multipart' === $format) {
104116
return $this->parseMultipartRequest($query, $operationName, $variables, $request->request->all(), $request->files->all());
105117
}
106118

src/Laravel/State/AccessCheckerProvider.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
namespace ApiPlatform\Laravel\State;
1515

16+
use ApiPlatform\Metadata\HttpOperation;
1617
use ApiPlatform\Metadata\Operation;
1718
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
1819
use ApiPlatform\State\ProviderInterface;
1920
use Illuminate\Auth\Access\AuthorizationException;
21+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
2022

2123
/**
2224
* Allows access based on the ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface.
@@ -54,7 +56,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5456
];
5557

5658
if (!$this->resourceAccessChecker->isGranted($operation->getClass(), $policy, $resourceAccessCheckerContext)) {
57-
throw new AuthorizationException($message ?? 'Access Denied.');
59+
throw $operation instanceof HttpOperation ? new AuthorizationException($message ?? 'Access Denied.') : new AccessDeniedHttpException($message ?? 'Access Denied.');
5860
}
5961

6062
return $body;

0 commit comments

Comments
 (0)