Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 66 additions & 8 deletions src/JsonApi/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\State\ApiResource\Error;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;

/**
* Decorator factory which adds JSON:API properties to the JSON Schema document.
Expand Down Expand Up @@ -286,21 +292,73 @@
private function getRelationship(string $resourceClass, string $property, ?array $serializerContext): ?array
{
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $serializerContext ?? []);
$types = $propertyMetadata->getBuiltinTypes() ?? [];

if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$types = $propertyMetadata->getBuiltinTypes() ?? [];
$isRelationship = false;
$isOne = $isMany = false;
$relatedClasses = [];

Check warning on line 300 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L297-L300

Added lines #L297 - L300 were not covered by tests

foreach ($types as $type) {
if ($type->isCollection()) {
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
$isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);

Check warning on line 305 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L302-L305

Added lines #L302 - L305 were not covered by tests
} else {
$isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className);

Check warning on line 307 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L307

Added line #L307 was not covered by tests
}
if (!isset($className) || (!$isOne && !$isMany)) {
continue;

Check warning on line 310 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L309-L310

Added lines #L309 - L310 were not covered by tests
}
$isRelationship = true;
$resourceMetadata = $this->resourceMetadataFactory->create($className);
$operation = $resourceMetadata->getOperation();

Check warning on line 314 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L312-L314

Added lines #L312 - L314 were not covered by tests
// @see https://github.com/api-platform/core/issues/5501
// @see https://github.com/api-platform/core/pull/5722
$relatedClasses[$className] = $operation->canRead();

Check warning on line 317 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L317

Added line #L317 was not covered by tests
}

return $isRelationship ? [$isOne, $relatedClasses] : null;

Check warning on line 320 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L320

Added line #L320 was not covered by tests
}

if (null === $type = $propertyMetadata->getNativeType()) {
return null;
}

$isRelationship = false;
$isOne = $isMany = false;
$relatedClasses = [];

foreach ($types as $type) {
if ($type->isCollection()) {
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
$isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
} else {
$isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
/** @var class-string|null $className */
$className = null;

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {

Check warning on line 335 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L335

Added line #L335 was not covered by tests
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
};

Check warning on line 339 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L339

Added line #L339 was not covered by tests
};

$collectionValueIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
return match (true) {

Check warning on line 343 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L343

Added line #L343 was not covered by tests
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => false,
};

Check warning on line 348 in src/JsonApi/JsonSchema/SchemaFactory.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/JsonSchema/SchemaFactory.php#L348

Added line #L348 was not covered by tests
};

foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
if ($t->isSatisfiedBy($collectionValueIsResourceClass)) {
$isMany = true;
} elseif ($t->isSatisfiedBy($typeIsResourceClass)) {
$isOne = true;
}
if (!isset($className) || (!$isOne && !$isMany)) {

if (!$className || (!$isOne && !$isMany)) {
continue;
}

$isRelationship = true;
$resourceMetadata = $this->resourceMetadataFactory->create($className);
$operation = $resourceMetadata->getOperation();
Expand Down
25 changes: 22 additions & 3 deletions src/JsonApi/Serializer/ConstraintViolationListNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
namespace ApiPlatform\JsonApi\Serializer;

use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;

Expand Down Expand Up @@ -83,9 +88,23 @@
$fieldName = $this->nameConverter->normalize($fieldName, $class, self::FORMAT);
}

$type = $propertyMetadata->getBuiltinTypes()[0] ?? null;
if ($type && null !== $type->getClassName()) {
return "data/relationships/$fieldName";
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$type = $propertyMetadata->getBuiltinTypes()[0] ?? null;
if ($type && null !== $type->getClassName()) {
return "data/relationships/$fieldName";

Check warning on line 94 in src/JsonApi/Serializer/ConstraintViolationListNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ConstraintViolationListNormalizer.php#L92-L94

Added lines #L92 - L94 were not covered by tests
}
} else {
$typeIsObject = static function (Type $type) use (&$typeIsObject): bool {
return match (true) {

Check warning on line 98 in src/JsonApi/Serializer/ConstraintViolationListNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ConstraintViolationListNormalizer.php#L98

Added line #L98 was not covered by tests
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsObject),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsObject),
default => $type instanceof ObjectType,
};

Check warning on line 102 in src/JsonApi/Serializer/ConstraintViolationListNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ConstraintViolationListNormalizer.php#L102

Added line #L102 was not covered by tests
};

if ($propertyMetadata->getNativeType()?->isSatisfiedBy($typeIsObject)) {
return "data/relationships/$fieldName";

Check warning on line 106 in src/JsonApi/Serializer/ConstraintViolationListNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ConstraintViolationListNormalizer.php#L106

Added line #L106 was not covered by tests
}
}

return "data/attributes/$fieldName";
Expand Down
143 changes: 107 additions & 36 deletions src/JsonApi/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@
use ApiPlatform\Serializer\TagCollectorInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;

