Skip to content

Commit 6f71d4b

Browse files
committed
Merge 4.0
2 parents a4b79d1 + 001d771 commit 6f71d4b

24 files changed

+456
-115
lines changed

CHANGELOG.md

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## v4.0.6
4+
5+
### Bug fixes
6+
7+
* [195c4e788](https://github.com/api-platform/core/commit/195c4e7883520416e042ac78143b18652a216fbf) fix(hydra): hydra context changed (#6710)
8+
* [4f65ef2d0](https://github.com/api-platform/core/commit/4f65ef2d061215df348e3505856f0f41c7c909ed) fix(metadata): providing parameter constraints skips automatic ones (#6756)
9+
* [5a8ef115a](https://github.com/api-platform/core/commit/5a8ef115a90791992a6c1325fb6d1ac458b22153) fix(symfony): ECMA-262 pattern with RegExp validator (#6733)
10+
* [67c5a2a24](https://github.com/api-platform/core/commit/67c5a2a2463bca94f0997b4fab1248a08994465b) fix(laravel): jsonapi error serialization (#6755)
11+
* [ac6f667f3](https://github.com/api-platform/core/commit/ac6f667f301f6c4c399a707faf00567239bd98d8) fix(laravel): collection relations other than HasMany (#6737)
12+
13+
### Features
14+
15+
* [cecd77149](https://github.com/api-platform/core/commit/cecd77149795c1a455ac72bc3ed0606413e69900) feat(laravel): use laravel cache setting (#6751)
16+
317
## v4.0.5
418

519
### Bug fixes
@@ -152,6 +166,12 @@ Notes:
152166

153167
* [0d5f35683](https://github.com/api-platform/core/commit/0d5f356839eb6aa9f536044abe4affa736553e76) feat(laravel): laravel component (#5882)
154168

169+
## v3.4.5
170+
171+
### Bug fixes
172+
173+
* [fc8fa00a1](https://github.com/api-platform/core/commit/fc8fa00a19320b65547a60537261959c11f8e6a8) fix(hydra): iri template when using query parameter (#6742)
174+
155175
## v3.4.4
156176

157177
### Bug fixes

docs/guides/doctrine-search-filter.php

+4-7
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,21 @@ public function testGetDocumentation(): void
120120
$this->assertJsonContains([
121121
'search' => [
122122
'@type' => 'IriTemplate',
123-
'template' => '/books.jsonld{?id,title,author}',
123+
'template' => '/books.jsonld{?title,author}',
124124
'variableRepresentation' => 'BasicRepresentation',
125125
'mapping' => [
126-
[
127-
'@type' => 'IriTemplateMapping',
128-
'variable' => 'id',
129-
'property' => 'id',
130-
],
131126
[
132127
'@type' => 'IriTemplateMapping',
133128
'variable' => 'title',
134129
'property' => 'title',
130+
'required' => false,
135131
],
136132
[
137133
'@type' => 'IriTemplateMapping',
138134
'variable' => 'author',
139135
'property' => 'author',
140-
]
136+
'required' => false,
137+
],
141138
],
142139
],
143140
]);

src/Hydra/Serializer/CollectionFiltersNormalizer.php

+15-9
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use ApiPlatform\Doctrine\Orm\State\Options;
1818
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
1919
use ApiPlatform\Metadata\FilterInterface;
20-
use ApiPlatform\Metadata\Parameter;
2120
use ApiPlatform\Metadata\Parameters;
2221
use ApiPlatform\Metadata\QueryParameterInterface;
2322
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
@@ -133,10 +132,9 @@ public function setNormalizer(NormalizerInterface $normalizer): void
133132
/**
134133
* Returns the content of the Hydra search property.
135134
*
136-
* @param FilterInterface[] $filters
137-
* @param array<string, Parameter> $parameters
135+
* @param FilterInterface[] $filters
138136
*/
139-
private function getSearch(string $resourceClass, array $parts, array $filters, array|Parameters|null $parameters, string $hydraPrefix): array
137+
private function getSearch(string $resourceClass, array $parts, array $filters, ?Parameters $parameters, string $hydraPrefix): array
140138
{
141139
$variables = [];
142140
$mapping = [];
@@ -153,13 +151,19 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
153151
continue;
154152
}
155153

156-
if (!($property = $parameter->getProperty()) && ($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) {
157-
foreach ($filter->getDescription($resourceClass) as $variable => $description) {
158-
// This is a practice induced by PHP and is not necessary when implementing URI template
154+
if (($filterId = $parameter->getFilter()) && \is_string($filterId) && ($filter = $this->getFilter($filterId))) {
155+
$filterDescription = $filter->getDescription($resourceClass);
156+
157+
foreach ($filterDescription as $variable => $description) {
158+
// // This is a practice induced by PHP and is not necessary when implementing URI template
159159
if (str_ends_with((string) $variable, '[]')) {
160160
continue;
161161
}
162162

163+
if (($prop = $parameter->getProperty()) && ($description['property'] ?? null) !== $prop) {
164+
continue;
165+
}
166+
163167
// :property is a pattern allowed when defining parameters
164168
$k = str_replace(':property', $description['property'], $key);
165169
$variable = str_replace($description['property'], $k, $variable);
@@ -171,10 +175,12 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
171175
$mapping[] = $m;
172176
}
173177

174-
continue;
178+
if ($filterDescription) {
179+
continue;
180+
}
175181
}
176182

177-
if (!$property) {
183+
if (!($property = $parameter->getProperty())) {
178184
continue;
179185
}
180186

src/JsonApi/Serializer/ErrorNormalizer.php

+27-2
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,34 @@ public function normalize(mixed $object, ?string $format = null, array $context
3737
$jsonApiObject = $this->itemNormalizer->normalize($object, $format, $context);
3838
$error = $jsonApiObject['data']['attributes'];
3939
$error['id'] = $jsonApiObject['data']['id'];
40-
$error['type'] = $jsonApiObject['data']['id'];
40+
if (isset($error['type'])) {
41+
$error['links'] = ['type' => $error['type']];
42+
}
43+
44+
if (!isset($error['code']) && method_exists($object, 'getId')) {
45+
$error['code'] = $object->getId();
46+
}
47+
48+
if (!isset($error['violations'])) {
49+
return ['errors' => [$error]];
50+
}
51+
52+
$errors = [];
53+
foreach ($error['violations'] as $violation) {
54+
$e = ['detail' => $violation['message']] + $error;
55+
if (isset($error['links']['type'])) {
56+
$type = $error['links']['type'];
57+
$e['links']['type'] = \sprintf('%s/%s', $type, $violation['propertyPath']);
58+
$e['id'] = str_replace($type, $e['links']['type'], $e['id']);
59+
}
60+
if (isset($e['code'])) {
61+
$e['code'] = \sprintf('%s/%s', $error['code'], $violation['propertyPath']);
62+
}
63+
unset($e['violations']);
64+
$errors[] = $e;
65+
}
4166

42-
return ['errors' => [$error]];
67+
return ['errors' => $errors];
4368
}
4469

4570
/**

src/JsonApi/Serializer/ReservedAttributeNameConverter.php

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\JsonApi\Serializer;
1515

16+
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
1617
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
1718
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
1819

@@ -44,6 +45,10 @@ public function normalize(string $propertyName, ?string $class = null, ?string $
4445
$propertyName = $this->nameConverter->normalize($propertyName, $class, $format, $context);
4546
}
4647

48+
if ($class && is_a($class, ProblemExceptionInterface::class, true)) {
49+
return $propertyName;
50+
}
51+
4752
if (isset(self::JSON_API_RESERVED_ATTRIBUTES[$propertyName])) {
4853
$propertyName = self::JSON_API_RESERVED_ATTRIBUTES[$propertyName];
4954
}

src/Laravel/ApiPlatformProvider.php

+17-9
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory;
5959
use ApiPlatform\JsonApi\Serializer\CollectionNormalizer as JsonApiCollectionNormalizer;
6060
use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer as JsonApiEntrypointNormalizer;
61+
use ApiPlatform\JsonApi\Serializer\ErrorNormalizer as JsonApiErrorNormalizer;
6162
use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer;
6263
use ApiPlatform\JsonApi\Serializer\ObjectNormalizer as JsonApiObjectNormalizer;
6364
use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter;
@@ -297,7 +298,7 @@ public function register(): void
297298
});
298299

299300
$this->app->extend(PropertyMetadataFactoryInterface::class, function (PropertyInfoPropertyMetadataFactory $inner, Application $app) {
300-
/** @var ConfigRepository */
301+
/** @var ConfigRepository $config */
301302
$config = $app['config'];
302303

303304
return new CachePropertyMetadataFactory(
@@ -313,12 +314,12 @@ public function register(): void
313314
$app->make(ResourceClassResolverInterface::class)
314315
),
315316
),
316-
true === $config->get('app.debug') ? 'array' : 'file'
317+
true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file')
317318
);
318319
});
319320

320321
$this->app->singleton(PropertyNameCollectionFactoryInterface::class, function (Application $app) {
321-
/** @var ConfigRepository */
322+
/** @var ConfigRepository $config */
322323
$config = $app['config'];
323324

324325
return new CachePropertyNameCollectionMetadataFactory(
@@ -331,7 +332,7 @@ public function register(): void
331332
)
332333
)
333334
),
334-
true === $config->get('app.debug') ? 'array' : 'file'
335+
true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file')
335336
);
336337
});
337338

@@ -345,7 +346,7 @@ public function register(): void
345346

346347
// TODO: add cached metadata factories
347348
$this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) {
348-
/** @var ConfigRepository */
349+
/** @var ConfigRepository $config */
349350
$config = $app['config'];
350351
$formats = $config->get('api-platform.formats');
351352

@@ -401,7 +402,7 @@ public function register(): void
401402
$app->make('filters')
402403
)
403404
),
404-
true === $config->get('app.debug') ? 'array' : 'file'
405+
true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file')
405406
);
406407
});
407408

