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

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;
  26. use PhpParser\NodeTraverser;
  27. use PhpParser\NodeVisitor\CloningVisitor;
  28. use PhpParser\Parser\Php7;
  29. use PhpParser\PrettyPrinter\Standard;
  30. use SebastianBergmann\Diff\Differ;
  31. use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
  32. use Symfony\Component\Console\Attribute\AsCommand;
  33. use Symfony\Component\Console\Command\Command;
  34. use Symfony\Component\Console\Input\InputArgument;
  35. use Symfony\Component\Console\Input\InputInterface;
  36. use Symfony\Component\Console\Input\InputOption;
  37. use Symfony\Component\Console\Output\OutputInterface;
  38. #[AsCommand(name: 'api:upgrade-resource')]
  39. final class UpgradeApiResourceCommand extends Command
  40. {
  41. /**
  42. * @deprecated To be removed along with Symfony < 6.1 compatibility
  43. */
  44. protected static $defaultName = 'api:upgrade-resource';
  45. private $resourceNameCollectionFactory;
  46. private $resourceMetadataFactory;
  47. private $subresourceOperationFactory;
  48. private $subresourceTransformer;
  49. private $reader;
  50. private $identifiersExtractor;
  51. public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, SubresourceOperationFactoryInterface $subresourceOperationFactory, SubresourceTransformer $subresourceTransformer, IdentifiersExtractorInterface $identifiersExtractor, AnnotationReader $reader = null)
  52. {
  53. $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
  54. $this->resourceMetadataFactory = $resourceMetadataFactory;
  55. $this->subresourceOperationFactory = $subresourceOperationFactory;
  56. $this->subresourceTransformer = $subresourceTransformer;
  57. $this->identifiersExtractor = $identifiersExtractor;
  58. $this->reader = $reader;
  59. parent::__construct();
  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. ->addArgument('class', InputArgument::OPTIONAL, 'A fully qualified class name.');
  71. }
  72. protected function execute(InputInterface $input, OutputInterface $output): int
  73. {
  74. if (!$input->getOption('force') && \PHP_VERSION_ID < 80100) {
  75. $output->write('<error>The new metadata system only works with PHP 8.1 and above.');
  76. return \defined(Command::class.'::INVALID') ? Command::INVALID : 2;
  77. }
  78. if (!class_exists(NodeTraverser::class)) {
  79. $output->writeln('Run `composer require --dev `nikic/php-parser` or install phpunit to use this command.');
  80. return \defined(Command::class.'::FAILURE') ? Command::FAILURE : 1;
  81. }
  82. $subresources = $this->getSubresources();
  83. $prettyPrinter = new Standard();
  84. foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
  85. if ($input->getArgument('class') && $input->getArgument('class') !== $resourceClass) {
  86. continue;
  87. }
  88. try {
  89. $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
  90. } catch (ResourceClassNotFoundException $e) {
  91. continue;
  92. }
  93. $lexer = new Lexer([
  94. 'usedAttributes' => [
  95. 'comments',
  96. 'startLine',
  97. 'endLine',
  98. 'startTokenPos',
  99. 'endTokenPos',
  100. ],
  101. ]);
  102. $parser = new Php7($lexer);
  103. $fileName = (new \ReflectionClass($resourceClass))->getFilename();
  104. $traverser = new NodeTraverser();
  105. [$attribute, $isAnnotation] = $this->readApiResource($resourceClass);
  106. $traverser->addVisitor(new UpgradeApiFilterVisitor($this->reader, $resourceClass));
  107. $traverser->addVisitor(new UpgradeApiPropertyVisitor($this->reader, $resourceClass));
  108. if (!$attribute) {
  109. continue;
  110. }
  111. $traverser->addVisitor(new UpgradeApiResourceVisitor($attribute, $isAnnotation, $this->identifiersExtractor, $resourceClass));
  112. if (isset($subresources[$resourceClass])) {
  113. $referenceType = $resourceMetadata->getAttribute('url_generation_strategy');
  114. foreach ($subresources[$resourceClass] as $subresourceMetadata) {
  115. $traverser->addVisitor(new UpgradeApiSubresourceVisitor($subresourceMetadata, $referenceType));
  116. }
  117. }
  118. $oldCode = file_get_contents($fileName);
  119. $oldStmts = $parser->parse($oldCode);
  120. $oldTokens = $lexer->getTokens();
  121. $cloningTraverser = new NodeTraverser();
  122. $cloningTraverser->addVisitor(new CloningVisitor()); // Required to preserve formatting
  123. $newStmts = $traverser->traverse($cloningTraverser->traverse($oldStmts));
  124. $newCode = $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
  125. if (!$input->getOption('force') && $input->getOption('dry-run')) {
  126. if ($input->getOption('silent')) {
  127. continue;
  128. }
  129. if (!class_exists(Differ::class)) {
  130. $output->writeln('Run `composer require --dev sebastian/diff` or install phpunit to print a diff.');
  131. return \defined(Command::class.'::FAILURE') ? Command::FAILURE : 1;
  132. }
  133. $this->printDiff($oldCode, $newCode, $output);
  134. continue;
  135. }
  136. file_put_contents($fileName, $newCode);
  137. }
  138. return \defined(Command::class.'::SUCCESS') ? Command::SUCCESS : 0;
  139. }
  140. /**
  141. * This computes a local cache with resource classes having subresources.
  142. * We first loop over all the classes and re-map the metadata on the correct Resource class.
  143. * Then we transform the ApiSubresource to an ApiResource class.
  144. */
  145. private function getSubresources(): array
  146. {
  147. $localCache = [];
  148. foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
  149. try {
  150. new \ReflectionClass($resourceClass);
  151. } catch (\Exception $e) {
  152. continue;
  153. }
  154. if (!isset($localCache[$resourceClass])) {
  155. $localCache[$resourceClass] = [];
  156. }
  157. foreach ($this->subresourceOperationFactory->create($resourceClass) as $subresourceMetadata) {
  158. if (!isset($localCache[$subresourceMetadata['resource_class']])) {
  159. $localCache[$subresourceMetadata['resource_class']] = [];
  160. }
  161. foreach ($localCache[$subresourceMetadata['resource_class']] as $currentSubresourceMetadata) {
  162. if ($currentSubresourceMetadata['path'] === $subresourceMetadata['path']) {
  163. continue 2;
  164. }
  165. }
  166. $localCache[$subresourceMetadata['resource_class']][] = $subresourceMetadata;
  167. }
  168. }
  169. // Compute URI variables
  170. foreach ($localCache as $class => $subresources) {
  171. if (!$subresources) {
  172. unset($localCache[$class]);
  173. continue;
  174. }
  175. foreach ($subresources as $i => $subresourceMetadata) {
  176. $localCache[$class][$i]['uri_variables'] = $this->subresourceTransformer->toUriVariables($subresourceMetadata);
  177. }
  178. }
  179. return $localCache;
  180. }
  181. private function printDiff(string $oldCode, string $newCode, OutputInterface $output): void
  182. {
  183. $consoleFormatter = new ColorConsoleDiffFormatter();
  184. $differ = class_exists(UnifiedDiffOutputBuilder::class) ? new Differ(new UnifiedDiffOutputBuilder()) : new Differ();
  185. $diff = $differ->diff($oldCode, $newCode);
  186. $output->write($consoleFormatter->format($diff));
  187. }
  188. /**
  189. * @return array[ApiResource, bool]
  190. */
  191. private function readApiResource(string $resourceClass): array
  192. {
  193. $reflectionClass = new \ReflectionClass($resourceClass);
  194. if (\PHP_VERSION_ID >= 80000 && $attributes = $reflectionClass->getAttributes(ApiResource::class)) {
  195. return [$attributes[0]->newInstance(), false];
  196. }
  197. if (null === $this->reader) {
  198. throw new \RuntimeException(sprintf('Resource "%s" not found.', $resourceClass));
  199. }
  200. return [$this->reader->getClassAnnotation($reflectionClass, ApiResource::class), true];
  201. }
  202. }