Skip to content

Commit 9e382e0

Browse files
committed
Merge 4.1
2 parents b747ab7 + 2d501b3 commit 9e382e0

File tree

30 files changed

+871
-33
lines changed

30 files changed

+871
-33
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ TypeInfo:
4040
* [cff61eab8](https://github.com/api-platform/core/commit/cff61eab8643f8ed08d59c0684e77740d0d81b04) fix(metadata): append php file resource extractor (#7193)
4141
* [f3d4afe03](https://github.com/api-platform/core/commit/f3d4afe032385f3b665131a365e42706930f0730) fix(symfony): validator type-info
4242

43+
## v4.1.20
44+
45+
### Bug fixes
46+
47+
* [c41a0bca4](https://github.com/api-platform/core/commit/c41a0bca49778663743a52e9da9ddb3b1439c2f8) fix(jsonld): child class @type shortName (#7312)
48+
* [d26088ba1](https://github.com/api-platform/core/commit/d26088ba1080f9fb8d94db5b476b0322d2a14ab2) chore: allow doctrine-persistence:^4.0 (#7309)
49+
50+
## v4.1.19
51+
52+
### Bug fixes
53+
54+
* [2c06a22e2](https://github.com/api-platform/core/commit/2c06a22e244e5b0683558589a7ed2d7dd34a16a2) fix(validation): moving dependency from require-dev to require (#7296)
55+
* [2cde06246](https://github.com/api-platform/core/commit/2cde06246cd593ad094de9c8f0ad1b178608f275) fix(openapi): output `partial` query parameter to OpenAPI when `pagination_client_enabled` is true (#7295)
56+
* [6bc112193](https://github.com/api-platform/core/commit/6bc11219320315af1a811ab49a4c451498f75430) fix(metadata): do not fail if phpstan/phpdoc-parser is missing (#7279)
57+
* [871e5d3e1](https://github.com/api-platform/core/commit/871e5d3e1916e0d8bd1b4e1c4d983cd270a0922f) fix(symfony): restore graphql_playground option (#7274)
58+
* [d1e6772e3](https://github.com/api-platform/core/commit/d1e6772e32f632a5612f79ec58cad874af938694) fix(validation): property path on deepObject style (#7179)
59+
* [d35e46b14](https://github.com/api-platform/core/commit/d35e46b1426063dbed4b59d1f07dbbde398a390e) fix(hydra): "property" may not be defined (#7293)
60+
* [f3a54a239](https://github.com/api-platform/core/commit/f3a54a239e21c18716967e579d2cd2120a52ece1) fix: json formatted resource should not get xml errors #7287 (#7297)
61+
4362
## v4.1.18
4463

4564
### Bug fixes

src/Doctrine/Common/Filter/DateFilterTrait.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ private function normalizeValue(mixed $value, string $operator): ?string
9191
return null;
9292
}
9393

94+
if ('' === $value) {
95+
$this->getLogger()->notice('Invalid filter ignored', [
96+
'exception' => new InvalidArgumentException(\sprintf('Invalid value for "[%s]", expected non-empty string', $operator)),
97+
]);
98+
99+
return null;
100+
}
101+
94102
return $value;
95103
}
96104
}

src/JsonApi/Serializer/ConstraintViolationListNormalizer.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,13 @@ private function getSourcePointerFromViolation(ConstraintViolationInterface $vio
7676
return 'data';
7777
}
7878

79-
$class = $violation->getRoot()::class;
79+
$root = $violation->getRoot();
80+
81+
if (!\is_object($root)) {
82+
return "data/attributes/$fieldName";
83+
}
84+
85+
$class = $root::class;
8086
$propertyMetadata = $this->propertyMetadataFactory
8187
->create(
8288
// Im quite sure this requires some thought in case of validations over relationships

src/JsonApi/Tests/Serializer/ConstraintViolationNormalizerTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,61 @@ public function testNormalize(): void
9393
(new ConstraintViolationListNormalizer($propertyMetadataFactoryProphecy->reveal(), $nameConverterProphecy->reveal()))->normalize($constraintViolationList)
9494
);
9595
}
96+
97+
public function testNormalizeWithStringRoot(): void
98+
{
99+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
100+
101+
// Create a violation with a string root (simulating query parameter validation)
102+
$constraintViolationList = new ConstraintViolationList([
103+
new ConstraintViolation('Invalid page value.', 'Invalid page value.', [], 'page', 'page', 'invalid'),
104+
]);
105+
106+
$normalizer = new ConstraintViolationListNormalizer($propertyMetadataFactoryProphecy->reveal());
107+
108+
$result = $normalizer->normalize($constraintViolationList);
109+
110+
$this->assertEquals(
111+
[
112+
'errors' => [
113+
[
114+
'detail' => 'Invalid page value.',
115+
'source' => [
116+
'pointer' => 'data/attributes/page',
117+
],
118+
],
119+
],
120+
],
121+
$result
122+
);
123+
}
124+
125+
public function testNormalizeWithNullRoot(): void
126+
{
127+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
128+
129+
// Create a violation with a null root
130+
$constraintViolationList = new ConstraintViolationList([
131+
new ConstraintViolation('Invalid value.', 'Invalid value.', [], null, 'field', 'invalid'),
132+
]);
133+
134+
$normalizer = new ConstraintViolationListNormalizer($propertyMetadataFactoryProphecy->reveal());
135+
136+
// This should not throw a TypeError and should handle the null root gracefully
137+
$result = $normalizer->normalize($constraintViolationList);
138+
139+
$this->assertEquals(
140+
[
141+
'errors' => [
142+
[
143+
'detail' => 'Invalid value.',
144+
'source' => [
145+
'pointer' => 'data/attributes/field',
146+
],
147+
],
148+
],
149+
],
150+
$result
151+
);
152+
}
96153
}

src/JsonLd/ContextBuilder.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ public function getAnonymousResourceContext(object $object, array $context = [],
148148
}
149149

150150
// here the object can be different from the resource given by the $context['api_resource'] value
151+
// TODO: this is probably not used anymore and is slow we get that @type way earlier, remove this
151152
if (isset($context['api_resource'])) {
152153
$jsonLdContext['@type'] = $this->resourceMetadataFactory->create($this->getObjectClass($context['api_resource']))[0]->getShortName();
153154
}

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ public function normalize(mixed $object, ?string $format = null, array $context
118118
$context['output']['iri'] = null;
119119
}
120120

121+
if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
122+
$context['output']['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
123+
}
124+
121125
// We should improve what's behind the context creation, its probably more complicated then it should
122126
$metadata = $this->createJsonLdContext($this->contextBuilder, $object, $context);
123127
}

src/Laravel/ApiPlatformProvider.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata;
8787
use ApiPlatform\Laravel\Eloquent\Metadata\ResourceClassResolver as EloquentResourceClassResolver;
8888
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
89+
use ApiPlatform\Laravel\Eloquent\PropertyInfo\EloquentExtractor;
90+
use ApiPlatform\Laravel\Eloquent\Serializer\EloquentNameConverter;
8991
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
9092
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
9193
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
@@ -195,15 +197,16 @@ public function register(): void
195197
{
196198
$this->mergeConfigFrom(__DIR__.'/config/api-platform.php', 'api-platform');
197199

198-
$this->app->singleton(PropertyInfoExtractorInterface::class, function () {
200+
$this->app->singleton(PropertyInfoExtractorInterface::class, function (Application $app) {
199201
$phpstanExtractor = class_exists(PhpDocParser::class) ? new PhpStanExtractor() : null;
200202
$reflectionExtractor = new ReflectionExtractor();
203+
$eloquentExtractor = new EloquentExtractor($app->make(ModelMetadata::class));
201204

202205
return new PropertyInfoExtractor(
203206
[$reflectionExtractor],
204207
$phpstanExtractor ? [$phpstanExtractor, $reflectionExtractor] : [$reflectionExtractor],
205208
[],
206-
[$reflectionExtractor],
209+
[$eloquentExtractor],
207210
[$reflectionExtractor]
208211
);
209212
});
@@ -262,10 +265,10 @@ public function register(): void
262265
return new CachePropertyMetadataFactory(
263266
new SchemaPropertyMetadataFactory(
264267
$app->make(ResourceClassResolverInterface::class),
265-
new PropertyInfoPropertyMetadataFactory(
266-
$app->make(PropertyInfoExtractorInterface::class),
267-
new SerializerPropertyMetadataFactory(
268-
$app->make(SerializerClassMetadataFactory::class),
268+
new SerializerPropertyMetadataFactory(
269+
$app->make(SerializerClassMetadataFactory::class),
270+
new PropertyInfoPropertyMetadataFactory(
271+
$app->make(PropertyInfoExtractorInterface::class),
269272
new AttributePropertyMetadataFactory(
270273
new EloquentAttributePropertyMetadataFactory(
271274
new EloquentPropertyMetadataFactory(
@@ -315,7 +318,7 @@ public function register(): void
315318
$config = $app['config'];
316319
$nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class);
317320
if ($nameConverter && class_exists($nameConverter)) {
318-
$nameConverter = $app->make($nameConverter);
321+
$nameConverter = new EloquentNameConverter($app->make($nameConverter));
319322
}
320323

321324
$defaultContext = $config->get('api-platform.serializer', []);
@@ -400,9 +403,13 @@ public function register(): void
400403
});
401404
$this->app->bind(SerializerContextBuilderInterface::class, EloquentSerializerContextBuilder::class);
402405
$this->app->singleton(EloquentSerializerContextBuilder::class, function (Application $app) {
406+
/** @var ConfigRepository */
407+
$config = $app['config'];
408+
403409
return new EloquentSerializerContextBuilder(
404410
$app->make(SerializerContextBuilder::class),
405-
$app->make(PropertyNameCollectionFactoryInterface::class)
411+
$app->make(PropertyNameCollectionFactoryInterface::class),
412+
$config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class)
406413
);
407414
});
408415

src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function create(string $resourceClass, string $property, array $options =
6565
}
6666

6767
if ($model->getKeyName() === $property) {
68-
$propertyMetadata = $propertyMetadata->withIdentifier(true)->withWritable($propertyMetadata->isWritable() ?? false);
68+
$propertyMetadata = $propertyMetadata->withIdentifier(true);
6969
}
7070

7171
foreach ($this->modelMetadata->getAttributes($model) as $p) {
@@ -94,17 +94,6 @@ public function create(string $resourceClass, string $property, array $options =
9494
$propertyMetadata = $propertyMetadata
9595
->withNativeType($type);
9696

97-
// If these are set let the SerializerPropertyMetadataFactory do the work
98-
if (!isset($options['denormalization_groups'])) {
99-
$propertyMetadata = $propertyMetadata
100-
->withWritable($propertyMetadata->isWritable() ?? true === $p['fillable']);
101-
}
102-
103-
if (!isset($options['normalization_groups'])) {
104-
$propertyMetadata = $propertyMetadata
105-
->withReadable($propertyMetadata->isReadable() ?? false === $p['hidden']);
106-
}
107-
10897
return $propertyMetadata;
10998
}
11099

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Laravel\Eloquent\PropertyInfo;
15+
16+
use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata;
17+
use Illuminate\Database\Eloquent\Model;
18+
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
19+
20+
class EloquentExtractor implements PropertyAccessExtractorInterface
21+
{
22+
public function __construct(private readonly ModelMetadata $modelMetadata)
23+
{
24+
}
25+
26+
/**
27+
* @param array<string, mixed> $context
28+
*/
29+
public function isReadable(string $class, string $property, array $context = []): ?bool
30+
{
31+
if (!is_a($class, Model::class, true)) {
32+
return null;
33+
}
34+
35+
try {
36+
$refl = new \ReflectionClass($class);
37+
$model = $refl->newInstanceWithoutConstructor();
38+
} catch (\ReflectionException) {
39+
return null;
40+
}
41+
42+
foreach ($this->modelMetadata->getAttributes($model) as $p) {
43+
if ($p['name'] !== $property) {
44+
continue;
45+
}
46+
47+
if (($visible = $model->getVisible()) && \in_array($property, $visible, true)) {
48+
return true;
49+
}
50+
51+
if (($hidden = $model->getHidden()) && \in_array($property, $hidden, true)) {
52+
return false;
53+
}
54+
55+
return true;
56+
}
57+
58+
return null;
59+
}
60+
61+
/**
62+
* @param array<string, mixed> $context
63+
*/
64+
public function isWritable(string $class, string $property, array $context = []): ?bool
65+
{
66+
if (!is_a($class, Model::class, true)) {
67+
return null;
68+
}
69+
70+
try {
71+
$refl = new \ReflectionClass($class);
72+
$model = $refl->newInstanceWithoutConstructor();
73+
} catch (\ReflectionException) {
74+
return null;
75+
}
76+
77+
foreach ($this->modelMetadata->getAttributes($model) as $p) {
78+
if ($p['name'] !== $property) {
79+
continue;
80+
}
81+
82+
if ($fillable = $model->getFillable()) {
83+
return \in_array($property, $fillable, true);
84+
}
85+
86+
return true;
87+
}
88+
89+
return null;
90+
}
91+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Laravel\Eloquent\Serializer;
15+
16+
use Symfony\Component\Serializer\Exception\UnexpectedPropertyException;
17+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
18+
19+
final class EloquentNameConverter implements NameConverterInterface
20+
{
21+
public function __construct(private readonly NameConverterInterface $nameConverter)
22+
{
23+
}
24+
25+
/**
26+
* @param array<string, mixed> $context
27+
*/
28+
public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
29+
{
30+
try {
31+
return $this->nameConverter->normalize($propertyName, $class, $format, $context); // @phpstan-ignore-line
32+
} catch (UnexpectedPropertyException $e) {
33+
return $this->nameConverter->denormalize($propertyName, $class, $format, $context); // @phpstan-ignore-line
34+
}
35+
}
36+
37+
/**
38+
* @param array<string, mixed> $context
39+
*/
40+
public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
41+
{
42+
try {
43+
return $this->nameConverter->denormalize($propertyName, $class, $format, $context); // @phpstan-ignore-line
44+
} catch (UnexpectedPropertyException $e) {
45+
return $this->nameConverter->normalize($propertyName, $class, $format, $context); // @phpstan-ignore-line
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)