diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 904193b70b..4ac38113a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -779,7 +779,7 @@ jobs: run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction behat-rector-upgrade: - name: Behat (PHP ${{ matrix.php }}) (Rector) + name: Behat (PHP ${{ matrix.php }}) (upgrade script) runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -808,16 +808,15 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies run: composer update --no-interaction --no-progress --ansi - - name: Require Symfony components and Rector dependencies - run: composer require symfony/uid rector/rector:0.12.5 --dev --no-interaction --no-progress --ansi + - name: Require Symfony components + run: composer require symfony/uid --dev --no-interaction --no-progress --ansi - name: Install PHPUnit run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: rm -Rf tests/Fixtures/app/var/cache/* - - name: Convert annotations to attributes + - name: Convert metadata to API Platform 3 run: | - tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Entity --transform-apisubresource -s -n - tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Entity --annotation-to-api-resource -s -n + tests/Fixtures/app/console api:upgrade-resource -f - name: Clear test app cache run: rm -Rf tests/Fixtures/app/var/cache/* - name: Run Behat tests @@ -857,7 +856,7 @@ jobs: continue-on-error: true behat-rector-upgrade-mongodb: - name: Behat (PHP ${{ matrix.php }}) (Rector / MongoDB) + name: Behat (PHP ${{ matrix.php }}) (upgrade script / MongoDB) runs-on: ubuntu-latest env: APP_ENV: mongodb @@ -892,16 +891,15 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies run: composer update --no-interaction --no-progress --ansi - - name: Require Symfony components and Rector dependencies - run: composer require symfony/uid rector/rector:0.12.5 --dev --no-interaction --no-progress --ansi + - name: Require Symfony components + run: composer require symfony/uid --dev --no-interaction --no-progress --ansi - name: Install PHPUnit run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: rm -Rf tests/Fixtures/app/var/cache/* - - name: Convert annotations to attributes + - name: Convert metadata to API Platform 3 run: | - tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Document --transform-apisubresource -s -n - tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Document --annotation-to-api-resource -s -n + tests/Fixtures/app/console api:upgrade-resource -f - name: Clear test app cache run: rm -Rf tests/Fixtures/app/var/cache/* - name: Run Behat tests diff --git a/src/Core/Bridge/Rector/Resolver/OperationClassResolver.php b/src/Core/Bridge/Rector/Resolver/OperationClassResolver.php deleted file mode 100644 index d1831819bc..0000000000 --- a/src/Core/Bridge/Rector/Resolver/OperationClassResolver.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * 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\Core\Bridge\Rector\Resolver; - -use ApiPlatform\Metadata\Delete; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\Patch; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; - -/** - * @experimental - */ -final class OperationClassResolver -{ - private static array $operationsClass = [ - 'itemOperations' => [ - 'get' => Get::class, - 'put' => Put::class, - 'patch' => Patch::class, - 'delete' => Delete::class, - 'post' => Post::class, - ], - 'collectionOperations' => [ - 'get' => GetCollection::class, - 'post' => Post::class, - ], - 'graphql' => [ - 'collection_query' => QueryCollection::class, - 'item_query' => Query::class, - 'mutation' => Mutation::class, - ], - ]; - - public static function resolve(string $operationName, string $operationType, array $arguments): string - { - if (\array_key_exists($operationName, self::$operationsClass[$operationType])) { - return self::$operationsClass[$operationType][$operationName]; - } - - if (isset($arguments['method'], self::$operationsClass[$operationType][$method = strtolower($arguments['method'])])) { - return self::$operationsClass[$operationType][$method]; - } - - // graphql - if ('graphql' === $operationType) { - $intersect = array_intersect_key($arguments, array_flip(['mutation', 'itemQuery', 'collectionQuery'])); - $camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); - - if (1 === \count($intersect)) { - return self::$operationsClass[$operationType][$camelCaseToSnakeCaseNameConverter->normalize(array_key_first($intersect))]; - } - - return self::$operationsClass[$operationType]['mutation']; - } - - throw new \Exception(sprintf('Unable to resolve operation class for %s "%s"', $operationType, $operationName)); - } -} diff --git a/src/Core/Bridge/Rector/Rules/AbstractAnnotationToAttributeRector.php b/src/Core/Bridge/Rector/Rules/AbstractAnnotationToAttributeRector.php deleted file mode 100644 index a5795da154..0000000000 --- a/src/Core/Bridge/Rector/Rules/AbstractAnnotationToAttributeRector.php +++ /dev/null @@ -1,200 +0,0 @@ - - * - * 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\Core\Bridge\Rector\Rules; - -use ApiPlatform\Core\Bridge\Rector\Resolver\OperationClassResolver; -use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait; -use PhpParser\Node; -use PhpParser\Node\AttributeGroup; -use PhpParser\Node\Stmt\Class_; -use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; -use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\CurlyListNode; -use Rector\Core\Rector\AbstractRector; -use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; -use Symfony\Component\String\UnicodeString; - -/** - * @experimental - */ -abstract class AbstractAnnotationToAttributeRector extends AbstractRector -{ - use DeprecationMetadataTrait; - - protected PhpAttributeGroupFactory $phpAttributeGroupFactory; - - protected array $operationTypes = ['graphql', 'collectionOperations', 'itemOperations']; // operations will be added below #[ApiResource] in the reverse order : itemOperations, collectionOperations, then graphql - protected array $defaultOperationsByType = [ - 'itemOperations' => [ - 'get', - 'put', - 'delete', - 'patch', // TODO: add this only if our API accepts the patch format - ], - 'collectionOperations' => [ - 'get', - 'post', - ], - ]; - - private array $operations = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']; - private array $graphQlOperations = ['item_query', 'collection_query', 'mutation']; - - protected function normalizeOperations(array $operations, string $type): array - { - foreach (array_reverse($operations) as $name => $arguments) { - /* - * Case of custom action, ex: - * itemOperations={ - * "get_by_isbn"={"method"="GET", "path"="/books/by_isbn/{isbn}.{_format}", "requirements"={"isbn"=".+"}, "identifiers"="isbn"} - * } - */ - if (\is_array($arguments)) { - // add operation name - $arguments = ['name' => $name] + $arguments; - foreach ($arguments as $key => $argument) { - // camelize argument name - $camelizedKey = (string) (new UnicodeString($key))->camel(); - if ($key === $camelizedKey) { - continue; - } - $arguments[$camelizedKey] = $argument; - unset($arguments[$key]); - } - // Prevent wrong order of operations - unset($operations[$name]); - } - - /* - * Case of default action, ex: - * collectionOperations={"get", "post"}, - * itemOperations={"get", "put", "delete"}, - * graphql={"create", "delete"} - */ - if (\is_string($arguments)) { - unset($operations[$name]); - $name = $arguments; - $arguments = ('graphql' !== $type) ? [] : ['name' => $arguments]; - } - - if (isset($arguments['name']) && \in_array(strtolower($arguments['name']), 'graphql' !== $type ? $this->operations : $this->graphQlOperations, true)) { - unset($arguments['name']); - } - - $operations[$name] = $arguments; - } - - return $operations; - } - - protected function createOperationAttributeGroup(string $type, string $name, array $arguments): AttributeGroup - { - $operationClass = OperationClassResolver::resolve($name, $type, $arguments); - - $camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); - // Replace old attributes with new attributes - foreach ($arguments as $key => $value) { - [$updatedKey, $updatedValue] = $this->getKeyValue($camelCaseToSnakeCaseNameConverter->normalize($key), $value); - unset($arguments[$key]); - $arguments[$updatedKey] = $updatedValue; - } - // remove unnecessary argument "method" after resolving the operation class - if (isset($arguments['method'])) { - unset($arguments['method']); - } - - return $this->phpAttributeGroupFactory->createFromClassWithItems($operationClass, $arguments); - } - - /** - * @param Class_ $node - * @param mixed $items - */ - protected function resolveOperations($items, Node $node): array - { - $values = $items instanceof DoctrineAnnotationTagValueNode ? $items->getValues() : $items; - - foreach ($this->operationTypes as $type) { - if (isset($values[$type])) { - $operations = $this->normalizeOperations($items instanceof DoctrineAnnotationTagValueNode ? $values[$type]->getValuesWithExplicitSilentAndWithoutQuotes() : $values[$type], $type); - foreach ($operations as $name => $arguments) { - array_unshift($node->attrGroups, $this->createOperationAttributeGroup($type, $name, $arguments)); - } - - if ('graphql' === $type && [] === $operations) { - $values['graphQlOperations'] = []; - continue; - } - - if ($items instanceof DoctrineAnnotationTagValueNode) { - // Remove collectionOperations|itemOperations from Tag values - $items->removeValue($type); - $values = $items->getValues(); - continue; - } - - unset($values[$type]); - continue; - } - - // Add default operations if not specified - if (\in_array($type, array_keys($this->defaultOperationsByType), true)) { - foreach (array_reverse($this->defaultOperationsByType[$type]) as $operationName) { - array_unshift($node->attrGroups, $this->createOperationAttributeGroup($type, $operationName, [])); - } - } - } - - return $this->resolveAttributes($values); - } - - /** - * @param mixed $items - */ - protected function resolveAttributes($items): array - { - $values = $items instanceof DoctrineAnnotationTagValueNode ? $items->getValues() : $items; - - $camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); - - // Transform "attributes" keys - if (isset($values['attributes'])) { - $attributes = \is_array($values['attributes']) ? $values['attributes'] : $values['attributes']->values; - foreach ($attributes as $attribute => $value) { - $values[$camelCaseToSnakeCaseNameConverter->denormalize($attribute)] = $value; - } - - unset($values['attributes']); - } - - // Transform deprecated keys - foreach ($values as $attribute => $value) { - [$updatedAttribute, $updatedValue] = $this->getKeyValue(str_replace('"', '', $camelCaseToSnakeCaseNameConverter->normalize($attribute)), $value); - if ($attribute !== $updatedAttribute) { - if ($updatedValue instanceof CurlyListNode) { - $updatedValue = $updatedValue->getValues(); - } - - if (\array_key_exists($updatedAttribute, $values) && \is_array($values[$updatedAttribute])) { - $values[$updatedAttribute] = array_merge($values[$updatedAttribute], $updatedValue); - } else { - $values[$updatedAttribute] = $updatedValue; - } - unset($values[$attribute]); - } - } - - return $values; - } -} diff --git a/src/Core/Bridge/Rector/Rules/ApiPropertyAnnotationToApiPropertyAttributeRector.php b/src/Core/Bridge/Rector/Rules/ApiPropertyAnnotationToApiPropertyAttributeRector.php deleted file mode 100644 index f6267c6c5d..0000000000 --- a/src/Core/Bridge/Rector/Rules/ApiPropertyAnnotationToApiPropertyAttributeRector.php +++ /dev/null @@ -1,182 +0,0 @@ - - * - * 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\Core\Bridge\Rector\Rules; - -use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait; -use PhpParser\Node; -use PhpParser\Node\Stmt\Property; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; -use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; -use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover; -use Rector\Core\Contract\Rector\ConfigurableRectorInterface; -use Rector\Core\ValueObject\PhpVersionFeature; -use Rector\Php80\ValueObject\AnnotationToAttribute; -use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory; -use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; -use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; -use Webmozart\Assert\Assert; - -/** - * @experimental - */ -final class ApiPropertyAnnotationToApiPropertyAttributeRector extends AbstractAnnotationToAttributeRector implements ConfigurableRectorInterface -{ - use DeprecationMetadataTrait; - - /** - * @var string - */ - public const ANNOTATION_TO_ATTRIBUTE = 'api_property_annotation_to_api_property_attribute'; - /** - * @var string - */ - public const REMOVE_TAG = 'remove_tag'; - /** - * @var AnnotationToAttribute[] - */ - private $annotationsToAttributes = []; - /** - * @var bool - */ - private $removeTag; - /** - * @var PhpDocTagRemover - */ - private $phpDocTagRemover; - - public function __construct(PhpAttributeGroupFactory $phpAttributeGroupFactory, PhpDocTagRemover $phpDocTagRemover) - { - $this->phpAttributeGroupFactory = $phpAttributeGroupFactory; - $this->phpDocTagRemover = $phpDocTagRemover; - } - - public function getRuleDefinition(): RuleDefinition - { - return new RuleDefinition('Change annotation to attribute', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' -use ApiPlatform\Core\Annotation\ApiProperty; - -/** - * @ApiProperty(iri="https://schema.org/alternateName") - */ -private $alias; -CODE_SAMPLE - , <<<'CODE_SAMPLE' -use ApiPlatform\Metadata\ApiProperty; - -#[ApiProperty(types: ['https://schema.org/alternateName'])] -private $alias; -CODE_SAMPLE - , [ - self::ANNOTATION_TO_ATTRIBUTE => [new AnnotationToAttribute(\ApiPlatform\Core\Annotation\ApiProperty::class, \ApiPlatform\Metadata\ApiProperty::class)], - self::REMOVE_TAG => true, - ]), - ]); - } - - /** - * @return array> - */ - public function getNodeTypes(): array - { - return [Property::class]; - } - - /** - * @param Property $node - */ - public function refactor(Node $node): ?Node - { - if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::ATTRIBUTES)) { - return null; - } - $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); - if (!$phpDocInfo instanceof PhpDocInfo) { - return null; - } - $tags = $phpDocInfo->getPhpDocNode()->getTags(); - $hasNewAttrGroups = $this->processApplyAttrGroups($tags, $phpDocInfo, $node); - if ($hasNewAttrGroups) { - return $node; - } - - return null; - } - - /** - * @param array $configuration - */ - public function configure(array $configuration): void - { - $annotationsToAttributes = $configuration[self::ANNOTATION_TO_ATTRIBUTE] ?? []; - Assert::allIsInstanceOf($annotationsToAttributes, AnnotationToAttribute::class); - $this->annotationsToAttributes = $annotationsToAttributes; - $this->removeTag = $configuration[self::REMOVE_TAG] ?? true; - } - - /** - * @param array $tags - * @param Property $node - */ - private function processApplyAttrGroups(array $tags, PhpDocInfo $phpDocInfo, Node $node): bool - { - $hasNewAttrGroups = false; - foreach ($tags as $tag) { - foreach ($this->annotationsToAttributes as $annotationToAttribute) { - $annotationToAttributeTag = $annotationToAttribute->getTag(); - - if ($phpDocInfo->hasByName($annotationToAttributeTag)) { - if (true === $this->removeTag) { - // 1. remove php-doc tag - $this->phpDocTagRemover->removeByName($phpDocInfo, $annotationToAttributeTag); - } - // 2. add attributes - array_unshift($node->attrGroups, $this->phpAttributeGroupFactory->createFromSimpleTag($annotationToAttribute)); - $hasNewAttrGroups = true; - continue 2; - } - if ($this->shouldSkip($tag->value, $phpDocInfo, $annotationToAttributeTag)) { - continue; - } - - if (true === $this->removeTag) { - // 1. remove php-doc tag - $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $tag->value); - } - // 2. add attributes - /** @var DoctrineAnnotationTagValueNode $tagValue */ - $tagValue = clone $tag->value; - $tagValue->values = $this->resolveAttributes($tagValue); - - $resourceAttributeGroup = $this->phpAttributeGroupFactory->create($tagValue, $annotationToAttribute); - array_unshift($node->attrGroups, $resourceAttributeGroup); - $hasNewAttrGroups = true; - continue 2; - } - } - - return $hasNewAttrGroups; - } - - private function shouldSkip(PhpDocTagValueNode $phpDocTagValueNode, PhpDocInfo $phpDocInfo, string $annotationToAttributeTag): bool - { - $doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass($annotationToAttributeTag); - if ($phpDocTagValueNode !== $doctrineAnnotationTagValueNode) { - return true; - } - - return !$phpDocTagValueNode instanceof DoctrineAnnotationTagValueNode; - } -} diff --git a/src/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector.php b/src/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector.php deleted file mode 100644 index efca64c9aa..0000000000 --- a/src/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector.php +++ /dev/null @@ -1,188 +0,0 @@ - - * - * 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\Core\Bridge\Rector\Rules; - -use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait; -use PhpParser\Node; -use PhpParser\Node\Stmt\Class_; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; -use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; -use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover; -use Rector\Core\Contract\Rector\ConfigurableRectorInterface; -use Rector\Core\ValueObject\PhpVersionFeature; -use Rector\Php80\ValueObject\AnnotationToAttribute; -use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory; -use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; -use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; -use Webmozart\Assert\Assert; - -/** - * @experimental - */ -final class ApiResourceAnnotationToApiResourceAttributeRector extends AbstractAnnotationToAttributeRector implements ConfigurableRectorInterface -{ - use DeprecationMetadataTrait; - - /** - * @var string - */ - public const ANNOTATION_TO_ATTRIBUTE = 'api_resource_annotation_to_api_resource_attribute'; - /** - * @var string - */ - public const REMOVE_TAG = 'remove_tag'; - /** - * @var AnnotationToAttribute[] - */ - private $annotationsToAttributes = []; - /** - * @var bool - */ - private $removeTag; - /** - * @var PhpDocTagRemover - */ - private $phpDocTagRemover; - - public function __construct(PhpAttributeGroupFactory $phpAttributeGroupFactory, PhpDocTagRemover $phpDocTagRemover) - { - $this->phpAttributeGroupFactory = $phpAttributeGroupFactory; - $this->phpDocTagRemover = $phpDocTagRemover; - } - - public function getRuleDefinition(): RuleDefinition - { - return new RuleDefinition('Change annotation to attribute', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' -use ApiPlatform\Core\Annotation\ApiResource; - -/** - * @ApiResource(collectionOperations={}, itemOperations={ - * "get", - * "get_by_isbn"={"method"="GET", "path"="/books/by_isbn/{isbn}.{_format}", "requirements"={"isbn"=".+"}, "identifiers"="isbn"} - * }) - */ -class Book -CODE_SAMPLE - , <<<'CODE_SAMPLE' -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; - -#[ApiResource] -#[Get] -#[Get(name: 'get_by_isbn', uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')] -class Book -CODE_SAMPLE - , [ - self::ANNOTATION_TO_ATTRIBUTE => [new AnnotationToAttribute(\ApiPlatform\Core\Annotation\ApiResource::class, \ApiPlatform\Metadata\ApiResource::class)], - self::REMOVE_TAG => true, - ]), - ]); - } - - /** - * @return array> - */ - public function getNodeTypes(): array - { - return [Class_::class]; - } - - /** - * @param Class_ $node - */ - public function refactor(Node $node): ?Node - { - if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::ATTRIBUTES)) { - return null; - } - $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); - if (!$phpDocInfo instanceof PhpDocInfo) { - return null; - } - $tags = $phpDocInfo->getPhpDocNode()->getTags(); - $hasNewAttrGroups = $this->processApplyAttrGroups($tags, $phpDocInfo, $node); - if ($hasNewAttrGroups) { - return $node; - } - - return null; - } - - /** - * @param array $configuration - */ - public function configure(array $configuration): void - { - $annotationsToAttributes = $configuration[self::ANNOTATION_TO_ATTRIBUTE] ?? []; - Assert::allIsInstanceOf($annotationsToAttributes, AnnotationToAttribute::class); - $this->annotationsToAttributes = $annotationsToAttributes; - $this->removeTag = $configuration[self::REMOVE_TAG] ?? true; - } - - /** - * @param array $tags - * @param Class_ $node - */ - private function processApplyAttrGroups(array $tags, PhpDocInfo $phpDocInfo, Node $node): bool - { - $hasNewAttrGroups = false; - foreach ($tags as $tag) { - foreach ($this->annotationsToAttributes as $annotationToAttribute) { - $annotationToAttributeTag = $annotationToAttribute->getTag(); - - if ($phpDocInfo->hasByName($annotationToAttributeTag)) { - if (true === $this->removeTag) { - // 1. remove php-doc tag - $this->phpDocTagRemover->removeByName($phpDocInfo, $annotationToAttributeTag); - } - // 2. add attributes - array_unshift($node->attrGroups, $this->phpAttributeGroupFactory->createFromSimpleTag($annotationToAttribute)); - $hasNewAttrGroups = true; - continue 2; - } - if ($this->shouldSkip($tag->value, $phpDocInfo, $annotationToAttributeTag)) { - continue; - } - - if (true === $this->removeTag) { - // 1. remove php-doc tag - $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $tag->value); - } - // 2. add attributes - /** @var DoctrineAnnotationTagValueNode $tagValue */ - $tagValue = clone $tag->value; - $tagValue->values = $this->resolveOperations($tagValue, $node); - - $resourceAttributeGroup = $this->phpAttributeGroupFactory->create($tagValue, $annotationToAttribute); - array_unshift($node->attrGroups, $resourceAttributeGroup); - $hasNewAttrGroups = true; - continue 2; - } - } - - return $hasNewAttrGroups; - } - - private function shouldSkip(PhpDocTagValueNode $phpDocTagValueNode, PhpDocInfo $phpDocInfo, string $annotationToAttributeTag): bool - { - $doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass($annotationToAttributeTag); - if ($phpDocTagValueNode !== $doctrineAnnotationTagValueNode) { - return true; - } - - return !$phpDocTagValueNode instanceof DoctrineAnnotationTagValueNode; - } -} diff --git a/src/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector.php b/src/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector.php deleted file mode 100644 index 2c94148483..0000000000 --- a/src/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector.php +++ /dev/null @@ -1,174 +0,0 @@ - - * - * 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\Core\Bridge\Rector\Rules; - -use PhpParser\Node; -use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\ClassConstFetch; -use PhpParser\Node\Identifier; -use PhpParser\Node\Scalar\LNumber; -use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Stmt\Class_; -use Rector\Core\Contract\Rector\ConfigurableRectorInterface; -use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory; -use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; -use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; - -/** - * @experimental - */ -final class LegacyApiResourceAttributeToApiResourceAttributeRector extends AbstractAnnotationToAttributeRector implements ConfigurableRectorInterface -{ - /** - * @var string - */ - public const REMOVE_INITIAL_ATTRIBUTE = 'remove_initial_attribute'; - - private bool $removeInitialAttribute; - - public function __construct(PhpAttributeGroupFactory $phpAttributeGroupFactory) - { - $this->phpAttributeGroupFactory = $phpAttributeGroupFactory; - } - - public function getRuleDefinition(): RuleDefinition - { - return new RuleDefinition('Upgrade Legacy ApiResource attribute to ApiResource and Operations attributes', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' -use ApiPlatform\Core\Annotation\ApiResource; - -#[ApiResource(collectionOperations: [], itemOperations: ['get', 'get_by_isbn' => ['method' => 'GET', 'path' => '/books/by_isbn/{isbn}.{_format}', 'requirements' => ['isbn' => '.+'], 'identifiers' => 'isbn']])] -class Book -CODE_SAMPLE - , <<<'CODE_SAMPLE' -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; - -#[ApiResource] -#[Get] -#[Get(name: 'get_by_isbn', uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')] -class Book -CODE_SAMPLE - , [self::REMOVE_INITIAL_ATTRIBUTE => true])]); - } - - /** - * @return array> - */ - public function getNodeTypes(): array - { - return [Class_::class]; - } - - /** - * @param array $configuration - */ - public function configure(array $configuration): void - { - $this->removeInitialAttribute = $configuration[self::REMOVE_INITIAL_ATTRIBUTE] ?? true; - } - - /** - * @param Class_ $node - */ - public function refactor(Node $node): ?Node - { - foreach ($node->attrGroups as $key => $attrGroup) { - foreach ($attrGroup->attrs as $attribute) { - if (!$this->isName($attribute->name, \ApiPlatform\Core\Annotation\ApiResource::class)) { - continue; - } - $items = $this->createItemsFromArgs($attribute->args); - $arguments = $this->resolveOperations($items, $node); - $apiResourceAttributeGroup = $this->phpAttributeGroupFactory->createFromClassWithItems(\ApiPlatform\Metadata\ApiResource::class, $arguments); - array_unshift($node->attrGroups, $apiResourceAttributeGroup); - } - } - - $this->cleanupAttrGroups($node); - - return $node; - } - - private function createItemsFromArgs(array $args): array - { - $items = []; - - foreach ($args as $arg) { - $itemValue = $this->normalizeNodeValue($arg->value); - $itemName = $this->normalizeNodeValue($arg->name); - $items[$itemName] = $itemValue; - } - - return $items; - } - - /** - * @param mixed $value - * - * @return bool|float|int|string|array|Node\Expr - */ - private function normalizeNodeValue($value) - { - if ($value instanceof ClassConstFetch) { - return sprintf('%s::%s', (string) end($value->class->parts), (string) $value->name); - } - if ($value instanceof Array_) { - return $this->normalizeNodeValue($value->items); - } - if ($value instanceof String_) { - return (string) $value->value; - } - if ($value instanceof Identifier) { - return $value->name; - } - if ($value instanceof LNumber) { - return (int) $value->value; - } - if (\is_array($value)) { - $items = []; - foreach ($value as $itemKey => $itemValue) { - if (null === $itemValue->key) { - $items[] = $this->normalizeNodeValue($itemValue->value); - } else { - $items[$this->normalizeNodeValue($itemValue->key)] = $this->normalizeNodeValue($itemValue->value); - } - } - - return $items; - } - - return $value; - } - - /** - * Remove initial ApiResource attribute from node. - * - * @param Class_ $node - */ - private function cleanupAttrGroups(Node $node): void - { - if (false === $this->removeInitialAttribute) { - return; - } - - foreach ($node->attrGroups as $key => $attrGroup) { - foreach ($attrGroup->attrs as $attribute) { - if ($this->isName($attribute->name, \ApiPlatform\Core\Annotation\ApiResource::class)) { - unset($node->attrGroups[$key]); - continue 2; - } - } - } - } -} diff --git a/src/Core/Bridge/Rector/Set/ApiPlatformSetList.php b/src/Core/Bridge/Rector/Set/ApiPlatformSetList.php deleted file mode 100644 index 2a94efc4d8..0000000000 --- a/src/Core/Bridge/Rector/Set/ApiPlatformSetList.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * 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\Core\Bridge\Rector\Set; - -use Rector\Set\Contract\SetListInterface; - -/** - * @experimental - */ -final class ApiPlatformSetList implements SetListInterface -{ - /** - * @var string - */ - public const ANNOTATION_TO_LEGACY_API_RESOURCE_ATTRIBUTE = __DIR__.'/../config/sets/annotation-to-legacy-api-resource-attribute.php'; - /** - * @var string - */ - public const ANNOTATION_TO_API_RESOURCE_ATTRIBUTE = __DIR__.'/../config/sets/annotation-to-api-resource-attribute.php'; - /** - * @var string - */ - public const ATTRIBUTE_TO_API_RESOURCE_ATTRIBUTE = __DIR__.'/../config/sets/attribute-to-api-resource-attribute.php'; - /** - * @var string - */ - public const TRANSFORM_API_SUBRESOURCE = __DIR__.'/../config/sets/transform-api-subresource.php'; -} diff --git a/src/Core/Bridge/Rector/config/sets/annotation-to-api-resource-attribute.php b/src/Core/Bridge/Rector/config/sets/annotation-to-api-resource-attribute.php deleted file mode 100644 index d01866bb88..0000000000 --- a/src/Core/Bridge/Rector/config/sets/annotation-to-api-resource-attribute.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use ApiPlatform\Core\Bridge\Rector\Rules\ApiPropertyAnnotationToApiPropertyAttributeRector; -use ApiPlatform\Core\Bridge\Rector\Rules\ApiResourceAnnotationToApiResourceAttributeRector; -use Rector\Core\Configuration\Option; -use Rector\Core\ValueObject\PhpVersion; -use Rector\Php80\Rector\Class_\AnnotationToAttributeRector; -use Rector\Php80\ValueObject\AnnotationToAttribute; -use Rector\Renaming\Rector\Namespace_\RenameNamespaceRector; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; -use Symplify\SymfonyPhpConfig\ValueObjectInliner; - -return static function (ContainerConfigurator $containerConfigurator): void { - $parameters = $containerConfigurator->parameters(); - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_80); - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - - $services = $containerConfigurator->services(); - - $services->set(RenameNamespaceRector::class) - ->call('configure', [[ - RenameNamespaceRector::OLD_TO_NEW_NAMESPACES => [ - 'ApiPlatform\Core\Annotation\ApiResource' => 'ApiPlatform\Metadata\ApiResource', - 'ApiPlatform\Core\Annotation\ApiProperty' => 'ApiPlatform\Metadata\ApiProperty', - 'ApiPlatform\Core\Annotation\ApiFilter' => 'ApiPlatform\Metadata\ApiFilter', - 'ApiPlatform\Core\Api\UrlGeneratorInterface' => 'ApiPlatform\Api\UrlGeneratorInterface', - ], - ]]); - - // ApiResource annotation to ApiResource & operation attributes - $services->set(ApiResourceAnnotationToApiResourceAttributeRector::class) - ->call('configure', [[ - ApiResourceAnnotationToApiResourceAttributeRector::ANNOTATION_TO_ATTRIBUTE => ValueObjectInliner::inline([ - new AnnotationToAttribute( - \ApiPlatform\Core\Annotation\ApiResource::class, - \ApiPlatform\Metadata\ApiResource::class - ), - ]), - ]]); - - // ApiProperty annotation to ApiProperty attribute - $services->set(ApiPropertyAnnotationToApiPropertyAttributeRector::class) - ->call('configure', [[ - ApiPropertyAnnotationToApiPropertyAttributeRector::ANNOTATION_TO_ATTRIBUTE => ValueObjectInliner::inline([ - new AnnotationToAttribute( - \ApiPlatform\Core\Annotation\ApiProperty::class, - \ApiPlatform\Metadata\ApiProperty::class - ), - ]), - ]]); - - // ApiFilter annotation to ApiFilter attribute - $services->set(AnnotationToAttributeRector::class) - ->call('configure', [[ - AnnotationToAttributeRector::ANNOTATION_TO_ATTRIBUTE => ValueObjectInliner::inline([ - new AnnotationToAttribute( - \ApiPlatform\Core\Annotation\ApiFilter::class, - \ApiPlatform\Metadata\ApiFilter::class - ), - ]), - ]]); -}; diff --git a/src/Core/Bridge/Rector/config/sets/annotation-to-legacy-api-resource-attribute.php b/src/Core/Bridge/Rector/config/sets/annotation-to-legacy-api-resource-attribute.php deleted file mode 100644 index 2cb3446fb5..0000000000 --- a/src/Core/Bridge/Rector/config/sets/annotation-to-legacy-api-resource-attribute.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Rector\Core\Configuration\Option; -use Rector\Core\ValueObject\PhpVersion; -use Rector\Php80\Rector\Class_\AnnotationToAttributeRector; -use Rector\Php80\ValueObject\AnnotationToAttribute; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; -use Symplify\SymfonyPhpConfig\ValueObjectInliner; - -return static function (ContainerConfigurator $containerConfigurator): void { - $parameters = $containerConfigurator->parameters(); - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_80); - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - - $services = $containerConfigurator->services(); - - // ApiResource annotation to ApiResource attribute - $services->set(AnnotationToAttributeRector::class) - ->call('configure', [[ - AnnotationToAttributeRector::ANNOTATION_TO_ATTRIBUTE => ValueObjectInliner::inline([ - new AnnotationToAttribute( - \ApiPlatform\Core\Annotation\ApiResource::class, - \ApiPlatform\Core\Annotation\ApiResource::class - ), - ]), - ]]); -}; diff --git a/src/Core/Bridge/Rector/config/sets/attribute-to-api-resource-attribute.php b/src/Core/Bridge/Rector/config/sets/attribute-to-api-resource-attribute.php deleted file mode 100644 index adad7fba31..0000000000 --- a/src/Core/Bridge/Rector/config/sets/attribute-to-api-resource-attribute.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use ApiPlatform\Core\Bridge\Rector\Rules\LegacyApiResourceAttributeToApiResourceAttributeRector; -use Rector\Core\Configuration\Option; -use Rector\Core\ValueObject\PhpVersion; -use Rector\Renaming\Rector\Namespace_\RenameNamespaceRector; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - -return static function (ContainerConfigurator $containerConfigurator): void { - $parameters = $containerConfigurator->parameters(); - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_80); - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - - $services = $containerConfigurator->services(); - - $services->set(RenameNamespaceRector::class) - ->call('configure', [[ - RenameNamespaceRector::OLD_TO_NEW_NAMESPACES => [ - 'ApiPlatform\Core\Annotation\ApiResource' => 'ApiPlatform\Metadata\ApiResource', - 'ApiPlatform\Core\Annotation\ApiProperty' => 'ApiPlatform\Metadata\ApiProperty', - 'ApiPlatform\Core\Annotation\ApiFilter' => 'ApiPlatform\Metadata\ApiFilter', - 'ApiPlatform\Core\Api\UrlGeneratorInterface' => 'ApiPlatform\Api\UrlGeneratorInterface', - ], - ]]); - - $services->set(LegacyApiResourceAttributeToApiResourceAttributeRector::class) - ->call('configure', [[ - LegacyApiResourceAttributeToApiResourceAttributeRector::REMOVE_INITIAL_ATTRIBUTE => true, - ]]); -}; diff --git a/src/Core/Bridge/Rector/config/sets/transform-api-subresource.php b/src/Core/Bridge/Rector/config/sets/transform-api-subresource.php deleted file mode 100644 index 47fb60627e..0000000000 --- a/src/Core/Bridge/Rector/config/sets/transform-api-subresource.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Rector\Core\Configuration\Option; -use Rector\Core\ValueObject\PhpVersion; -use Rector\DeadCode\Rector\ClassLike\RemoveAnnotationRector; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - -return static function (ContainerConfigurator $containerConfigurator): void { - $parameters = $containerConfigurator->parameters(); - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_80); - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - - $services = $containerConfigurator->services(); - - $services->set(RemoveAnnotationRector::class) - ->call('configure', [[ - RemoveAnnotationRector::ANNOTATIONS_TO_REMOVE => ['ApiSubresource'], - ]]); -}; diff --git a/src/Core/Bridge/Symfony/Bundle/Command/RectorCommand.php b/src/Core/Bridge/Symfony/Bundle/Command/RectorCommand.php deleted file mode 100644 index f95f9ea01d..0000000000 --- a/src/Core/Bridge/Symfony/Bundle/Command/RectorCommand.php +++ /dev/null @@ -1,279 +0,0 @@ - - * - * 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\Core\Bridge\Symfony\Bundle\Command; - -use ApiPlatform\Core\Bridge\Rector\Parser\TransformApiSubresourceVisitor; -use ApiPlatform\Core\Bridge\Rector\Service\SubresourceTransformer; -use ApiPlatform\Core\Bridge\Rector\Set\ApiPlatformSetList; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use PhpParser\Lexer\Emulative; -use PhpParser\NodeTraverser; -use PhpParser\Parser\Php7; -use PhpParser\PrettyPrinter\Standard; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; - -/** - * @experimental - */ -final class RectorCommand extends Command -{ - private const OPERATIONS = [ - 'annotation-to-legacy-api-resource' => '@ApiResource to #[ApiPlatform\Core\Annotation\ApiResource] - deprecated since 2.7', - 'annotation-to-api-resource' => '@ApiResource to #[ApiPlatform\Metadata\ApiResource]', - 'keep-attribute' => '#[ApiPlatform\Core\Annotation\ApiResource] to #[ApiPlatform\Metadata\ApiResource]', - 'transform-apisubresource' => 'Transform @ApiSubresource to alternate resources', - ]; - - protected static $defaultName = 'api:rector:upgrade'; - - private $resourceNameCollectionFactory; - private $resourceMetadataFactory; - private $subresourceOperationFactory; - private $subresourceTransformer; - private $localCache = []; - - public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, SubresourceOperationFactoryInterface $subresourceOperationFactory, SubresourceTransformer $subresourceTransformer) - { - $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->subresourceOperationFactory = $subresourceOperationFactory; - $this->subresourceTransformer = $subresourceTransformer; - - parent::__construct(); - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setDescription('Change "ApiPlatform\Core\Annotation\ApiResource" annotation/attribute to new "ApiPlatform\Metadata\ApiResource" attribute') - ->addOption('dry-run', '-d', InputOption::VALUE_NONE, 'Rector will show you diff of files that it would change. To make the changes, drop --dry-run') - ->addOption('silent', '-s', InputOption::VALUE_NONE, 'Run Rector silently') - ->addArgument('src', InputArgument::REQUIRED, 'Path to folder/file to convert, forwarded to Rector'); - - foreach (self::OPERATIONS as $operationKey => $operationDescription) { - $this->addOption($operationKey, null, InputOption::VALUE_NONE, $operationDescription); - } - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - if (!file_exists('vendor/bin/rector')) { - $output->write('Rector is not installed. Please execute composer require --dev rector/rector:0.12.5'); - - return Command::FAILURE; - } - - $io = new SymfonyStyle($input, $output); - $operations = self::OPERATIONS; - - $choices = array_values($operations); - - $choice = null; - $operationCount = 0; - $askForSubresources = true; - - foreach ($operations as $operationKey => $operationDescription) { - if ($input->getOption($operationKey)) { - $choice = $operationKey; - ++$operationCount; - } - } - - if ($operationCount > 1) { - $output->write('Only one operation can be given as a parameter.'); - - return Command::FAILURE; - } - - if (!$choice) { - $io->text([ - 'Welcome,', - 'This tool allows you to transform Doctrine Annotations "@ApiResource" into Attributes "#[ApiPlatform\Core\Annotation\ApiResource]".', - 'Note that since 2.7 there is a new Attribute at ApiPlatform\Metadata\ApiResource that allows you more control over resources. It\'s the new default in 3.0.', - ]); - $choice = $io->choice('Choose an operation to perform:', $choices); - } else { - $askForSubresources = false; - } - - $operationKey = $this->getOperationKeyByChoice($operations, $choice); - - $command = 'vendor/bin/rector process '.$input->getArgument('src'); - - if ($output->isDebug()) { - $command .= ' --debug'; - } - - $operationKeys = array_keys($operations); - - switch ($operationKey) { - case $operationKeys[0]: - $command .= ' --config='.ApiPlatformSetList::ANNOTATION_TO_LEGACY_API_RESOURCE_ATTRIBUTE; - break; - case $operationKeys[1]: - if ($askForSubresources && $this->isThereSubresources($io, $output)) { - return Command::FAILURE; - } - $command .= ' --config='.ApiPlatformSetList::ANNOTATION_TO_API_RESOURCE_ATTRIBUTE; - break; - case $operationKeys[2]: - if ($askForSubresources && $this->isThereSubresources($io, $output)) { - return Command::FAILURE; - } - $command .= ' --config='.ApiPlatformSetList::ATTRIBUTE_TO_API_RESOURCE_ATTRIBUTE; - break; - case $operationKeys[3]: - $command .= ' --config='.ApiPlatformSetList::TRANSFORM_API_SUBRESOURCE; - break; - } - - if ($input->getOption('dry-run')) { - $command .= ' --dry-run'; - } else { - if (!$io->confirm('Your files will be overridden. Do you want to continue ?')) { - $output->write('Migration aborted.'); - - return Command::FAILURE; - } - } - - $io->title('Run '.$command); - - if ($operationKey === $operationKeys[3] && !$input->getOption('dry-run')) { - $this->transformApiSubresource($input->getArgument('src'), $output); - } - - if ($input->getOption('silent')) { - exec($command.' --no-progress-bar --no-diffs'); - } else { - passthru($command); - } - - $output->writeln('Migration successful.'); - - return Command::SUCCESS; - } - - private function getOperationKeyByChoice($operations, $choice): string - { - if (\in_array($choice, array_keys($operations), true)) { - return $choice; - } - - return array_search($choice, $operations, true); - } - - private function transformApiSubresource(string $src, OutputInterface $output) - { - foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { - try { - new \ReflectionClass($resourceClass); - } catch (\Exception $e) { - continue; - } - - if (!isset($this->localCache[$resourceClass])) { - $this->localCache[$resourceClass] = []; - } - - foreach ($this->subresourceOperationFactory->create($resourceClass) as $subresourceMetadata) { - if (!isset($this->localCache[$subresourceMetadata['resource_class']])) { - $this->localCache[$subresourceMetadata['resource_class']] = []; - } - - foreach ($this->localCache[$subresourceMetadata['resource_class']] as $currentSubresourceMetadata) { - if ($currentSubresourceMetadata['path'] === $subresourceMetadata['path']) { - continue 2; - } - } - $this->localCache[$subresourceMetadata['resource_class']][] = $subresourceMetadata; - } - } - - // Compute URI variables - foreach ($this->localCache as $class => $subresources) { - if (!$subresources) { - unset($this->localCache[$class]); - continue; - } - - foreach ($subresources as $i => $subresourceMetadata) { - $this->localCache[$class][$i]['uri_variables'] = $this->subresourceTransformer->toUriVariables($subresourceMetadata); - } - } - - foreach ($this->localCache as $resourceClass => $linkedSubresourceMetadata) { - $fileName = (new \ReflectionClass($resourceClass))->getFilename(); - - if (!str_contains($fileName, $src)) { - continue; - } - - $referenceType = null; - try { - $metadata = $this->resourceMetadataFactory->create($resourceClass); - $referenceType = $metadata->getAttribute('url_generation_strategy'); - } catch (\Exception $e) { - } - - foreach ($linkedSubresourceMetadata as $subresourceMetadata) { - $lexer = new Emulative([ - 'usedAttributes' => [ - 'comments', - 'startLine', 'endLine', - 'startTokenPos', 'endTokenPos', - ], - ]); - $parser = new Php7($lexer); - - $traverser = new NodeTraverser(); - $traverser->addVisitor(new TransformApiSubresourceVisitor($subresourceMetadata, $referenceType)); - $prettyPrinter = new Standard(); - - $oldStmts = $parser->parse(file_get_contents($fileName)); - $oldTokens = $lexer->getTokens(); - - $newStmts = $traverser->traverse($oldStmts); - - $newCode = $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens); - - file_put_contents($fileName, $newCode); - } - } - } - - private function isThereSubresources($io, $output): bool - { - if ($io->confirm('Do you have any @ApiSubresource or #[ApiSubresource] left in your code ?')) { - $output->writeln('You will not be able to convert them afterwards. Please run the command "Transform @ApiSubresource to alternate resources" first.'); - - return true; - } - - return false; - } -} diff --git a/src/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommand.php b/src/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommand.php new file mode 100644 index 0000000000..2f038731ec --- /dev/null +++ b/src/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommand.php @@ -0,0 +1,250 @@ + + * + * 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\Core\Bridge\Symfony\Bundle\Command; + +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; +use ApiPlatform\Core\Upgrade\ColorConsoleDiffFormatter; +use ApiPlatform\Core\Upgrade\SubresourceTransformer; +use ApiPlatform\Core\Upgrade\UpgradeApiResourceVisitor; +use ApiPlatform\Core\Upgrade\UpgradeApiSubresourceVisitor; +use ApiPlatform\Exception\ResourceClassNotFoundException; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use Doctrine\Common\Annotations\AnnotationReader; +use PhpParser\Lexer\Emulative; +use PhpParser\NodeTraverser; +use PhpParser\Parser\Php7; +use PhpParser\PrettyPrinter\Standard; +use SebastianBergmann\Diff\Differ; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +final class UpgradeApiResourceCommand extends Command +{ + protected static $defaultName = 'api:upgrade-resource'; + + private $resourceNameCollectionFactory; + private $resourceMetadataFactory; + private $subresourceOperationFactory; + private $subresourceTransformer; + private $reader; + private $localCache = []; + + public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, SubresourceOperationFactoryInterface $subresourceOperationFactory, SubresourceTransformer $subresourceTransformer, AnnotationReader $reader) + { + $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->subresourceOperationFactory = $subresourceOperationFactory; + $this->subresourceTransformer = $subresourceTransformer; + $this->reader = $reader; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDescription('The "api:upgrade-resource" command upgrades your API Platform metadata from versions below 2.6 to the new metadata from versions above 2.7. +Once you executed this script, make sure that the "metadata_backward_compatibility_layer" flag is set to "false" in the API Platform configuration. +This will remove "ApiPlatform\Core\Annotation\ApiResource" annotation/attribute and use the "ApiPlatform\Metadata\ApiResource" attribute instead.') + ->addOption('dry-run', '-d', InputOption::VALUE_NEGATABLE, 'Dry mode outputs a diff instead of writing files.', true) + ->addOption('silent', '-s', InputOption::VALUE_NONE, 'Silent output.') + ->addOption('force', '-f', InputOption::VALUE_NONE, 'Writes the files in place and skips PHP version check.'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$input->getOption('force') && \PHP_VERSION_ID < 80100) { + $output->write('The new metadata system only works with PHP 8.1 and above.'); + + return Command::INVALID; + } + + $this->transformApiSubresource($input, $output); + $this->transformApiResource($input, $output); + + return Command::SUCCESS; + } + + /** + * This computes a local cache with resource classes having subresources. + * We first loop over all the classes and re-map the metadata on the correct Resource class. + * Then we transform the ApiSubresource to an ApiResource class. + */ + private function transformApiSubresource(InputInterface $input, OutputInterface $output): void + { + foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { + try { + new \ReflectionClass($resourceClass); + } catch (\Exception $e) { + continue; + } + + if (!isset($this->localCache[$resourceClass])) { + $this->localCache[$resourceClass] = []; + } + + foreach ($this->subresourceOperationFactory->create($resourceClass) as $subresourceMetadata) { + if (!isset($this->localCache[$subresourceMetadata['resource_class']])) { + $this->localCache[$subresourceMetadata['resource_class']] = []; + } + + foreach ($this->localCache[$subresourceMetadata['resource_class']] as $currentSubresourceMetadata) { + if ($currentSubresourceMetadata['path'] === $subresourceMetadata['path']) { + continue 2; + } + } + $this->localCache[$subresourceMetadata['resource_class']][] = $subresourceMetadata; + } + } + + // Compute URI variables + foreach ($this->localCache as $class => $subresources) { + if (!$subresources) { + unset($this->localCache[$class]); + continue; + } + + foreach ($subresources as $i => $subresourceMetadata) { + $this->localCache[$class][$i]['uri_variables'] = $this->subresourceTransformer->toUriVariables($subresourceMetadata); + } + } + + foreach ($this->localCache as $resourceClass => $linkedSubresourceMetadata) { + $fileName = (new \ReflectionClass($resourceClass))->getFilename(); + + $referenceType = null; + try { + $metadata = $this->resourceMetadataFactory->create($resourceClass); + $referenceType = $metadata->getAttribute('url_generation_strategy'); + } catch (\Exception $e) { + } + + foreach ($linkedSubresourceMetadata as $subresourceMetadata) { + $lexer = new Emulative([ + 'usedAttributes' => [ + 'comments', + 'startLine', 'endLine', + 'startTokenPos', 'endTokenPos', + ], + ]); + $parser = new Php7($lexer); + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new UpgradeApiSubresourceVisitor($subresourceMetadata, $referenceType)); + $prettyPrinter = new Standard(); + + $oldCode = file_get_contents($fileName); + $oldStmts = $parser->parse($oldCode); + $oldTokens = $lexer->getTokens(); + + $newStmts = $traverser->traverse($oldStmts); + + $newCode = $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens); + + if (!$input->getOption('force') && $input->getOption('dry-run')) { + if ($input->getOption('silent')) { + continue; + } + + $this->printDiff($oldCode, $newCode, $output); + continue; + } + + file_put_contents($fileName, $newCode); + } + } + } + + private function transformApiResource(InputInterface $input, OutputInterface $output): void + { + $prettyPrinter = new Standard(); + foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { + try { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + } catch (ResourceClassNotFoundException $e) { + continue; + } + $lexer = new Emulative([ + 'usedAttributes' => [ + 'comments', + 'startLine', + 'endLine', + 'startTokenPos', + 'endTokenPos', + ], + ]); + $parser = new Php7($lexer); + $fileName = (new \ReflectionClass($resourceClass))->getFilename(); + + $traverser = new NodeTraverser(); + [$attribute, $isAnnotation] = $this->readApiResource($resourceClass); + + if (!$attribute) { + continue; + } + + $traverser->addVisitor(new UpgradeApiResourceVisitor($attribute, $isAnnotation)); + + $oldCode = file_get_contents($fileName); + $oldStmts = $parser->parse($oldCode); + $oldTokens = $lexer->getTokens(); + + $newStmts = $traverser->traverse($oldStmts); + $newCode = $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens); + + if (!$input->getOption('force') && $input->getOption('dry-run')) { + if ($input->getOption('silent')) { + continue; + } + + $this->printDiff($oldCode, $newCode, $output); + continue; + } + + file_put_contents($fileName, $newCode); + } + } + + private function printDiff(string $oldCode, string $newCode, OutputInterface $output): void + { + $consoleFormatter = new ColorConsoleDiffFormatter(); + $differ = new Differ(); + $diff = $differ->diff($oldCode, $newCode); + $output->write($consoleFormatter->format($diff)); + } + + /** + * @return [ApiResource, bool] + */ + private function readApiResource(string $resourceClass): array + { + $reflectionClass = new \ReflectionClass($resourceClass); + + if (\PHP_VERSION_ID >= 80000 && $attributes = $reflectionClass->getAttributes(ApiResource::class)) { + return [$attributes[0]->newInstance(), false]; + } + + return [$this->reader->getClassAnnotation($reflectionClass, ApiResource::class), true]; + } +} diff --git a/src/Core/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php b/src/Core/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php index 91ab9fcd66..5cfb852d3c 100644 --- a/src/Core/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php +++ b/src/Core/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php @@ -213,9 +213,12 @@ private function getSerializerAttributeMetadata(string $class, string $attribute */ private function getClassSerializerGroups(string $class): array { - $resourceMetadata = $this->resourceMetadataFactory->create($class); - if ($outputClass = $resourceMetadata->getAttribute('output')['class'] ?? null) { - $class = $outputClass; + try { + $resourceMetadata = $this->resourceMetadataFactory->create($class); + if ($outputClass = $resourceMetadata->getAttribute('output')['class'] ?? null) { + $class = $outputClass; + } + } catch (ResourceClassNotFoundException $e) { } $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class); diff --git a/src/Core/Upgrade/ColorConsoleDiffFormatter.php b/src/Core/Upgrade/ColorConsoleDiffFormatter.php new file mode 100644 index 0000000000..c423afaf39 --- /dev/null +++ b/src/Core/Upgrade/ColorConsoleDiffFormatter.php @@ -0,0 +1,119 @@ + + * + * 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\Core\Upgrade; + +use Symfony\Component\Console\Formatter\OutputFormatter; + +/** + * Inspired by @see https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/src/Differ/DiffConsoleFormatter.php to be + * used as standalone class, without need to require whole package by Dariusz Rumiński + * Forked by @soyuka from Symplify\PackageBuilder\Console\Formatter to remove Nette\Utils\Strings dependency to be even more standalone. + * + * @see \Symplify\PackageBuilder\Tests\Console\Formatter\ColorConsoleDiffFormatterTest + * + * @internal + */ +final class ColorConsoleDiffFormatter +{ + /** + * @var string + * + * @see https://regex101.com/r/ovLMDF/1 + */ + private const PLUS_START_REGEX = '#^(\+.*)#'; + + /** + * @var string + * + * @see https://regex101.com/r/xwywpa/1 + */ + private const MINUT_START_REGEX = '#^(\-.*)#'; + + /** + * @var string + * + * @see https://regex101.com/r/CMlwa8/1 + */ + private const AT_START_REGEX = '#^(@.*)#'; + + /** + * @var string + * + * @see https://regex101.com/r/qduj2O/1 + */ + private const NEWLINES_REGEX = "#\n\r|\n#"; + + private string $template; + + public function __construct() + { + $this->template = sprintf( + ' ---------- begin diff ----------%s%%s%s ----------- end diff -----------'.\PHP_EOL, + \PHP_EOL, + \PHP_EOL + ); + } + + public function format(string $diff): string + { + return $this->formatWithTemplate($diff, $this->template); + } + + private function formatWithTemplate(string $diff, string $template): string + { + $escapedDiff = OutputFormatter::escape(rtrim($diff)); + + $escapedDiffLines = preg_split(self::NEWLINES_REGEX, $escapedDiff); + + // remove description of added + remove; obvious on diffs + foreach ($escapedDiffLines as $key => $escapedDiffLine) { + if ('--- Original' === $escapedDiffLine) { + unset($escapedDiffLines[$key]); + } + + if ('+++ New' === $escapedDiffLine) { + unset($escapedDiffLines[$key]); + } + } + + $coloredLines = array_map(function (string $string): string { + $string = $this->makePlusLinesGreen($string); + $string = $this->makeMinusLinesRed($string); + $string = $this->makeAtNoteCyan($string); + + if (' ' === $string) { + return ''; + } + + return $string; + }, $escapedDiffLines); + + return sprintf($template, implode(\PHP_EOL, $coloredLines)); + } + + private function makePlusLinesGreen(string $string): string + { + return preg_replace(self::PLUS_START_REGEX, '$1', $string); + } + + private function makeMinusLinesRed(string $string): string + { + return preg_replace(self::MINUT_START_REGEX, '$1', $string); + } + + private function makeAtNoteCyan(string $string): string + { + return preg_replace(self::AT_START_REGEX, '$1', $string); + } +} diff --git a/src/Core/Upgrade/RemoveAnnotationTrait.php b/src/Core/Upgrade/RemoveAnnotationTrait.php new file mode 100644 index 0000000000..be49d3ce0e --- /dev/null +++ b/src/Core/Upgrade/RemoveAnnotationTrait.php @@ -0,0 +1,34 @@ + + * + * 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\Core\Upgrade; + +use phpDocumentor\Reflection\DocBlock\Serializer; +use phpDocumentor\Reflection\DocBlockFactory; +use PhpParser\Comment\Doc; + +trait RemoveAnnotationTrait +{ + private function removeAnnotationByTag(Doc $comment, string $tagName): Doc + { + $factory = DocBlockFactory::createInstance(); + $docBlock = $factory->create($comment->getText()); + foreach ($docBlock->getTagsByName($tagName) as $tag) { + $docBlock->removeTag($tag); + } + + $serializer = new Serializer(0, '', true, null, null, \PHP_EOL); + + return new Doc($serializer->getDocComment($docBlock)); + } +} diff --git a/src/Core/Bridge/Rector/Service/SubresourceTransformer.php b/src/Core/Upgrade/SubresourceTransformer.php similarity index 98% rename from src/Core/Bridge/Rector/Service/SubresourceTransformer.php rename to src/Core/Upgrade/SubresourceTransformer.php index 81bd619e8d..e14dd94d9e 100644 --- a/src/Core/Bridge/Rector/Service/SubresourceTransformer.php +++ b/src/Core/Upgrade/SubresourceTransformer.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\Core\Bridge\Rector\Service; +namespace ApiPlatform\Core\Upgrade; use ApiPlatform\Util\Inflector; use Doctrine\Common\Annotations\AnnotationReader; diff --git a/src/Core/Upgrade/UpgradeApiResourceVisitor.php b/src/Core/Upgrade/UpgradeApiResourceVisitor.php new file mode 100644 index 0000000000..dd398eff16 --- /dev/null +++ b/src/Core/Upgrade/UpgradeApiResourceVisitor.php @@ -0,0 +1,397 @@ + + * + * 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\Core\Upgrade; + +use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Core\Annotation\ApiResource as LegacyApiResource; +use ApiPlatform\Core\Annotation\ApiSubresource; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait; +use PhpParser\Node; +use PhpParser\NodeVisitorAbstract; + +final class UpgradeApiResourceVisitor extends NodeVisitorAbstract +{ + use DeprecationMetadataTrait; + use RemoveAnnotationTrait; + + private LegacyApiResource $resourceAnnotation; + private bool $isAnnotation = false; + + public function __construct(LegacyApiResource $resourceAnnotation, bool $isAnnotation = false) + { + $this->resourceAnnotation = $resourceAnnotation; + $this->isAnnotation = $isAnnotation; + } + + /** + * In API Platform 3.x there's no difference between items and collections other then a flag within the Operation + * Therefore we need to fix the behavior with an empty array. + */ + private function getLegacyOperations(bool $isCollection = false): array + { + $key = $isCollection ? 'collectionOperations' : 'itemOperations'; + if ([] === $this->resourceAnnotation->{$key}) { + return []; + } + + $default = $isCollection ? ['post', 'get'] : ['get', 'put', 'patch', 'delete']; + + return $this->resourceAnnotation->{$key} ?? $default; + } + + public function enterNode(Node $node) + { + // We don't go through every resources to remove ApiSubresource annotations, do this here as well if there are some + // @see UpgradeApiSubresourceVisitor + $comment = $node->getDocComment(); + if ($comment && preg_match('/@ApiSubresource/', $comment->getText())) { + $node->setDocComment($this->removeAnnotationByTag($comment, 'ApiSubresource')); + } + + if ($node instanceof Node\Stmt\Namespace_) { + $namespaces = array_unique(array_merge( + [ApiResource::class], + $this->getOperationsNamespaces($this->getLegacyOperations()), + $this->getOperationsNamespaces($this->getLegacyOperations(true), true), + $this->getGraphQlOperationsNamespaces($this->resourceAnnotation->graphql ?? []) + )); + + foreach ($node->stmts as $k => $stmt) { + if (!$stmt instanceof Node\Stmt\Use_) { + break; + } + + $useStatement = implode('\\', $stmt->uses[0]->name->parts); + + if (LegacyApiResource::class === $useStatement) { + unset($node->stmts[$k]); + continue; + } + + // There might be a use left as the UpgradeApiSubresourceVisitor doesn't go through all the resources + if (ApiSubresource::class === $useStatement) { + unset($node->stmts[$k]); + continue; + } + + if (false !== ($key = array_search($useStatement, $namespaces, true))) { + unset($namespaces[$key]); + } + } + + foreach ($namespaces as $namespace) { + array_unshift($node->stmts, new Node\Stmt\Use_([ + new Node\Stmt\UseUse( + new Node\Name( + $namespace + ) + ), + ])); + } + } + + if ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Interface_) { + if ($this->isAnnotation) { + $this->removeAnnotation($node); + } else { + $this->removeAttribute($node); + } + + $arguments = []; + $operations = null === $this->resourceAnnotation->itemOperations && null === $this->resourceAnnotation->collectionOperations ? null : array_merge( + $this->legacyOperationsToOperations($this->getLegacyOperations()), + $this->legacyOperationsToOperations($this->getLegacyOperations(true), true) + ); + + if (null !== $operations) { + $arguments['operations'] = new Node\Expr\Array_( + array_map(function ($value) { + return new Node\Expr\ArrayItem($value); + }, $operations), + [ + 'kind' => Node\Expr\Array_::KIND_SHORT, + ] + ); + } + + $graphQlOperations = null === $this->resourceAnnotation->graphql ? null : []; + foreach ($this->resourceAnnotation->graphql ?? [] as $operationName => $graphQlOperation) { + if (\is_int($operationName)) { + $ns = $this->getGraphQlOperationNamespace($graphQlOperation); + $graphQlOperations[] = new Node\Expr\New_(new Node\Name($this->getShortName($ns)), $this->arrayToArguments(['name' => $this->valueToNode($graphQlOperation)])); + continue; + } + + $ns = $this->getGraphQlOperationNamespace($operationName, $graphQlOperation); + $args = ['name' => $this->valueToNode($operationName)]; + foreach ($graphQlOperation as $key => $value) { + [$key, $value] = $this->getKeyValue($key, $value); + $args[$key] = $this->valueToNode($value); + } + + $graphQlOperations[] = new Node\Expr\New_(new Node\Name($this->getShortName($ns)), $this->arrayToArguments($args)); + } + + if (null !== $graphQlOperations) { + $arguments['graphQlOperations'] = new Node\Expr\Array_( + array_map(function ($value) { + return new Node\Expr\ArrayItem($value); + }, $graphQlOperations), + [ + 'kind' => Node\Expr\Array_::KIND_SHORT, + ] + ); + } + + foreach (['shortName', 'description', 'iri'] as $key) { + if (!($value = $this->resourceAnnotation->{$key})) { + continue; + } + + if ('iri' === $key) { + $arguments['types'] = new Node\Expr\Array_([new Node\Expr\ArrayItem( + new Node\Scalar\String_($value) + )], ['kind' => Node\Expr\Array_::KIND_SHORT]); + continue; + } + + $arguments[$key] = new Node\Scalar\String_($value); + } + + foreach ($this->resourceAnnotation->attributes ?? [] as $key => $value) { + if (null === $value) { + continue; + } + + [$key, $value] = $this->getKeyValue($key, $value); + + if ('urlGenerationStrategy' === $key) { + $urlGeneratorInterface = new \ReflectionClass(UrlGeneratorInterface::class); + $urlGeneratorConstants = array_flip($urlGeneratorInterface->getConstants()); + $currentUrlGeneratorConstant = $urlGeneratorConstants[$value]; + + $arguments[$key] = + new Node\Expr\ClassConstFetch( + new Node\Name('UrlGeneratorInterface'), + $currentUrlGeneratorConstant + ); + continue; + } + + $arguments[$key] = $this->valueToNode($value); + } + + $apiResourceAttribute = + new Node\AttributeGroup([ + new Node\Attribute( + new Node\Name('ApiResource'), + $this->arrayToArguments($arguments) + ), + ]); + + array_unshift($node->attrGroups, $apiResourceAttribute); + } + } + + private function getGraphQlOperationNamespace(string $operationName, array $operation = []): string + { + switch ($operationName) { + case 'item_query': + return Query::class; + case 'collection_query': + return QueryCollection::class; + case 'update': + return Mutation::class; + case 'delete': + return Mutation::class; + case 'create': + return Mutation::class; + default: + if (isset($operation['item_query'])) { + return Query::class; + } + + if (isset($operation['collection_query'])) { + return QueryCollection::class; + } + + if (isset($operation['mutation'])) { + return Mutation::class; + } + + throw new \LogicException(sprintf('The graphql operation %s is not following API Platform naming convention.', $operationName)); + } + } + + private function getOperationNamespace(string $method, bool $isCollection = false): string + { + switch ($method) { + case 'POST': + return Post::class; + case 'PUT': + return Put::class; + case 'PATCH': + return Patch::class; + case 'DELETE': + return Delete::class; + default: + return $isCollection ? GetCollection::class : Get::class; + } + } + + private function getGraphQlOperationsNamespaces(array $operations): array + { + $namespaces = []; + foreach ($operations as $operationName => $operation) { + if (\is_string($operationName)) { + $namespaces[] = $this->getGraphQlOperationNamespace($operationName, $operation); + continue; + } + + $namespaces[] = $this->getGraphQlOperationNamespace($operation); + } + + return $namespaces; + } + + private function getOperationsNamespaces(array $operations, bool $isCollection = false): array + { + $namespaces = []; + foreach ($operations as $operationName => $operation) { + if (\is_string($operationName)) { + $namespaces[] = $this->getOperationNamespace($operation['method'] ?? strtoupper($operationName), $isCollection); + continue; + } + + $method = \is_string($operation) ? strtoupper($operation) : $operation['method']; + $namespaces[] = $this->getOperationNamespace($method, $isCollection); + } + + return $namespaces; + } + + /** + * @return Node\Arg[] + */ + private function arrayToArguments(array $arguments) + { + $args = []; + foreach ($arguments as $key => $value) { + $args[] = new Node\Arg($value, false, false, [], new Node\Identifier($key)); + } + + return $args; + } + + private function removeAttribute(Node\Stmt\Class_|Node\Stmt\Interface_ $node) + { + foreach ($node->attrGroups as $k => $attrGroupNode) { + foreach ($attrGroupNode->attrs as $i => $attribute) { + if (str_ends_with(implode('\\', $attribute->name->parts), 'ApiResource')) { + unset($node->attrGroups[$k]); + break; + } + } + } + } + + private function removeAnnotation(Node\Stmt\Class_|Node\Stmt\Interface_ $node) + { + $comment = $node->getDocComment(); + + if (preg_match('/@ApiResource/', $comment->getText())) { + $node->setDocComment($this->removeAnnotationByTag($comment, 'ApiResource')); + } + } + + private function valueToNode(mixed $value) + { + if (\is_string($value)) { + if (class_exists($value)) { + return new Node\Expr\ClassConstFetch(new Node\Name($this->getShortName($value)), 'class'); + } + + return new Node\Scalar\String_($value); + } + + if (\is_bool($value)) { + return new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false')); + } + + if (is_numeric($value)) { + return \is_int($value) ? new Node\Scalar\LNumber($value) : new Node\Scalar\DNumber($value); + } + + if (\is_array($value)) { + return new Node\Expr\Array_( + array_map(function ($key, $value) { + return new Node\Expr\ArrayItem( + $this->valueToNode($value), + \is_string($key) ? $this->valueToNode($key) : null, + ); + }, array_keys($value), array_values($value)), + [ + 'kind' => Node\Expr\Array_::KIND_SHORT, + ] + ); + } + } + + private function getShortName(string $class): string + { + if (false !== $pos = strrpos($class, '\\')) { + return substr($class, $pos + 1); + } + + return $class; + } + + private function createOperation(string $namespace, array $arguments = []) + { + $args = []; + foreach ($arguments as $key => $value) { + [$key, $value] = $this->getKeyValue($key, $value); + $args[$key] = $this->valueToNode($value); + } + + return new Node\Expr\New_(new Node\Name($this->getShortName($namespace)), $this->arrayToArguments($args)); + } + + private function legacyOperationsToOperations($legacyOperations, bool $isCollection = false) + { + $operations = []; + foreach ($legacyOperations as $operationName => $operation) { + if (\is_int($operationName)) { + $operations[] = $this->createOperation($this->getOperationNamespace(strtoupper($operation), $isCollection)); + continue; + } + + $method = $operation['method'] ?? strtoupper($operationName); + unset($operation['method']); + $operations[] = $this->createOperation($this->getOperationNamespace($method, $isCollection), $operation); + } + + return $operations; + } +} diff --git a/src/Core/Bridge/Rector/Parser/TransformApiSubresourceVisitor.php b/src/Core/Upgrade/UpgradeApiSubresourceVisitor.php similarity index 81% rename from src/Core/Bridge/Rector/Parser/TransformApiSubresourceVisitor.php rename to src/Core/Upgrade/UpgradeApiSubresourceVisitor.php index b89df8d43b..a9348690be 100644 --- a/src/Core/Bridge/Rector/Parser/TransformApiSubresourceVisitor.php +++ b/src/Core/Upgrade/UpgradeApiSubresourceVisitor.php @@ -11,17 +11,20 @@ declare(strict_types=1); -namespace ApiPlatform\Core\Bridge\Rector\Parser; +namespace ApiPlatform\Core\Upgrade; use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Core\Annotation\ApiSubresource; +use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; use ReflectionClass; -final class TransformApiSubresourceVisitor extends NodeVisitorAbstract +final class UpgradeApiSubresourceVisitor extends NodeVisitorAbstract { + use RemoveAnnotationTrait; private $subresourceMetadata; private $referenceType; @@ -35,19 +38,35 @@ public function enterNode(Node $node) { $operationToCreate = $this->subresourceMetadata['collection'] ? GetCollection::class : Get::class; $operationUseStatementNeeded = true; + $apiResourceUseStatementNeeded = true; + + $comment = $node->getDocComment(); + if ($comment && preg_match('/@ApiSubresource/', $comment->getText())) { + $node->setDocComment($this->removeAnnotationByTag($comment, 'ApiSubresource')); + } if ($node instanceof Node\Stmt\Namespace_) { - foreach ($node->stmts as $stmt) { + foreach ($node->stmts as $i => $stmt) { if (!$stmt instanceof Node\Stmt\Use_) { break; } $useStatement = implode('\\', $stmt->uses[0]->name->parts); + if (ApiSubresource::class === $useStatement) { + unset($node->stmts[$i]); + } + + if (ApiResource::class === $useStatement) { + $apiResourceUseStatementNeeded = false; + continue; + } + if ($useStatement === $operationToCreate) { $operationUseStatementNeeded = false; - break; + continue; } } + if ($operationUseStatementNeeded) { array_unshift( $node->stmts, @@ -60,6 +79,19 @@ public function enterNode(Node $node) ]) ); } + + if ($apiResourceUseStatementNeeded) { + array_unshift( + $node->stmts, + new Node\Stmt\Use_([ + new Node\Stmt\UseUse( + new Node\Name( + ApiResource::class + ) + ), + ]) + ); + } } if ($node instanceof Node\Stmt\Class_) { @@ -218,23 +250,34 @@ public function enterNode(Node $node) ); } - $apiResourceAttribute = - new Node\AttributeGroup([ - new Node\Attribute( - new Node\Name('\\ApiPlatform\\Metadata\\ApiResource'), - $arguments + $arguments[] = new Node\Arg( + new Node\Expr\Array_( + [ + new Node\Expr\ArrayItem( + new Node\Expr\New_( + new Node\Name($this->subresourceMetadata['collection'] ? 'GetCollection' : 'Get') + ), + ), + ], + [ + 'kind' => Node\Expr\Array_::KIND_SHORT, + ] ), - ]); + false, + false, + [], + new Node\Identifier('operations') + ); - $operationAttribute = + $apiResourceAttribute = new Node\AttributeGroup([ new Node\Attribute( - new Node\Name($this->subresourceMetadata['collection'] ? 'GetCollection' : 'Get') + new Node\Name('ApiResource'), + $arguments ), ]); $node->attrGroups[] = $apiResourceAttribute; - $node->attrGroups[] = $operationAttribute; } } } diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index 89606bd793..55314697ea 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -154,13 +154,22 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati return GraphQLType::string(); } + $operationName = $rootOperation->getName(); + $isCollection = 'collection_query' === $operationName; + + // We're retrieving the type of a property which is a relation to the rootResource + if ($resourceClass !== $rootResource && $property && $rootOperation instanceof Query) { + $isCollection = $this->typeBuilder->isCollection($type); + $operationName = $isCollection ? 'collection_query' : 'query'; + } + try { - $operation = $resourceMetadataCollection->getOperation($rootOperation->getName()); + $operation = $resourceMetadataCollection->getOperation($operationName); } catch (OperationNotFoundException $e) { $operation = (new Query()) ->withResource($resourceMetadataCollection[0]) - ->withName($rootOperation->getName()) - ->withCollection('collection_query' === $rootOperation->getName()); + ->withName($operationName) + ->withCollection($isCollection); } return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth); diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index 662f182d0f..f23da43cf1 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -29,6 +29,11 @@ public function __construct(array $operations = []) { $this->operations = []; foreach ($operations as $operationName => $operation) { + // When we use an int-indexed array in the constructor, compute priorities + if (\is_int($operationName)) { + $operation = $operation->withPriority($operationName); + } + $this->operations[] = [$operationName, $operation]; } diff --git a/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php b/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php index 43d7ff537a..485ed9dadc 100644 --- a/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Metadata\Property\Factory; -use ApiPlatform\Core\Metadata\Property\PropertyMetadata; -use ApiPlatform\Core\Metadata\Property\SubresourceMetadata; use ApiPlatform\Exception\PropertyNotFoundException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\Extractor\PropertyExtractorInterface; @@ -68,6 +66,7 @@ public function create(string $resourceClass, string $property, array $options = foreach ($propertyMetadata as $key => $value) { if ('subresource' === $key) { + trigger_deprecation('api-platform', '2.7', 'Using "subresource" is deprecated, declare another resource instead.'); continue; } @@ -93,23 +92,15 @@ public function create(string $resourceClass, string $property, array $options = $apiProperty = $apiProperty->withTypes([$propertyMetadata['iri']]); } - if (isset($propertyMetadata['subresource']) && $subresource = $this->createSubresourceMetadata($propertyMetadata['subresource'], $apiProperty)) { - return $apiProperty->withSubresource($subresource); - } - return $apiProperty; } /** * Returns the metadata from the decorated factory if available or throws an exception. * - * @param ApiProperty|PropertyMetadata|null $parentPropertyMetadata - * * @throws PropertyNotFoundException - * - * @return ApiProperty|PropertyMetadata */ - private function handleNotFound($parentPropertyMetadata, string $resourceClass, string $property) + private function handleNotFound(?ApiProperty $parentPropertyMetadata, string $resourceClass, string $property): ApiProperty { if ($parentPropertyMetadata) { return $parentPropertyMetadata; @@ -120,12 +111,8 @@ private function handleNotFound($parentPropertyMetadata, string $resourceClass, /** * Creates a new instance of metadata if the property is not already set. - * - * @param ApiProperty|PropertyMetadata|null $propertyMetadata - * - * @return ApiProperty|PropertyMetadata */ - private function update($propertyMetadata, array $metadata) + private function update(ApiProperty $propertyMetadata, array $metadata): ApiProperty { $metadataAccessors = [ 'description' => 'get', @@ -145,62 +132,15 @@ private function update($propertyMetadata, array $metadata) $propertyMetadata = $propertyMetadata->{'with'.ucfirst($metadataKey)}($metadata[$metadataKey]); } - if ($propertyMetadata instanceof ApiProperty) { - if (isset($metadata['attributes'])) { - $propertyMetadata = $this->withDeprecatedAttributes($propertyMetadata, $metadata['attributes']); - } - - if (isset($metadata['iri'])) { - trigger_deprecation('api-platform', '2.7', 'Using "iri" is deprecated, use "types" instead.'); - $propertyMetadata = $propertyMetadata->withTypes([$metadata['iri']]); - } - } else { - $propertyMetadata = $propertyMetadata->withIri($metadata['iri'])->withAttributes($metadata['attributes']); + if (isset($metadata['attributes'])) { + $propertyMetadata = $this->withDeprecatedAttributes($propertyMetadata, $metadata['attributes']); } - if ($propertyMetadata->hasSubresource()) { - return $propertyMetadata; - } - - if (isset($metadata['subresource']) && $subresource = $this->createSubresourceMetadata($metadata['subresource'], $propertyMetadata)) { - return $propertyMetadata->withSubresource($subresource); + if (isset($metadata['iri'])) { + trigger_deprecation('api-platform', '2.7', 'Using "iri" is deprecated, use "types" instead.'); + $propertyMetadata = $propertyMetadata->withTypes([$metadata['iri']]); } return $propertyMetadata; } - - /** - * Creates a SubresourceMetadata. - * - * @param bool|array|null $subresource the subresource metadata coming from XML or YAML - * @param ApiProperty|PropertyMetadata $propertyMetadata the current property metadata - */ - private function createSubresourceMetadata($subresource, $propertyMetadata): ?SubresourceMetadata - { - if (!$subresource) { - return null; - } - - $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : $propertyMetadata->getBuiltinTypes()[0] ?? null; - $maxDepth = \is_array($subresource) ? $subresource['maxDepth'] ?? null : null; - - if (null !== $type) { - $isCollection = $type->isCollection(); - if ( - $isCollection && - $collectionValueType = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType() - ) { - $resourceClass = $collectionValueType->getClassName(); - } else { - $resourceClass = $type->getClassName(); - } - } elseif (\is_array($subresource) && isset($subresource['resourceClass'])) { - $resourceClass = $subresource['resourceClass']; - $isCollection = $subresource['collection'] ?? true; - } else { - return null; - } - - return new SubresourceMetadata($resourceClass, $isCollection, $maxDepth); - } } diff --git a/src/Metadata/Property/Factory/LegacyPropertyMetadataFactory.php b/src/Metadata/Property/Factory/LegacyPropertyMetadataFactory.php index c376a590b4..84b530e52d 100644 --- a/src/Metadata/Property/Factory/LegacyPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/LegacyPropertyMetadataFactory.php @@ -77,6 +77,7 @@ public function create(string $resourceClass, string $property, array $options = } $wither = str_replace(['get', 'is'], 'with', $method); + if (method_exists($propertyMetadata, $wither) && null !== $legacyPropertyMetadata->{$method}()) { $propertyMetadata = $propertyMetadata->{$wither}($legacyPropertyMetadata->{$method}()); } diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index b46a424f90..8d713dd0c7 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -75,10 +75,6 @@ public function create(string $resourceClass): ResourceMetadataCollection if (\PHP_VERSION_ID >= 80000 && $this->hasResourceAttributes($reflectionClass)) { foreach ($this->buildResourceOperations($reflectionClass->getAttributes(), $resourceClass) as $i => $resource) { - if (0 === $i && $this->graphQlEnabled && null === $resource->getGraphQlOperations()) { - $resource = $this->addDefaultGraphQlOperations($resource); - } - $resourceMetadataCollection[] = $resource; } } @@ -144,6 +140,24 @@ private function buildResourceOperations(array $attributes, string $resourceClas } $resources[$index] = $resources[$index]->withOperations(new Operations($operations)); + $graphQlOperations = $resource->getGraphQlOperations(); + + if ([] === $graphQlOperations) { + continue; + } + + if (null === $graphQlOperations) { + $resources[$index] = $this->addDefaultGraphQlOperations($resources[$index]); + continue; + } + + $graphQlOperationsWithDefaults = []; + foreach ($graphQlOperations as $i => $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $graphQlOperationsWithDefaults[$key] = $operation; + } + + $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperationsWithDefaults); } return $resources; @@ -199,6 +213,9 @@ private function getOperationWithDefaults(ApiResource $resource, $operation): ar return [$operation->getName(), $operation]; } + if ($operation->getRouteName()) { + $operation = $operation->withName($operation->getRouteName()); + } // Check for name conflict if ($operation->getName()) { if (null !== $resource->getOperations() && !$resource->getOperations()->has($operation->getName())) { @@ -218,7 +235,7 @@ private function getOperationWithDefaults(ApiResource $resource, $operation): ar private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource { $resource = $resource - ->withShortName($shortName) + ->withShortName($resource->getShortName() ?? $shortName) ->withClass($resourceClass); foreach ($this->defaults['attributes'] as $key => $value) { diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index b478f34bd6..6e015dc4f8 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -52,7 +52,6 @@ public function create(string $resourceClass): ResourceMetadataCollection foreach ($resourceMetadataCollection as $i => $resource) { /** @var ApiResource $resource */ $resource = $this->configureUriVariables($resource); - if ($resource->getUriTemplate()) { $resourceMetadataCollection[$i] = $resource->withExtraProperties($resource->getExtraProperties() + ['user_defined_uri_template' => true]); } diff --git a/src/State/SerializerAwareProviderInterface.php b/src/State/SerializerAwareProviderInterface.php new file mode 100644 index 0000000000..25ff6f814a --- /dev/null +++ b/src/State/SerializerAwareProviderInterface.php @@ -0,0 +1,26 @@ + + * + * 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\State; + +use Psr\Container\ContainerInterface; + +/** + * Injects serializer in providers. + * + * @author Vincent Chalamon + */ +interface SerializerAwareProviderInterface +{ + public function setSerializerLocator(ContainerInterface $serializerLocator); +} diff --git a/src/State/SerializerAwareProviderTrait.php b/src/State/SerializerAwareProviderTrait.php new file mode 100644 index 0000000000..3c8c65570c --- /dev/null +++ b/src/State/SerializerAwareProviderTrait.php @@ -0,0 +1,42 @@ + + * + * 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\State; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * Injects serializer in providers. + * + * @author Vincent Chalamon + */ +trait SerializerAwareProviderTrait +{ + /** + * @internal + * + * @var ContainerInterface + */ + private $serializerLocator; + + public function setSerializerLocator(ContainerInterface $serializerLocator): void + { + $this->serializerLocator = $serializerLocator; + } + + private function getSerializer(): SerializerInterface + { + return $this->serializerLocator->get('serializer'); + } +} diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 9a1f50c2cf..e73a234c30 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -131,7 +131,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerMakerConfiguration($container, $config, $loader); $this->registerArgumentResolverConfiguration($container, $loader, $config); $this->registerLegacyServices($container, $config, $loader); - $this->registerRectorConfiguration($container, $loader, $config); + $this->registerUpgradeCommandConfiguration($container, $loader, $config); // TODO: remove in 3.x $container->registerForAutoconfiguration(DataPersisterInterface::class) @@ -918,9 +918,9 @@ private function registerLegacyServices(ContainerBuilder $container, array $conf $loader->load('legacy/api.xml'); } - private function registerRectorConfiguration(ContainerBuilder $container, XmlFileLoader $loader, array $config): void + private function registerUpgradeCommandConfiguration(ContainerBuilder $container, XmlFileLoader $loader, array $config): void { - $loader->load('legacy/rector.xml'); + $loader->load('legacy/upgrade.xml'); } private function buildDeprecationArgs(string $version, string $message): array diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php index 1364a760e4..ec3e7387e5 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php @@ -15,6 +15,7 @@ use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\DataProvider\SerializerAwareDataProviderInterface; +use ApiPlatform\State\SerializerAwareProviderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -37,6 +38,15 @@ public function process(ContainerBuilder $container) foreach (OperationType::TYPES as $type) { $this->addSerializerLocator($container, $type); } + + $services = $container->findTaggedServiceIds('api_platform.state_provider', true); + + foreach ($services as $id => $tags) { + $definition = $container->getDefinition((string) $id); + if (is_a($definition->getClass(), SerializerAwareProviderInterface::class, true)) { + $definition->addMethodCall('setSerializerLocator', [new Reference('api_platform.serializer_locator')]); + } + } } private function addSerializerLocator(ContainerBuilder $container, string $type): void diff --git a/src/Symfony/Bundle/Resources/config/legacy/rector.xml b/src/Symfony/Bundle/Resources/config/legacy/upgrade.xml similarity index 61% rename from src/Symfony/Bundle/Resources/config/legacy/rector.xml rename to src/Symfony/Bundle/Resources/config/legacy/upgrade.xml index aba913efb4..f3d0eeeb54 100644 --- a/src/Symfony/Bundle/Resources/config/legacy/rector.xml +++ b/src/Symfony/Bundle/Resources/config/legacy/upgrade.xml @@ -5,13 +5,14 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + - + + diff --git a/src/Symfony/Bundle/Resources/config/metadata/property.xml b/src/Symfony/Bundle/Resources/config/metadata/property.xml index a06ae61be3..25fac8fa1c 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/property.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/property.xml @@ -35,7 +35,8 @@ - + + @@ -51,9 +52,9 @@ - - - + + + diff --git a/src/Symfony/Bundle/Resources/config/metadata/yaml.xml b/src/Symfony/Bundle/Resources/config/metadata/yaml.xml index 8c55cd7191..2e417e7c57 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/yaml.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/yaml.xml @@ -35,5 +35,10 @@ + + + + + diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/ApiResourceAnnotationToApiResourceAttributeRectorTest.php b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/ApiResourceAnnotationToApiResourceAttributeRectorTest.php deleted file mode 100644 index 0441b5d952..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/ApiResourceAnnotationToApiResourceAttributeRectorTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * 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\Tests\Bridge\Rector\Rules\ApiResourceAnnotationToApiResourceAttributeRector; - -use Iterator; -use Rector\Testing\PHPUnit\AbstractRectorTestCase; -use Symplify\SmartFileSystem\SmartFileInfo; - -/* - * @requires PHP 8.0 - */ -if (class_exists(AbstractRectorTestCase::class)) { - class ApiResourceAnnotationToApiResourceAttributeRectorTest extends AbstractRectorTestCase - { - /** - * @dataProvider provideData() - */ - public function test(SmartFileInfo $fileInfo): void - { - $this->doTestFileInfo($fileInfo); - } - - /** - * @return Iterator - */ - public function provideData(): Iterator - { - return $this->yieldFilesFromDirectory(__DIR__.'/Fixture'); - } - - public function provideConfigFilePath(): string - { - return __DIR__.'/config/configured_rule.php'; - } - } -} diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/absolute_url_dummy.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/absolute_url_dummy.php.inc deleted file mode 100644 index 7f113fff55..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/absolute_url_dummy.php.inc +++ /dev/null @@ -1,41 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/book.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/book.php.inc deleted file mode 100644 index 3e9eb0e2bd..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/book.php.inc +++ /dev/null @@ -1,33 +0,0 @@ - ------ - '.+'], uriVariables: 'isbn')] -class Book -{ -} - -?> diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/custom_action_dummy.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/custom_action_dummy.php.inc deleted file mode 100644 index 9906dfdb44..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/custom_action_dummy.php.inc +++ /dev/null @@ -1,48 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/disable_item_operation.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/disable_item_operation.php.inc deleted file mode 100644 index 3bbcf87625..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/disable_item_operation.php.inc +++ /dev/null @@ -1,44 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/dummy_custom_mutation.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/dummy_custom_mutation.php.inc deleted file mode 100644 index c43d2914ca..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/dummy_custom_mutation.php.inc +++ /dev/null @@ -1,93 +0,0 @@ - ------ - ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'sumNotPersisted', resolver: 'app.graphql.mutation_resolver.dummy_custom_not_persisted', normalizationContext: ['groups' => ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'sumNoWriteCustomResult', resolver: 'app.graphql.mutation_resolver.dummy_custom_no_write_custom_result', write: false, normalizationContext: ['groups' => ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'sumOnlyPersist', resolver: 'app.graphql.mutation_resolver.dummy_custom_only_persist', read: false, deserialize: false, validate: false, serialize: false, normalizationContext: ['groups' => ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'testCustomArguments', resolver: 'app.graphql.mutation_resolver.dummy_custom', args: ['operandC' => ['type' => 'Int!']])] -#[Query(name: 'testQuery', serialize: false, resolver: 'app.graphql.query_resolver.dummy_custom')] -#[QueryCollection(name: 'testQueryCollection', read: false, resolver: 'app.graphql.collection_query_resolver.dummy_custom')] -#[QueryCollection(paginationEnabled: false)] -#[Mutation(name: 'create')] -class DummyCustomMutation -{ -} - -?> diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/dummy_dto_no_input.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/dummy_dto_no_input.php.inc deleted file mode 100644 index 7c70bda3d1..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/dummy_dto_no_input.php.inc +++ /dev/null @@ -1,65 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/minimal.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/minimal.php.inc deleted file mode 100644 index 01d2705527..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/minimal.php.inc +++ /dev/null @@ -1,39 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/not_a_resource.php.inc b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/not_a_resource.php.inc deleted file mode 100644 index d15884bcaf..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/Fixture/not_a_resource.php.inc +++ /dev/null @@ -1,19 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/config/configured_rule.php b/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/config/configured_rule.php deleted file mode 100644 index eaa8eb4dc6..0000000000 --- a/tests/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector/config/configured_rule.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use ApiPlatform\Core\Bridge\Rector\Rules\ApiResourceAnnotationToApiResourceAttributeRector; -use Rector\Core\Configuration\Option; -use Rector\Core\ValueObject\PhpVersion; -use Rector\Php80\ValueObject\AnnotationToAttribute; -use Rector\Renaming\Rector\Namespace_\RenameNamespaceRector; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; -use Symplify\SymfonyPhpConfig\ValueObjectInliner; - -return static function (ContainerConfigurator $containerConfigurator): void { - $parameters = $containerConfigurator->parameters(); - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_80); - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - - $services = $containerConfigurator->services(); - - $services->set(RenameNamespaceRector::class) - ->call('configure', [[ - RenameNamespaceRector::OLD_TO_NEW_NAMESPACES => [ - 'ApiPlatform\Core\Annotation\ApiResource' => 'ApiPlatform\Metadata\ApiResource', - ], - ]]); - - $services->set(ApiResourceAnnotationToApiResourceAttributeRector::class) - ->call('configure', [[ - ApiResourceAnnotationToApiResourceAttributeRector::ANNOTATION_TO_ATTRIBUTE => ValueObjectInliner::inline([ - new AnnotationToAttribute( - \ApiPlatform\Core\Annotation\ApiResource::class, - \ApiPlatform\Metadata\ApiResource::class - ), - ]), - ]]); -}; diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/absolute_url_dummy.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/absolute_url_dummy.php.inc deleted file mode 100644 index ee94fc38e9..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/absolute_url_dummy.php.inc +++ /dev/null @@ -1,39 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/book.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/book.php.inc deleted file mode 100644 index c0841dd655..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/book.php.inc +++ /dev/null @@ -1,28 +0,0 @@ - ['method' => 'GET', 'path' => '/books/by_isbn/{isbn}.{_format}', 'requirements' => ['isbn' => '.+'], 'identifiers' => 'isbn']])] -class Book -{ -} - -?> ------ - '.+'], uriVariables: 'isbn')] -class Book -{ -} - -?> diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/custom_action_dummy.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/custom_action_dummy.php.inc deleted file mode 100644 index 517a931599..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/custom_action_dummy.php.inc +++ /dev/null @@ -1,35 +0,0 @@ - ['method' => 'GET', 'path' => 'custom_action_collection_dummies/{id}'], 'custom_normalization' => ['route_name' => 'custom_normalization', 'method' => 'GET'], 'short_custom_normalization' => ['route_name' => 'short_custom_normalization', 'method' => 'GET']], collectionOperations: ['get', 'get_custom' => ['method' => 'GET', 'path' => 'custom_action_collection_dummies'], 'custom_denormalization' => ['route_name' => 'custom_denormalization', 'method' => 'GET'], 'short_custom_denormalization' => ['route_name' => 'short_custom_denormalization', 'method' => 'GET']])] -class CustomActionDummy -{ -} - -?> ------ - diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/disable_item_operation.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/disable_item_operation.php.inc deleted file mode 100644 index 804582ab32..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/disable_item_operation.php.inc +++ /dev/null @@ -1,31 +0,0 @@ - ['controller' => NotFoundAction::class, 'read' => false, 'output' => false]])] -class DisableItemOperation -{ -} - -?> ------ - diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/dummy_custom_mutation.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/dummy_custom_mutation.php.inc deleted file mode 100644 index 1841446241..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/dummy_custom_mutation.php.inc +++ /dev/null @@ -1,91 +0,0 @@ - [ - 'mutation' => 'app.graphql.mutation_resolver.dummy_custom', - 'normalization_context' => [ - 'groups' => ['result'] - ], - 'denormalization_context' => [ - 'groups' => ['sum'] - ] - ], - 'sumNotPersisted' => [ - 'mutation' => 'app.graphql.mutation_resolver.dummy_custom_not_persisted', - 'normalization_context' => [ - 'groups' => ['result'] - ], - 'denormalization_context' => [ - 'groups' => ['sum'] - ] - ], - 'sumNoWriteCustomResult' => [ - 'mutation' => 'app.graphql.mutation_resolver.dummy_custom_no_write_custom_result', - 'normalization_context' => [ - 'groups' => ['result'] - ], - 'denormalization_context' => [ - 'groups' => ['sum'] - ], - 'write' => false - ], - 'sumOnlyPersist' => [ - 'mutation' => 'app.graphql.mutation_resolver.dummy_custom_only_persist', - 'normalization_context' => [ - 'groups' => ['result'] - ], - 'denormalization_context' => [ - 'groups' => ['sum'] - ], - 'read' => false, - 'deserialize' => false, - 'validate' => false, - 'serialize' => false - ], - 'testCustomArguments' => [ - 'mutation' => 'app.graphql.mutation_resolver.dummy_custom', - 'args' => [ - 'operandC' => ['type' => 'Int!'] - ] - ] -])] -class DummyCustomMutation -{ -} - -?> ------ - ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'sumNotPersisted', resolver: 'app.graphql.mutation_resolver.dummy_custom_not_persisted', normalizationContext: ['groups' => ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'sumNoWriteCustomResult', resolver: 'app.graphql.mutation_resolver.dummy_custom_no_write_custom_result', write: false, normalizationContext: ['groups' => ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'sumOnlyPersist', resolver: 'app.graphql.mutation_resolver.dummy_custom_only_persist', read: false, deserialize: false, validate: false, serialize: false, normalizationContext: ['groups' => ['result']], denormalizationContext: ['groups' => ['sum']])] -#[Mutation(name: 'testCustomArguments', resolver: 'app.graphql.mutation_resolver.dummy_custom', args: ['operandC' => ['type' => 'Int!']])] -class DummyCustomMutation -{ -} - -?> diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/dummy_dto_no_input.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/dummy_dto_no_input.php.inc deleted file mode 100644 index 4068e115d3..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/dummy_dto_no_input.php.inc +++ /dev/null @@ -1,40 +0,0 @@ - false, 'output' => OutputDto::class], collectionOperations: ['post' => ['method' => 'POST', 'path' => '/dummy_dto_no_inputs', 'controller' => CreateItemAction::class], 'get'], itemOperations: ['get', 'delete', 'post_double_bat' => ['method' => 'POST', 'path' => '/dummy_dto_no_inputs/{id}/double_bat', 'controller' => DoubleBatAction::class, 'status' => 200]])] -class DummyDtoNoInput -{ -} - -?> ------ - diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/minimal.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/minimal.php.inc deleted file mode 100644 index 5180417d26..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/minimal.php.inc +++ /dev/null @@ -1,37 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/not_a_resource.php.inc b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/not_a_resource.php.inc deleted file mode 100644 index 06e47b51f1..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/Fixture/not_a_resource.php.inc +++ /dev/null @@ -1,19 +0,0 @@ - ------ - diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/LegacyApiResourceAttributeToResourceAttributeRectorTest.php b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/LegacyApiResourceAttributeToResourceAttributeRectorTest.php deleted file mode 100644 index 01999be40b..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/LegacyApiResourceAttributeToResourceAttributeRectorTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * 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\Tests\Bridge\Rector\Rules\LegacyApiResourceAttributeToApiResourceAttributeRector; - -use Iterator; -use Rector\Testing\PHPUnit\AbstractRectorTestCase; -use Symplify\SmartFileSystem\SmartFileInfo; - -/* - * @requires PHP 8.0 - */ -if (class_exists(AbstractRectorTestCase::class)) { - class LegacyApiResourceAttributeToResourceAttributeRectorTest extends AbstractRectorTestCase - { - /** - * @dataProvider provideData() - */ - public function test(SmartFileInfo $fileInfo): void - { - $this->doTestFileInfo($fileInfo); - } - - /** - * @return Iterator - */ - public function provideData(): Iterator - { - return $this->yieldFilesFromDirectory(__DIR__.'/Fixture'); - } - - public function provideConfigFilePath(): string - { - return __DIR__.'/config/configured_rule.php'; - } - } -} diff --git a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/config/configured_rule.php b/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/config/configured_rule.php deleted file mode 100644 index 28db72cd65..0000000000 --- a/tests/Core/Bridge/Rector/Rules/LegacyApiResourceAttributeToApiResourceAttributeRector/config/configured_rule.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use ApiPlatform\Core\Bridge\Rector\Rules\LegacyApiResourceAttributeToApiResourceAttributeRector; -use Rector\Core\Configuration\Option; -use Rector\Core\ValueObject\PhpVersion; -use Rector\Renaming\Rector\Namespace_\RenameNamespaceRector; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - -return static function (ContainerConfigurator $containerConfigurator): void { - $parameters = $containerConfigurator->parameters(); - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_80); - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - - $services = $containerConfigurator->services(); - - $services->set(RenameNamespaceRector::class) - ->call('configure', [[ - RenameNamespaceRector::OLD_TO_NEW_NAMESPACES => [ - 'ApiPlatform\Core\Annotation\ApiResource' => 'ApiPlatform\Metadata\ApiResource', - ], - ]]); - - $services->set(LegacyApiResourceAttributeToApiResourceAttributeRector::class) - ->call('configure', [[ - LegacyApiResourceAttributeToApiResourceAttributeRector::REMOVE_INITIAL_ATTRIBUTE => true, - ]]); -}; diff --git a/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php b/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php new file mode 100644 index 0000000000..0ef5588a9d --- /dev/null +++ b/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php @@ -0,0 +1,103 @@ + + * + * 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\Core\Tests\Bridge\Symfony\Bundle\Command; + +use ApiPlatform\Core\Bridge\Symfony\Bundle\Command\UpgradeApiResourceCommand; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; +use ApiPlatform\Core\Tests\ProphecyTrait; +use ApiPlatform\Core\Upgrade\SubresourceTransformer; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use Doctrine\Common\Annotations\AnnotationReader; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; + +class UpgradeApiResourceCommandTest extends TestCase +{ + use ProphecyTrait; + + private function getCommandTester(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, SubresourceOperationFactoryInterface $subresourceOperationFactory): CommandTester + { + $application = new Application(); + $application->setCatchExceptions(false); + $application->setAutoExit(false); + + $application->add(new UpgradeApiResourceCommand($resourceNameCollectionFactory, $resourceMetadataFactory, $subresourceOperationFactory, new SubresourceTransformer(), new AnnotationReader())); + + $command = $application->find('api:upgrade-resource'); + + return new CommandTester($command); + } + + /** + * @requires PHP 8.1 + */ + public function testDebugResource() + { + $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); + $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([RelatedDummy::class])); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadata()); + $subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class); + $subresourceOperationFactoryProphecy->create(RelatedDummy::class)->willReturn([[ + 'property' => 'id', + 'collection' => false, + 'resource_class' => RelatedDummy::class, + 'shortNames' => [ + 'RelatedDummy', + ], + 'legacy_filters' => [ + 'related_dummy.friends', + 'related_dummy.complex_sub_query', + ], + 'legacy_normalization_context' => [ + 'groups' => [ + 'friends', + ], + ], + 'legacy_type' => 'https://schema.org/Product', + 'identifiers' => [ + 'id' => [ + RelatedDummy::class, + 'id', + true, + ], + ], + 'operation_name' => 'id_get_subresource', + 'route_name' => 'api_related_dummies_id_get_subresource', + 'path' => '/related_dummies/{id}/id.{_format}', + ]]); + + $commandTester = $this->getCommandTester($resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $subresourceOperationFactoryProphecy->reveal()); + $commandTester->execute([]); + + $expectedStrings = [ + '-use ApiPlatform\\Core\\Annotation\\ApiSubresource', + '-use ApiPlatform\\Core\\Annotation\\ApiResource', + '+use ApiPlatform\\Metadata\\ApiResource', + '+use ApiPlatform\\Metadata\\Get', + "+#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])]", + "+#[ApiResource(uriTemplate: '/related_dummies/{id}/id', uriVariables: ['id' => ['from_class' => self::class, 'identifiers' => ['id']]], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]", + ]; + + $display = $commandTester->getDisplay(); + foreach ($expectedStrings as $expectedString) { + $this->assertStringContainsString($expectedString, $display); + } + } +} diff --git a/tests/Core/Bridge/Rector/Service/SubresourceTransformerTest.php b/tests/Core/Upgrade/SubresourceTransformerTest.php similarity index 99% rename from tests/Core/Bridge/Rector/Service/SubresourceTransformerTest.php rename to tests/Core/Upgrade/SubresourceTransformerTest.php index 5d6c00e8b1..aa70709ade 100644 --- a/tests/Core/Bridge/Rector/Service/SubresourceTransformerTest.php +++ b/tests/Core/Upgrade/SubresourceTransformerTest.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Tests\Core\Bridge\Rector\Service; -use ApiPlatform\Core\Bridge\Rector\Service\SubresourceTransformer; +use ApiPlatform\Core\Upgrade\SubresourceTransformer; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Answer; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyAggregateOffer; diff --git a/tests/Fixtures/TestBundle/Document/ConvertedRelated.php b/tests/Fixtures/TestBundle/Document/ConvertedRelated.php index bc06fc7fac..01e3a7037a 100644 --- a/tests/Fixtures/TestBundle/Document/ConvertedRelated.php +++ b/tests/Fixtures/TestBundle/Document/ConvertedRelated.php @@ -18,7 +18,6 @@ /** * @ApiResource - * @ * @ODM\Document */ class ConvertedRelated diff --git a/tests/Fixtures/TestBundle/State/ProductItemProvider.php b/tests/Fixtures/TestBundle/State/ProductItemProvider.php new file mode 100644 index 0000000000..e27b9c9bd2 --- /dev/null +++ b/tests/Fixtures/TestBundle/State/ProductItemProvider.php @@ -0,0 +1,55 @@ + + * + * 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\Tests\Fixtures\TestBundle\State; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\Product as ProductDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Product; +use ApiPlatform\Tests\Fixtures\TestBundle\Model\ProductInterface; +use Doctrine\Persistence\ManagerRegistry; + +class ProductItemProvider implements ProviderInterface +{ + private $managerRegistry; + private $orm; + + public function __construct(ManagerRegistry $managerRegistry, bool $orm = true) + { + $this->managerRegistry = $managerRegistry; + $this->orm = $orm; + } + + /** + * {@inheritDoc} + */ + public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []) + { + return $this->managerRegistry->getRepository($this->orm ? Product::class : ProductDocument::class)->findOneBy([ + 'code' => $identifiers['code'], + ]); + } + + /** + * {@inheritDoc} + */ + public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool + { + /** @var Operation */ + $operation = $context['operation'] ?? new Get(); + + return is_a($resourceClass, ProductInterface::class, true) && !$operation->isCollection(); + } +} diff --git a/tests/Fixtures/TestBundle/State/SerializableProvider.php b/tests/Fixtures/TestBundle/State/SerializableProvider.php index add710c7b2..e98de37e70 100644 --- a/tests/Fixtures/TestBundle/State/SerializableProvider.php +++ b/tests/Fixtures/TestBundle/State/SerializableProvider.php @@ -13,16 +13,17 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\State; -use ApiPlatform\Core\DataProvider\SerializerAwareDataProviderTrait; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerAwareProviderInterface; +use ApiPlatform\State\SerializerAwareProviderTrait; use ApiPlatform\Tests\Fixtures\TestBundle\Model\SerializableResource; /** * @author Vincent Chalamon */ -class SerializableProvider implements ProviderInterface +class SerializableProvider implements ProviderInterface, SerializerAwareProviderInterface { - use SerializerAwareDataProviderTrait; + use SerializerAwareProviderTrait; /** * {@inheritDoc} diff --git a/tests/Fixtures/TestBundle/State/TaxonItemProvider.php b/tests/Fixtures/TestBundle/State/TaxonItemProvider.php new file mode 100644 index 0000000000..0624c8a42d --- /dev/null +++ b/tests/Fixtures/TestBundle/State/TaxonItemProvider.php @@ -0,0 +1,55 @@ + + * + * 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\Tests\Fixtures\TestBundle\State; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\Taxon as TaxonDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Taxon; +use ApiPlatform\Tests\Fixtures\TestBundle\Model\TaxonInterface; +use Doctrine\Persistence\ManagerRegistry; + +class TaxonItemProvider implements ProviderInterface +{ + private $managerRegistry; + private $orm; + + public function __construct(ManagerRegistry $managerRegistry, bool $orm = true) + { + $this->managerRegistry = $managerRegistry; + $this->orm = $orm; + } + + /** + * {@inheritDoc} + */ + public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []) + { + return $this->managerRegistry->getRepository($this->orm ? Taxon::class : TaxonDocument::class)->findOneBy([ + 'code' => $identifiers['code'], + ]); + } + + /** + * {@inheritDoc} + */ + public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool + { + /** @var Operation */ + $operation = $context['operation'] ?? new Get(); + + return is_a($resourceClass, TaxonInterface::class, true) && !$operation->isCollection(); + } +} diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index eee246c2b8..c5e950d4ce 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -85,9 +85,7 @@ public function registerBundles(): array $bundles[] = new NelmioApiDocBundle(); } - if ('elasticsearch' !== $this->getEnvironment()) { - $bundles[] = new TestBundle(); - } + $bundles[] = new TestBundle(); return $bundles; } diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 2fd8a03993..66ed130d0f 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -12,7 +12,9 @@ doctrine: orm: auto_generate_proxy_classes: '%kernel.debug%' - auto_mapping: true + mappings: + TestBundle: + type: 'annotation' mercure: hubs: diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index cfa78abc54..4640bdd108 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -14,7 +14,11 @@ doctrine_mongodb: default_database: '%env(resolve:MONGODB_DB)%' document_managers: default: - auto_mapping: true + mappings: + TestBundle: + type: 'annotation' + dir: '%kernel.project_dir%/../TestBundle/Document' + prefix: 'ApiPlatform\Tests\Fixtures\TestBundle\Document' api_platform: doctrine: false @@ -76,6 +80,15 @@ services: tags: - name: 'api_platform.item_data_provider' + ApiPlatform\Tests\Fixtures\TestBundle\State\ProductItemProvider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\ProductItemProvider' + public: false + arguments: + $managerRegistry: '@doctrine_mongodb' + $orm: false + tags: + - name: 'api_platform.state_provider' + ApiPlatform\Tests\Fixtures\TestBundle\DataProvider\TaxonItemDataProvider: public: false arguments: @@ -84,6 +97,15 @@ services: tags: - name: 'api_platform.item_data_provider' + ApiPlatform\Tests\Fixtures\TestBundle\State\TaxonItemProvider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\TaxonItemProvider' + public: false + arguments: + $managerRegistry: '@doctrine_mongodb' + $orm: false + tags: + - name: 'api_platform.state_provider' + related_questions.state_provider: class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\RelatedQuestionsProvider' public: false diff --git a/tests/Fixtures/app/config/config_test.yml b/tests/Fixtures/app/config/config_test.yml index d930705760..019bbd7a2a 100644 --- a/tests/Fixtures/app/config/config_test.yml +++ b/tests/Fixtures/app/config/config_test.yml @@ -88,6 +88,14 @@ services: tags: - name: 'api_platform.item_data_provider' + ApiPlatform\Tests\Fixtures\TestBundle\State\ProductItemProvider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\ProductItemProvider' + public: false + arguments: + $managerRegistry: '@doctrine' + tags: + - name: 'api_platform.state_provider' + ApiPlatform\Tests\Fixtures\TestBundle\DataProvider\TaxonItemDataProvider: public: false arguments: @@ -95,6 +103,14 @@ services: tags: - name: 'api_platform.item_data_provider' + ApiPlatform\Tests\Fixtures\TestBundle\State\TaxonItemProvider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\TaxonItemProvider' + public: false + arguments: + $managerRegistry: '@doctrine' + tags: + - name: 'api_platform.state_provider' + app.dummy_dto_no_input.data_provider: class: 'ApiPlatform\Tests\Fixtures\TestBundle\DataProvider\DummyDtoNoInputCollectionDataProvider' public: false diff --git a/tests/Symfony/Bundle/Command/RectorCommandTest.php b/tests/Symfony/Bundle/Command/RectorCommandTest.php deleted file mode 100644 index cde29288d7..0000000000 --- a/tests/Symfony/Bundle/Command/RectorCommandTest.php +++ /dev/null @@ -1,113 +0,0 @@ - - * - * 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\Tests\Bridge\Symfony\Bundle\Command; - -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\Console\Exception\MissingInputException; -use Symfony\Component\Console\Tester\CommandTester; - -/** - * @group legacy - */ -class RectorCommandTest extends KernelTestCase -{ - private $commandTester; - - protected function setUp(): void - { - self::bootKernel(); - - if (!file_exists('vendor/bin/rector')) { - $this->markTestSkipped(); - } - - $application = new Application(static::$kernel); - $application->setCatchExceptions(false); - $application->setAutoExit(false); - - $command = $application->find('api:rector:upgrade'); - $this->commandTester = new CommandTester($command); - } - - /** - * @requires PHP 8.0 - */ - public function testExecuteOperations() - { - $instantOperations = ['annotation-to-legacy-api-resource', 'transform-apisubresource']; - $operationsWithSubresourceConfirmation = ['annotation-to-api-resource', 'keep-attribute']; - $operations = $instantOperations + $operationsWithSubresourceConfirmation; - - foreach ($operations as $operation) { - // Answer no to the subresource question - $this->commandTester->setInputs(['no']); - - $this->commandTester->execute([ - 'src' => 'tests/Fixtures/TestBundle/Entity', - '--'.$operation => null, - '--dry-run' => null, - '--silent' => null, - ]); - - $this->assertStringContainsString('Migration successful.', $this->commandTester->getDisplay()); - } - } - - /** - * @requires PHP 8.0 - */ - public function testExecuteCancelOperation() - { - $this->commandTester->setInputs([0, 'no']); - - $this->commandTester->execute([ - 'src' => 'tests/Fixtures/TestBundle/Entity', - '--silent' => null, - ]); - - $this->assertStringContainsString('Migration aborted.', $this->commandTester->getDisplay()); - } - - /** - * @requires PHP 8.0 - */ - public function testExecuteWithWrongInput() - { - $this->expectException(MissingInputException::class); - - $this->commandTester->setInputs([4, 'yes']); - - $this->commandTester->execute([ - 'src' => 'tests/Fixtures/TestBundle/Entity', - '--silent' => null, - ]); - } - - /** - * @requires PHP 8.0 - */ - public function testExecuteWithTooMuchOptions() - { - $this->commandTester->execute([ - 'src' => 'tests/Fixtures/TestBundle/Entity', - '--annotation-to-api-resource' => null, - '--keep-attribute' => null, - '--dry-run' => null, - '--silent' => null, - ]); - - $this->assertSame('Only one operation can be given as a parameter.', $this->commandTester->getDisplay()); - } -} diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index b0e332f383..98c214cfbe 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -1785,13 +1785,13 @@ public function testRectorConfiguration(): void (new ApiPlatformExtension())->load($config, $this->container); $services = [ - // legacy/rector.xml - 'api_platform.rector.subresource_transformer', - 'api_platform.rector.command', + // legacy/upgrade.xml + 'api_platform.upgrade.subresource_transformer', + 'api_platform.upgrade_resource.command', ]; $tags = [ - 'api_platform.rector.command' => 'console.command', + 'api_platform.upgrade_resource.command' => 'console.command', ]; $this->assertContainerHas($services, [], $tags);