@@ -907,6 +908,10 @@ public function register(): void
907908
return new ReservedAttributeNameConverter($app->make(NameConverterInterface::class));
908909
});
909910

911+
if (interface_exists(FieldsBuilderEnumInterface::class)) {
912+
$this->registerGraphQl($this->app);
913+
}
914+
910915
$this->app->singleton(JsonApiEntrypointNormalizer::class, function (Application $app) {
911916
return new JsonApiEntrypointNormalizer(
912917
$app->make(ResourceMetadataCollectionFactoryInterface::class),
@@ -946,9 +951,11 @@ public function register(): void
946951
);
947952
});
948953

949-
if (interface_exists(FieldsBuilderEnumInterface::class)) {
950-
$this->registerGraphQl($this->app);
951-
}
954+
$this->app->singleton(JsonApiErrorNormalizer::class, function (Application $app) {
955+
return new JsonApiErrorNormalizer(
956+
$app->make(JsonApiItemNormalizer::class),
957+
);
958+
});
952959

953960
$this->app->singleton(JsonApiObjectNormalizer::class, function (Application $app) {
954961
return new JsonApiObjectNormalizer(
@@ -985,6 +992,7 @@ public function register(): void
985992
$list->insert($app->make(JsonApiEntrypointNormalizer::class), -800);
986993
$list->insert($app->make(JsonApiCollectionNormalizer::class), -985);
987994
$list->insert($app->make(JsonApiItemNormalizer::class), -890);
995+
$list->insert($app->make(JsonApiErrorNormalizer::class), -790);
988996
$list->insert($app->make(JsonApiObjectNormalizer::class), -995);
989997

990998
if (interface_exists(FieldsBuilderEnumInterface::class)) {

src/Laravel/ApiResource/Error.php

+11-5
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
name: '_api_errors_jsonapi',
5353
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
5454
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true],
55-
uriTemplate: '/errros/{status}.jsonapi'
55+
uriTemplate: '/errors/{status}.jsonapi'
5656
),
5757
],
5858
graphQlOperations: []
@@ -124,6 +124,12 @@ public function getStatusCode(): int
124124
return $this->status;
125125
}
126126

