vendor/api-platform/core/src/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommand.php line 55

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the API Platform project.
  4. *
  5. * (c) Kévin Dunglas <dunglas@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. declare(strict_types=1);
  11. namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Command;
  12. use ApiPlatform\Core\Annotation\ApiResource;
  13. use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
  14. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  15. use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
  16. use ApiPlatform\Core\Upgrade\ColorConsoleDiffFormatter;
  17. use ApiPlatform\Core\Upgrade\SubresourceTransformer;
  18. use ApiPlatform\Core\Upgrade\UpgradeApiFilterVisitor;
  19. use ApiPlatform\Core\Upgrade\UpgradeApiPropertyVisitor;
  20. use ApiPlatform\Core\Upgrade\UpgradeApiResourceVisitor;
  21. use ApiPlatform\Core\Upgrade\UpgradeApiSubresourceVisitor;
  22. use ApiPlatform\Exception\ResourceClassNotFoundException;
  23. use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
  24. use Doctrine\Common\Annotations\AnnotationReader;
  25. use PhpParser\Lexer\Emulative;
  26. use PhpParser\NodeTraverser;
  27. use PhpParser\Parser\Php7;
  28. use PhpParser\PrettyPrinter\Standard;
  29. use SebastianBergmann\Diff\Differ;
  30. use Symfony\Component\Console\Attribute\AsCommand;
  31. use Symfony\Component\Console\Command\Command;
  32. use Symfony\Component\Console\Input\InputInterface;
  33. use Symfony\Component\Console\Input\InputOption;
  34. use Symfony\Component\Console\Output\OutputInterface;
  35. #[AsCommand(name: 'api:upgrade-resource')]
  36. final class UpgradeApiResourceCommand extends Command
  37. {
  38. /**
  39. * @deprecated To be removed along with Symfony < 6.1 compatibility
  40. */
  41. protected static $defaultName = 'api:upgrade-resource';
  42. private $resourceNameCollectionFactory;
  43. private $resourceMetadataFactory;
  44. private $subresourceOperationFactory;
  45. private $subresourceTransformer;
  46. private $reader;
  47. private $identifiersExtractor;
  48. public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, SubresourceOperationFactoryInterface $subresourceOperationFactory, SubresourceTransformer $subresourceTransformer, IdentifiersExtractorInterface $identifiersExtractor, AnnotationReader $reader = null)
  49. {
  50. $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
  51. $this->resourceMetadataFactory = $resourceMetadataFactory;
  52. $this->subresourceOperationFactory = $subresourceOperationFactory;
  53. $this->subresourceTransformer = $subresourceTransformer;
  54. $this->identifiersExtractor = $identifiersExtractor;
  55. $this->reader = $reader;
  56. parent::__construct();
  57. }
  58. /**
  59. * {@inheritdoc}
  60. */
  61. protected function configure()
  62. {
  63. $this
  64. ->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.
  65. Once you executed this script, make sure that the "metadata_backward_compatibility_layer" flag is set to "false" in the API Platform configuration.
  66. This will remove "ApiPlatform\Core\Annotation\ApiResource" annotation/attribute and use the "ApiPlatform\Metadata\ApiResource" attribute instead.')
  67. ->addOption('dry-run', '-d', InputOption::VALUE_OPTIONAL, 'Dry mode outputs a diff instead of writing files.', true)
  68. ->addOption('silent', '-s', InputOption::VALUE_NONE, 'Silent output.')
  69. ->addOption('force', '-f', InputOption::VALUE_NONE, 'Writes the files in place and skips PHP version check.');
  70. }
  71. /**
  72. * {@inheritdoc}
  73. */
  74. protected function execute(InputInterface $input, OutputInterface $output): int
  75. {
  76. if (!$input->getOption('force') && \PHP_VERSION_ID < 80100) {
  77. $output->write('<error>The new metadata system only works with PHP 8.1 and above.');
  78. return \defined(Command::class.'::INVALID') ? Command::INVALID : 2;
  79. }
  80. if (!class_exists(NodeTraverser::class)) {
  81. $output->writeln('Run `composer require --dev `nikic/php-parser` or install phpunit to use this command.');
  82. return \defined(Command::class.'::FAILURE') ? Command::FAILURE : 1;
  83. }
  84. $subresources = $this->getSubresources();
  85. $prettyPrinter = new Standard();
  86. foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
  87. try {
  88. $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
  89. } catch (ResourceClassNotFoundException $e) {
  90. continue;
  91. }
  92. $lexer = new Emulative([
  93. 'usedAttributes' => [
  94. 'comments',
  95. 'startLine',
  96. 'endLine',
  97. 'startTokenPos',
  98. 'endTokenPos',
  99. ],
  100. ]);
  101. $parser = new Php7($lexer);
  102. $fileName = (new \ReflectionClass($resourceClass))->getFilename();
  103. $traverser = new NodeTraverser();
  104. [$attribute, $isAnnotation] = $this->readApiResource($resourceClass);
  105. $traverser->addVisitor(new UpgradeApiFilterVisitor($this->reader, $resourceClass));
  106. $traverser->addVisitor(new UpgradeApiPropertyVisitor($this->reader, $resourceClass));
  107. if (!$attribute) {
  108. continue;
  109. }
  110. $traverser->addVisitor(new UpgradeApiResourceVisitor($attribute, $isAnnotation, $this->identifiersExtractor, $resourceClass));
  111. if (isset($subresources[$resourceClass])) {
  112. $referenceType = $resourceMetadata->getAttribute('url_generation_strategy');
  113. foreach ($subresources[$resourceClass] as $subresourceMetadata) {
  114. $traverser->addVisitor(new UpgradeApiSubresourceVisitor($subresourceMetadata, $referenceType));
  115. }
  116. }
  117. $oldCode = file_get_contents($fileName);
  118. $oldStmts = $parser->parse($oldCode);
  119. $oldTokens = $lexer->getTokens();
  120. $newStmts = $traverser->traverse($oldStmts);
  121. $newCode = $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
  122. if (!$input->getOption('force') && $input->getOption('dry-run')) {
  123. if ($input->getOption('silent')) {
  124. continue;
  125. }
  126. if (!class_exists(Differ::class)) {
  127. $output->writeln('Run `composer require --dev sebastian/diff` or install phpunit to print a diff.');
  128. return \defined(Command::class.'::FAILURE') ? Command::FAILURE : 1;
  129. }
  130. $this->printDiff($oldCode, $newCode, $output);
  131. continue;
  132. }
  133. file_put_contents($fileName, $newCode);
  134. }
  135. return \defined(Command::class.'::SUCCESS') ? Command::SUCCESS : 0;
  136. }
  137. /**
  138. * This computes a local cache with resource classes having subresources.
  139. * We first loop over all the classes and re-map the metadata on the correct Resource class.
  140. * Then we transform the ApiSubresource to an ApiResource class.
  141. */
  142. private function getSubresources(): array
  143. {
  144. $localCache = [];
  145. foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
  146. try {
  147. new \ReflectionClass($resourceClass);
  148. } catch (\Exception $e) {
  149. continue;
  150. }
  151. if (!isset($localCache[$resourceClass])) {
  152. $localCache[$resourceClass] = [];
  153. }
  154. foreach ($this->subresourceOperationFactory->create($resourceClass) as $subresourceMetadata) {
  155. if (!isset($localCache[$subresourceMetadata['resource_class']])) {
  156. $localCache[$subresourceMetadata['resource_class']] = [];
  157. }
  158. foreach ($localCache[$subresourceMetadata['resource_class']] as $currentSubresourceMetadata) {
  159. if ($currentSubresourceMetadata['path'] === $subresourceMetadata['path']) {
  160. continue 2;
  161. }
  162. }
  163. $localCache[$subresourceMetadata['resource_class']][] = $subresourceMetadata;
  164. }
  165. }
  166. // Compute URI variables
  167. foreach ($localCache as $class => $subresources) {
  168. if (!$subresources) {
  169. unset($localCache[$class]);
  170. continue;
  171. }
  172. foreach ($subresources as $i => $subresourceMetadata) {
  173. $localCache[$class][$i]['uri_variables'] = $this->subresourceTransformer->toUriVariables($subresourceMetadata);
  174. }
  175. }
  176. return $localCache;
  177. }
  178. private function printDiff(string $oldCode, string $newCode, OutputInterface $output): void
  179. {
  180. $consoleFormatter = new ColorConsoleDiffFormatter();
  181. $differ = new Differ();
  182. $diff = $differ->diff($oldCode, $newCode);
  183. $output->write($consoleFormatter->format($diff));
  184. }
  185. /**
  186. * @return array[ApiResource, bool]
  187. */
  188. private function readApiResource(string $resourceClass): array
  189. {
  190. $reflectionClass = new \ReflectionClass($resourceClass);
  191. if (\PHP_VERSION_ID >= 80000 && $attributes = $reflectionClass->getAttributes(ApiResource::class)) {
  192. return [$attributes[0]->newInstance(), false];
  193. }
  194. if (null === $this->reader) {
  195. throw new \RuntimeException(sprintf('Resource "%s" not found.', $resourceClass));
  196. }
  197. return [$this->reader->getClassAnnotation($reflectionClass, ApiResource::class), true];
  198. }
  199. }