/**
* Converts between objects and array.
Expand Down Expand Up @@ -319,50 +325,115 @@
->propertyMetadataFactory
->create($context['resource_class'], $attribute, $options);

$types = $propertyMetadata->getBuiltinTypes() ?? [];

// prevent declaring $attribute as attribute if it's already declared as relationship
$isRelationship = false;

foreach ($types as $type) {
$isOne = $isMany = false;
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$types = $propertyMetadata->getBuiltinTypes() ?? [];

Check warning on line 332 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L332

Added line #L332 was not covered by tests

if ($type->isCollection()) {
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
$isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
} else {
$isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
}
foreach ($types as $type) {
$isOne = $isMany = false;

Check warning on line 335 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L334-L335

Added lines #L334 - L335 were not covered by tests

if (!isset($className) || !$isOne && !$isMany) {
// don't declare it as an attribute too quick: maybe the next type is a valid resource
continue;
}
if ($type->isCollection()) {
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
$isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);

Check warning on line 339 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L337-L339

Added lines #L337 - L339 were not covered by tests
} else {
$isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className);

Check warning on line 341 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L341

Added line #L341 was not covered by tests
}

if (!isset($className) || !$isOne && !$isMany) {

Check warning on line 344 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L344

Added line #L344 was not covered by tests
// don't declare it as an attribute too quick: maybe the next type is a valid resource
continue;

Check warning on line 346 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L346

Added line #L346 was not covered by tests
}

$relation = [
'name' => $attribute,
'type' => $this->getResourceShortName($className),
'cardinality' => $isOne ? 'one' : 'many',
];

Check warning on line 353 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L349-L353

Added lines #L349 - L353 were not covered by tests

// if we specify the uriTemplate, generates its value for link definition
// @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
if ($itemUriTemplate = $propertyMetadata->getUriTemplate()) {
$attributeValue = $this->propertyAccessor->getValue($object, $attribute);
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
$childContext = $this->createChildContext($context, $attribute, $format);
unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']);

Check warning on line 361 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L357-L361

Added lines #L357 - L361 were not covered by tests

$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(
operationName: $itemUriTemplate,
httpOperation: true
);

Check warning on line 366 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L363-L366

Added lines #L363 - L366 were not covered by tests

$components['links'][$attribute] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);

Check warning on line 368 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L368

Added line #L368 was not covered by tests
}

$relation = [
'name' => $attribute,
'type' => $this->getResourceShortName($className),
'cardinality' => $isOne ? 'one' : 'many',
];

// if we specify the uriTemplate, generates its value for link definition
// @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
if ($itemUriTemplate = $propertyMetadata->getUriTemplate()) {
$attributeValue = $this->propertyAccessor->getValue($object, $attribute);
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
$childContext = $this->createChildContext($context, $attribute, $format);
unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']);

$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(
operationName: $itemUriTemplate,
httpOperation: true
);

$components['links'][$attribute] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
$components['relationships'][] = $relation;
$isRelationship = true;

Check warning on line 372 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L371-L372

Added lines #L371 - L372 were not covered by tests
}
} else {
if ($type = $propertyMetadata->getNativeType()) {
/** @var class-string|null $className */
$className = null;

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {

Check warning on line 380 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L380

Added line #L380 was not covered by tests
$type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => false,
};

Check warning on line 385 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L385

Added line #L385 was not covered by tests
};

$collectionValueIsResourceClass = function (Type $type) use ($typeIsResourceClass, &$collectionValueIsResourceClass): bool {
return match (true) {

Check warning on line 389 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L389

Added line #L389 was not covered by tests
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueIsResourceClass),
default => false,
};

Check warning on line 394 in src/JsonApi/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Serializer/ItemNormalizer.php#L394

Added line #L394 was not covered by tests
};

foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
$isOne = $isMany = false;

if ($t->isSatisfiedBy($collectionValueIsResourceClass)) {
$isMany = true;
} elseif ($t->isSatisfiedBy($typeIsResourceClass)) {
$isOne = true;
}

if (!$className || (!$isOne && !$isMany)) {
// don't declare it as an attribute too quick: maybe the next type is a valid resource
continue;
}

$components['relationships'][] = $relation;
$isRelationship = true;
$relation = [
'name' => $attribute,
'type' => $this->getResourceShortName($className),
'cardinality' => $isOne ? 'one' : 'many',
];

// if we specify the uriTemplate, generates its value for link definition
// @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
if ($itemUriTemplate = $propertyMetadata->getUriTemplate()) {
$attributeValue = $this->propertyAccessor->getValue($object, $attribute);
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
$childContext = $this->createChildContext($context, $attribute, $format);
unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']);

$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(
operationName: $itemUriTemplate,
httpOperation: true
);

$components['links'][$attribute] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
}

$components['relationships'][] = $relation;
$isRelationship = true;
}
}
}

// if all types are not relationships, declare it as an attribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
Expand Down Expand Up @@ -50,8 +50,8 @@
public function testNormalize(): void
{
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)]))->shouldBeCalledTimes(1);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalledTimes(1);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy')->willReturn((new ApiProperty())->withNativeType(Type::object(RelatedDummy::class)))->shouldBeCalledTimes(1);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalledTimes(1);

Check warning on line 54 in src/JsonApi/Tests/Serializer/ConstraintViolationNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

src/JsonApi/Tests/Serializer/ConstraintViolationNormalizerTest.php#L53-L54

Added lines #L53 - L54 were not covered by tests

$nameConverterProphecy = $this->prophesize(NameConverterInterface::class);
$nameConverterProphecy->normalize('relatedDummy', Dummy::class, 'jsonapi')->willReturn('relatedDummy')->shouldBeCalledTimes(1);
Expand Down
Loading
Loading