127+
#[Groups(['jsonapi'])]
128+
public function getId(): string
129+
{
130+
return (string) $this->status;
131+
}
132+
127133
/**
128134
* @param array<string, string> $headers
129135
*/
@@ -132,7 +138,7 @@ public function setHeaders(array $headers): void
132138
$this->headers = $headers;
133139
}
134140

135-
#[Groups(['jsonld', 'jsonproblem'])]
141+
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
136142
public function getType(): string
137143
{
138144
return $this->type;
@@ -149,7 +155,7 @@ public function setType(string $type): void
149155
$this->type = $type;
150156
}
151157

152-
#[Groups(['jsonld', 'jsonproblem'])]
158+
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
153159
public function getStatus(): ?int
154160
{
155161
return $this->status;
@@ -160,13 +166,13 @@ public function setStatus(int $status): void
160166
$this->status = $status;
161167
}
162168

163-
#[Groups(['jsonld', 'jsonproblem'])]
169+
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
164170
public function getDetail(): ?string
165171
{
166172
return $this->detail;
167173
}
168174

169-
#[Groups(['jsonld', 'jsonproblem'])]
175+
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
170176
public function getInstance(): ?string
171177
{
172178
return $this->instance;

src/Laravel/ApiResource/ValidationError.php

+7-7
Original file line numberDiff line numberDiff line change
@@ -86,25 +86,25 @@ public function getId(): string
8686
}
8787

