Skip to content
Closed
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
25 changes: 25 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,28 @@ parameters:
identifier: function.alreadyNarrowedType
path: tests/Symfony/Bundle/Command/DebugResourceCommandTest.php
reportUnmatched: false

# Structural assertions that document the ApiProperty internal API surface
# (see #8173). PHPStan correctly sees the methods as defined on this branch,
# but the assertions guard against accidental removal in future releases.
-
identifier: function.alreadyNarrowedType
path: src/Metadata/Tests/Property/Factory/InternalBuiltinTypesDeprecationTest.php
reportUnmatched: false

# Split-package consumers of ApiPlatform\Metadata guard internal accessors with
# method_exists() so the "lowest" dependency builds (api-platform/metadata <= 4.3.7,
# which lacks internalGetBuiltinTypes()) still install. PHPStan sees the method
# as always-defined when analysing the monorepo, hence the narrowed-type notice.
-
identifier: function.alreadyNarrowedType
paths:
- src/Elasticsearch
- src/GraphQl
- src/Hal
- src/Hydra
- src/JsonApi
- src/JsonSchema
- src/Serializer
- src/Symfony/Validator
reportUnmatched: false
2 changes: 1 addition & 1 deletion src/Elasticsearch/Filter/AbstractFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ protected function getLegacyMetadata(string $resourceClass, string $property): a
return $noop;
}

$types = $propertyMetadata->getBuiltinTypes();
$types = method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes();

if (null === $types) {
return $noop;
Expand Down
2 changes: 1 addition & 1 deletion src/Elasticsearch/Util/FieldDatatypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private function getNestedFieldPath(string $resourceClass, string $property): ?s
}

if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$types = $propertyMetadata->getBuiltinTypes() ?? [];
$types = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];

foreach ($types as $type) {
if (
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Resolver/Factory/ResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
return $body;
}
} else {
$type = $propertyMetadata?->getBuiltinTypes()[0] ?? null;
$type = ($propertyMetadata && method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata?->getBuiltinTypes())[0] ?? null;

// Data already fetched and normalized (field or nested resource)
if ($body || null === $resourceClass || ($type && !$type->isCollection())) {
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $context);

if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$propertyTypes = $propertyMetadata->getBuiltinTypes();
$propertyTypes = method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes();

if (
!$propertyTypes
Expand Down
2 changes: 1 addition & 1 deletion src/Hal/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ private function getComponents(object $object, ?string $format, array $context):
/** @var class-string|null $className */
$className = null;
} else {
$types = $propertyMetadata->getBuiltinTypes() ?? [];
$types = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];
}

// prevent declaring $attribute as attribute if it's already declared as relationship
Expand Down
4 changes: 2 additions & 2 deletions src/Hydra/Serializer/DocumentationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ private function getRange(ApiProperty $propertyMetadata): array|string|null
}
// TODO: remove in 5.x
} else {
$builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
$builtInTypes = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];

foreach ($builtInTypes as $type) {
if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) {
Expand Down Expand Up @@ -501,7 +501,7 @@ private function isSingleRelation(ApiProperty $propertyMetadata): bool
}

// TODO: remove in 5.x
$builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
$builtInTypes = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];

foreach ($builtInTypes as $type) {
$className = $type->getClassName();
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApi/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ private function getRelationship(string $resourceClass, string $property, ?array
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $serializerContext ?? []);

if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$types = $propertyMetadata->getBuiltinTypes() ?? [];
$types = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];
$isRelationship = false;
$isOne = $isMany = false;
$relatedClasses = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private function getSourcePointerFromViolation(ConstraintViolationInterface $vio
}

if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$type = $propertyMetadata->getBuiltinTypes()[0] ?? null;
$type = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes())[0] ?? null;
if ($type && null !== $type->getClassName()) {
return "data/relationships/$fieldName";
}
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApi/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ private function getComponents(object $object, ?string $format, array $context):
$isRelationship = false;

if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$types = $propertyMetadata->getBuiltinTypes() ?? [];
$types = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];

