Skip to content

Commit 3caae14

Browse files
committed
Merge 4.3
2 parents 9b1a58f + a5761cc commit 3caae14

24 files changed

Lines changed: 804 additions & 181 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Changelog
22

3+
## v4.3.14
4+
5+
### Bug fixes
6+
7+
* [0d9bcde6b](https://github.com/api-platform/core/commit/0d9bcde6b21d32b448afa6422ac69fae9aa2e7ab) fix(doctrine): filter parent link from uri variables in fetch_data=false reference (#8295)
8+
* [2abda532d](https://github.com/api-platform/core/commit/2abda532d0b8703c23cccd355644a192192f20d3) fix(serializer): fix union types denormalization fallback after security mismatch (#8333)
9+
* [553f6d3c0](https://github.com/api-platform/core/commit/553f6d3c007912761427254097368f43432819a4) fix(openapi): serialize Reference objects with $ref in the generated document (#8306)
10+
* [75c275cd0](https://github.com/api-platform/core/commit/75c275cd07b03c81cfebd2a419e2f12957e1eb23) fix(jsonapi): exclude relations from openapi attributes schema (#8313)
11+
* [8586a80e6](https://github.com/api-platform/core/commit/8586a80e6b8004c1d8f1c612e9d01cf658a3ffc5) fix(mcp): support mcp/sdk 0.6 (#8311, #8302)
12+
* [8999b60ca](https://github.com/api-platform/core/commit/8999b60ca840a5cd3e235a65e77646680c0503a1) fix(jsonapi): correct relationship schemas in generated json schema (#8321)
13+
* [977714184](https://github.com/api-platform/core/commit/97771418458277dba289eb12ad6f2b1c49ec6a3f) fix(doctrine): exclude self-reference relation links from GraphQL root item lookup (#8314)
14+
15+
16+
### Features
17+
18+
* [84e7818d4](https://github.com/api-platform/core/commit/84e7818d4b35ecf4aeb79edc2f0a9746c6474b0f) feat(laravel): boot without a database via dumped metadata (#8290)
19+
320
## v4.3.13
421

522
### Bug fixes

phpstan.neon.dist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ parameters:
8484
path: src/GraphQl/Tests/Type/TypesContainerTest.php
8585

8686
# Expected, due to backward compatibility
87-
- '#Method GraphQL\\Type\\Definition\\WrappingType::getWrappedType\(\) invoked with 1 parameter, 0 required\.#'
8887
- '#Access to an undefined property GraphQL\\Type\\Definition\\NamedType&GraphQL\\Type\\Definition\\Type::\$name\.#'
8988
- "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\PropertyInfo\\\\\\\\PropertyInfoExtractor' and 'getType' will always evaluate to true\\.#"
9089
- "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\HttpFoundation\\\\\\\\Request' and 'getContentTypeFormat' will always evaluate to true\\.#"

src/GraphQl/Type/FieldsBuilder.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -435,11 +435,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
435435

436436
$graphqlWrappedType = $graphqlType;
437437
if ($graphqlType instanceof WrappingType) {
438-
if (method_exists($graphqlType, 'getInnermostType')) {
439-
$graphqlWrappedType = $graphqlType->getInnermostType();
440-
} else {
441-
$graphqlWrappedType = $graphqlType->getWrappedType(true);
442-
}
438+
$graphqlWrappedType = $graphqlType->getInnermostType();
443439
}
444440
$isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
445441
if ($isStandardGraphqlType) {

src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,33 @@ public function testSchemaTypeBuildSchema(): void
161161
$this->assertEquals($resultSchema['allOf'][0]['$ref'], $forcedCollection['allOf'][0]['$ref']);
162162
}
163163

164+
// gen_id=false output schema must not require `@id` (e.g. an operation whose serializer omits the IRI).
165+
public function testGenIdFalseOutputSchemaDoesNotRequireId(): void
166+
{
167+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new Get(), null, ['gen_id' => false]);
168+
169+
$definitions = $resultSchema->getDefinitions();
170+
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
171+
172+
$this->assertSame(['$ref' => '#/definitions/HydraItemBaseSchemaWithoutId'], $definitions[$rootDefinitionKey]['allOf'][0]);
173+
$this->assertArrayHasKey('HydraItemBaseSchemaWithoutId', $definitions);
174+
$this->assertSame(['@type'], $definitions['HydraItemBaseSchemaWithoutId']['required']);
175+
$this->assertArrayNotHasKey('HydraItemBaseSchema', $definitions);
176+
}
177+
178+
// Default (gen_id left to its true default): the output schema keeps `@id` required.
179+
public function testOutputSchemaRequiresIdByDefault(): void
180+
{
181+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new Get());
182+
183+
$definitions = $resultSchema->getDefinitions();
184+
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
185+
186+
$this->assertSame(['$ref' => '#/definitions/HydraItemBaseSchema'], $definitions[$rootDefinitionKey]['allOf'][0]);
187+
$this->assertSame(['@id', '@type'], $definitions['HydraItemBaseSchema']['required']);
188+
$this->assertArrayNotHasKey('HydraItemBaseSchemaWithoutId', $definitions);
189+
}
190+
164191
public function testSchemaTypeBuildSchemaWithoutPrefix(): void
165192
{
166193
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new GetCollection(), null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false]);

src/JsonApi/JsonSchema/SchemaFactory.php

Lines changed: 21 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace ApiPlatform\JsonApi\JsonSchema;
1515

16+
use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter;
17+
use ApiPlatform\JsonApi\Util\ResourceLinkageResolver;
1618
use ApiPlatform\JsonSchema\DefinitionNameFactory;
1719
use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface;
1820
use ApiPlatform\JsonSchema\ResourceMetadataTrait;
@@ -25,12 +27,7 @@
2527
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2628
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2729
use ApiPlatform\Metadata\ResourceClassResolverInterface;
28-
use ApiPlatform\Metadata\Util\TypeHelper;
2930
use ApiPlatform\State\ApiResource\Error;
30-
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
31-
use Symfony\Component\TypeInfo\Type;
32-
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
33-
use Symfony\Component\TypeInfo\Type\ObjectType;
3431

3532
/**
3633
* Decorator factory which adds JSON:API properties to the JSON Schema document.
@@ -83,7 +80,9 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
8380
*/
8481
private $builtSchema = [];
8582

86-
public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null)
83+
private readonly ResourceLinkageResolver $resourceLinkageResolver;
84+
85+
public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null, ?ResourceLinkageResolver $resourceLinkageResolver = null)
8786
{
8887
if (!$definitionNameFactory) {
8988
$this->definitionNameFactory = new DefinitionNameFactory();
@@ -93,6 +92,7 @@ public function __construct(private readonly SchemaFactoryInterface $schemaFacto
9392
}
9493
$this->resourceClassResolver = $resourceClassResolver;
9594
$this->resourceMetadataFactory = $resourceMetadataFactory;
95+
$this->resourceLinkageResolver = $resourceLinkageResolver ?? new ResourceLinkageResolver($resourceClassResolver);
9696
}
9797

9898
/**
@@ -326,28 +326,27 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
326326
foreach (array_keys($refs) as $ref) {
327327
$relatedDefinitions[$ref] = ['$ref' => $ref];
328328
}
329+
// A relationship literally named "relationships"/"included" is prefixed at runtime too.
330+
$relationshipName = ReservedAttributeNameConverter::JSON_API_RESERVED_ATTRIBUTES[$propertyName] ?? $propertyName;
329331
if ($isOne) {
330-
$relationships[$propertyName]['properties']['data'] = [
332+
$relationships[$relationshipName]['properties']['data'] = [
331333
'oneOf' => [
332334
['type' => 'null'],
333335
self::RELATION_PROPS,
334336
],
335337
];
336338
continue;
337339
}
338-
$relationships[$propertyName]['properties']['data'] = [
340+
$relationships[$relationshipName]['properties']['data'] = [
339341
'type' => 'array',
340342
'items' => self::RELATION_PROPS,
341343
];
342344
continue;
343345
}
344346

345-
if ('id' === $propertyName) {
346-
// should probably be renamed "lid" and moved to the above node
347-
$attributes['_id'] = $property;
348-
continue;
349-
}
350-
$attributes[$propertyName] = $property;
347+
// Reserved names (id, type, links, relationships, included) are prefixed by the
348+
// ReservedAttributeNameConverter at runtime; mirror that single source of truth here.
349+
$attributes[ReservedAttributeNameConverter::JSON_API_RESERVED_ATTRIBUTES[$propertyName] ?? $propertyName] = $property;
351350
}
352351

353352
$replacement = self::PROPERTY_PROPS;
@@ -391,67 +390,21 @@ private function getRelationship(string $resourceClass, string $property, ?array
391390
{
392391
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $serializerContext ?? []);
393392

394-
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
395-
$types = $propertyMetadata->getBuiltinTypes() ?? [];
396-
$isRelationship = false;
397-
$isOne = $isMany = false;
398-
$relatedClasses = [];
399-
400-
foreach ($types as $type) {
401-
if ($type->isCollection()) {
402-
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
403-
$isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
404-
} else {
405-
$isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
406-
}
407-
if (!isset($className) || (!$isOne && !$isMany)) {
408-
continue;
409-
}
410-
$isRelationship = true;
411-
$resourceMetadata = $this->resourceMetadataFactory->create($className);
412-
$operation = $resourceMetadata->getOperation();
413-
// @see https://github.com/api-platform/core/issues/5501
414-
// @see https://github.com/api-platform/core/pull/5722
415-
$relatedClasses[$className] = $operation->canRead();
416-
}
417-
418-
return $isRelationship ? [$isOne, $relatedClasses] : null;
419-
}
420-
421-
if (null === $type = $propertyMetadata->getNativeType()) {
393+
// Share the runtime attributes/relationships split so the generated schema cannot drift from the document.
394+
$relationships = $this->resourceLinkageResolver->getRelationships($propertyMetadata);
395+
if ([] === $relationships) {
422396
return null;
423397
}
424398

425-
$isRelationship = false;
426-
$isOne = $isMany = false;
399+
$isOne = false;
427400
$relatedClasses = [];
428-
429-
/** @var class-string|null $className */
430-
$className = null;
431-
432-
$typeIsResourceClass = function (Type $type) use (&$className): bool {
433-
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
434-
};
435-
436-
foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
437-
if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) {
438-
$isMany = true;
439-
} elseif ($t->isSatisfiedBy($typeIsResourceClass)) {
440-
$isOne = true;
441-
}
442-
443-
if (!$className || (!$isOne && !$isMany)) {
444-
continue;
445-
}
446-
447-
$isRelationship = true;
448-
$resourceMetadata = $this->resourceMetadataFactory->create($className);
449-
$operation = $resourceMetadata->getOperation();
401+
foreach ($relationships as [$className, $isCollection]) {
402+
$isOne = $isOne || !$isCollection;
450403
// @see https://github.com/api-platform/core/issues/5501
451404
// @see https://github.com/api-platform/core/pull/5722
452-
$relatedClasses[$className] = $operation->canRead();
405+
$relatedClasses[$className] = $this->resourceMetadataFactory->create($className)->getOperation()->canRead();
453406
}
454407

455-
return $isRelationship ? [$isOne, $relatedClasses] : null;
408+
return [$isOne, $relatedClasses];
456409
}
457410
}

0 commit comments

Comments
 (0)