8888
#[SerializedName('description')]
89-
#[Groups(['jsonapi', 'jsonld', 'json'])]
89+
#[Groups(['jsonld', 'json'])]
9090
public function getDescription(): string
9191
{
9292
return $this->detail;
9393
}
9494

95-
#[Groups(['jsonld', 'json'])]
95+
#[Groups(['jsonld', 'json', 'jsonapi'])]
9696
public function getType(): string
9797
{
9898
return '/validation_errors/'.$this->id;
9999
}
100100

101-
#[Groups(['jsonld', 'json'])]
101+
#[Groups(['jsonld', 'json', 'jsonapi'])]
102102
public function getTitle(): ?string
103103
{
104104
return 'Validation Error';
105105
}
106106

107-
#[Groups(['jsonld', 'json'])]
107+
#[Groups(['jsonld', 'json', 'jsonapi'])]
108108
private string $detail;
109109

110110
public function getDetail(): ?string
@@ -117,7 +117,7 @@ public function setDetail(string $detail): void
117117
$this->detail = $detail;
118118
}
119119

120-
#[Groups(['jsonld', 'json'])]
120+
#[Groups(['jsonld', 'json', 'jsonapi'])]
121121
public function getStatus(): ?int
122122
{
123123
return $this->status;
@@ -128,7 +128,7 @@ public function setStatus(int $status): void
128128
$this->status = $status;
129129
}
130130

131-
#[Groups(['jsonld', 'json'])]
131+
#[Groups(['jsonld', 'json', 'jsonapi'])]
132132
public function getInstance(): ?string
133133
{
134134
return null;
@@ -138,7 +138,7 @@ public function getInstance(): ?string
138138
* @return array<int,array{propertyPath:string,message:string,code?:string}>
139139
*/
140140
#[SerializedName('violations')]
141-
#[Groups(['json', 'jsonld'])]
141+
#[Groups(['json', 'jsonld', 'jsonapi'])]
142142
public function getViolations(): array
143143
{
144144
return $this->violations;

src/Laravel/State/ValidateProvider.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,14 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7474
return $body;
7575
}
7676

77-
$validator = Validator::make($request->request->all(), $rules);
77+
// In Symfony, validation is done on the Resource object (here $body) using Deserialization before Validation
78+
// Here, we did not deserialize yet, we validate on the raw body before.
79+
$validationBody = $request->request->all();
80+
if ('jsonapi' === $request->getRequestFormat()) {
81+
$validationBody = $validationBody['data']['attributes'];
82+
}
83+
84+
$validator = Validator::make($validationBody, $rules);
7885
if ($validator->fails()) {
7986
throw $this->getValidationError($validator, new ValidationException($validator));
8087
}

0 commit comments

Comments
 (0)