foreach ($types as $type) {
$isOne = $isMany = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ private function getClassSchemaDefinition(?string $className, ?bool $readableLin

private function getLegacyTypeSchema(ApiProperty $propertyMetadata, array $propertySchema, string $resourceClass, string $property, ?bool $link): array
{
$types = $propertyMetadata->getBuiltinTypes() ?? [];
$types = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];
$className = ($types[0] ?? null)?->getClassName() ?? null;

if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) && !$className) {
Expand Down
2 changes: 1 addition & 1 deletion src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private function buildLegacyPropertySchema(Schema $schema, string $definitionNam
return;
}

$types = $propertyMetadata->getBuiltinTypes() ?? [];
$types = (method_exists($propertyMetadata, 'internalGetBuiltinTypes') ? $propertyMetadata->internalGetBuiltinTypes() : $propertyMetadata->getBuiltinTypes()) ?? [];

// never override the following keys if at least one is already set
// or if property has no type(s) defined
Expand Down
30 changes: 26 additions & 4 deletions src/Metadata/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,29 @@ public function getBuiltinTypes(): ?array
{
trigger_deprecation('api-platform/metadata', '4.2', 'The "%s()" method is deprecated, use "%s::getNativeType()" instead.', __METHOD__, self::class);

return $this->internalGetBuiltinTypes();
}

/**
* deprecated since 4.2, use "withNativeType" instead.
*
* @param LegacyType[] $builtinTypes
*/
public function withBuiltinTypes(array $builtinTypes = []): static
{
trigger_deprecation('api-platform/metadata', '4.2', 'The "%s()" method is deprecated, use "%s::withNativeType()" instead.', __METHOD__, self::class);

return $this->internalWithBuiltinTypes($builtinTypes);
}

/**
* @internal Same as {@see getBuiltinTypes()} but without the deprecation; used by API Platform's
* own factories that still need the legacy representation on symfony/property-info < 7.1.
*
* @return LegacyType[]|null
*/
public function internalGetBuiltinTypes(): ?array
{
if (null === $this->builtinTypes && null !== $this->nativeType) {
$this->builtinTypes = PropertyInfoToTypeInfoHelper::convertTypeToLegacyTypes($this->nativeType) ?? [];
}
Expand All @@ -542,14 +565,13 @@ public function getBuiltinTypes(): ?array
}

/**
* deprecated since 4.2, use "withNativeType" instead.
* @internal Same as {@see withBuiltinTypes()} but without the deprecation; used by API Platform's
* own factories that still produce legacy types on symfony/property-info < 7.1.
*
* @param LegacyType[] $builtinTypes
*/
public function withBuiltinTypes(array $builtinTypes = []): static
public function internalWithBuiltinTypes(array $builtinTypes = []): static
{
trigger_deprecation('api-platform/metadata', '4.2', 'The "%s()" method is deprecated, use "%s::withNativeType()" instead.', __METHOD__, self::class);

$self = clone $this;
$self->builtinTypes = $builtinTypes;
$self->nativeType = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType($builtinTypes);
Expand Down
2 changes: 1 addition & 1 deletion src/Metadata/IdentifiersExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ private function getIdentifierValue(object $item, string $class, string $propert

// TODO: remove in 5.x
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$types = $propertyMetadata->getBuiltinTypes();
$types = $propertyMetadata->internalGetBuiltinTypes();
if (null === ($type = $types[0] ?? null)) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ private function createMetadata(ApiProperty $attribute, ?ApiProperty $propertyMe
continue;
}

if ($builtinTypes = $attribute->getBuiltinTypes()) {
$propertyMetadata = $propertyMetadata->withBuiltinTypes($builtinTypes);
if ($builtinTypes = $attribute->internalGetBuiltinTypes()) {
$propertyMetadata = $propertyMetadata->internalWithBuiltinTypes($builtinTypes);
}

continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function create(string $resourceClass, string $property, array $options =
continue;
}

$apiProperty = $apiProperty->withBuiltinTypes(array_map(static fn (string $builtinType): LegacyType => new LegacyType($builtinType), $value));
$apiProperty = $apiProperty->internalWithBuiltinTypes(array_map(static fn (string $builtinType): LegacyType => new LegacyType($builtinType), $value));

continue;
}
Expand Down Expand Up @@ -118,6 +118,13 @@ private function update(ApiProperty $propertyMetadata, array $metadata): ApiProp
{
foreach (get_class_methods(ApiProperty::class) as $method) {
if (preg_match('/^(?:get|is)(.*)/', (string) $method, $matches) && null !== ($val = $metadata[lcfirst($matches[1])] ?? null) && method_exists($propertyMetadata, "with{$matches[1]}")) {
// BC layer, to remove in 5.0: route the deprecated builtinTypes setter to the internal one.
if ('BuiltinTypes' === $matches[1]) {
$propertyMetadata = $propertyMetadata->internalWithBuiltinTypes($val);

continue;
}

$propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function create(string $resourceClass, string $property, array $options =

// TODO: remove in 5.x
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
if (!$propertyMetadata->getBuiltinTypes()) {
if (!$propertyMetadata->internalGetBuiltinTypes()) {
$types = $this->propertyInfo->getTypes($resourceClass, $property, $options) ?? []; // @phpstan-ignore-line

foreach ($types as $i => $type) {
Expand All @@ -58,7 +58,7 @@ public function create(string $resourceClass, string $property, array $options =
}
}

$propertyMetadata = $propertyMetadata->withBuiltinTypes($types);
$propertyMetadata = $propertyMetadata->internalWithBuiltinTypes($types);
}
} else {
if (!$propertyMetadata->getNativeType()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function create(string $resourceClass, string $property, array $options =

// TODO: remove in 5.x
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$types = $propertyMetadata->getBuiltinTypes() ?? [];
$types = $propertyMetadata->internalGetBuiltinTypes() ?? [];

if (!$this->isResourceClass($resourceClass) && $types) {
foreach ($types as $builtinType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Metadata\Tests\Property\Factory;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory;
use ApiPlatform\Metadata\Property\Factory\ExtractorPropertyMetadataFactory;
use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyMetadataFactory;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\PropertyInfo\Type as LegacyType;

/**
* Regression test for #8173: internal factories must not emit
* "ApiProperty::withBuiltinTypes()" / "getBuiltinTypes()" deprecations
* when building property metadata.
*
* The public deprecated methods must keep emitting the deprecation for
* userland callers, but ApiPlatform's own factories must use an internal
* code path that does not trigger it.
*/
final class InternalBuiltinTypesDeprecationTest extends TestCase
{
use ProphecyTrait;

/**
* @var list<string>
*/
private array $deprecations = [];

protected function setUp(): void
{
$this->deprecations = [];
// Only record deprecations that target ApiProperty's legacy builtin types accessors:
// the surrounding code (e.g. instantiating Symfony's LegacyType on property-info 7.3+)
// also emits unrelated deprecations that must not influence this test.
set_error_handler(function (int $errno, string $errstr): bool {
if (str_contains($errstr, 'ApiPlatform\Metadata\ApiProperty::')) {
$this->deprecations[] = $errstr;
}

return true;
}, \E_USER_DEPRECATED);
}

protected function tearDown(): void
{
restore_error_handler();
}

public function testApiPropertyExposesInternalBuiltinTypesAccessors(): void
{
$this->assertTrue(method_exists(ApiProperty::class, 'internalGetBuiltinTypes'), 'ApiProperty must expose an internal getter to read builtin types without triggering the deprecation.');
$this->assertTrue(method_exists(ApiProperty::class, 'internalWithBuiltinTypes'), 'ApiProperty must expose an internal setter to store builtin types without triggering the deprecation.');
}

public function testInternalBuiltinTypesAccessorsDoNotEmitDeprecation(): void
{
$types = class_exists(LegacyType::class) ? [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)] : [];

$property = (new ApiProperty())->internalWithBuiltinTypes($types);
$property->internalGetBuiltinTypes();
$this->assertEmpty($this->deprecations, 'The internal builtin types accessors must not emit any deprecation. Got: '.implode("\n", $this->deprecations));
}

public function testPublicWithBuiltinTypesStillEmitsDeprecation(): void
{
$types = class_exists(LegacyType::class) ? [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)] : [];

(new ApiProperty())->withBuiltinTypes($types);

$found = false;
foreach ($this->deprecations as $message) {
if (str_contains($message, 'ApiPlatform\Metadata\ApiProperty::withBuiltinTypes()')) {
$found = true;
break;
}
}

$this->assertTrue($found, 'The public withBuiltinTypes() method must still emit a deprecation for external callers.');
}

public function testPublicGetBuiltinTypesStillEmitsDeprecation(): void
{
$property = new ApiProperty();
$property->getBuiltinTypes();

$found = false;
foreach ($this->deprecations as $message) {
if (str_contains($message, 'ApiPlatform\Metadata\ApiProperty::getBuiltinTypes()')) {
$found = true;
break;
}
}

$this->assertTrue($found, 'The public getBuiltinTypes() method must still emit a deprecation for external callers.');
}

public function testAttributePropertyMetadataFactorySourceUsesInternalAccessors(): void
{
$source = file_get_contents((new \ReflectionClass(AttributePropertyMetadataFactory::class))->getFileName());

$this->assertStringNotContainsString('->withBuiltinTypes(', $source, 'AttributePropertyMetadataFactory must not call the deprecated public withBuiltinTypes() on ApiProperty.');
$this->assertStringNotContainsString('->getBuiltinTypes(', $source, 'AttributePropertyMetadataFactory must not call the deprecated public getBuiltinTypes() on ApiProperty.');
}

public function testExtractorPropertyMetadataFactorySourceUsesInternalAccessors(): void
{
$source = file_get_contents((new \ReflectionClass(ExtractorPropertyMetadataFactory::class))->getFileName());

$this->assertStringNotContainsString('->withBuiltinTypes(', $source, 'ExtractorPropertyMetadataFactory must not call the deprecated public withBuiltinTypes() on ApiProperty.');
$this->assertStringNotContainsString('->getBuiltinTypes(', $source, 'ExtractorPropertyMetadataFactory must not call the deprecated public getBuiltinTypes() on ApiProperty.');
}

public function testPropertyInfoPropertyMetadataFactorySourceUsesInternalAccessors(): void
{
$source = file_get_contents((new \ReflectionClass(PropertyInfoPropertyMetadataFactory::class))->getFileName());

$this->assertStringNotContainsString('->withBuiltinTypes(', $source, 'PropertyInfoPropertyMetadataFactory must not call the deprecated public withBuiltinTypes() on ApiProperty.');
$this->assertStringNotContainsString('->getBuiltinTypes(', $source, 'PropertyInfoPropertyMetadataFactory must not call the deprecated public getBuiltinTypes() on ApiProperty.');
}
}
Loading
Loading