From b63200c8567618e562cf88be47745bc1071a0d6a Mon Sep 17 00:00:00 2001 From: SamuelBigard Date: Thu, 26 Aug 2021 11:35:22 +0200 Subject: [PATCH] feat(debug): add a debug resource command --- .../Bundle/Command/DebugResourceCommand.php | 121 ++++++++++++++++++ .../Symfony/Bundle/Resources/config/debug.xml | 11 ++ .../Command/DebugResourceCommandTest.php | 108 ++++++++++++++++ .../ApiPlatformExtensionTest.php | 6 + 4 files changed, 246 insertions(+) create mode 100644 src/Bridge/Symfony/Bundle/Command/DebugResourceCommand.php create mode 100644 tests/Bridge/Symfony/Bundle/Command/DebugResourceCommandTest.php diff --git a/src/Bridge/Symfony/Bundle/Command/DebugResourceCommand.php b/src/Bridge/Symfony/Bundle/Command/DebugResourceCommand.php new file mode 100644 index 00000000000..e178a9f39a2 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Command/DebugResourceCommand.php @@ -0,0 +1,121 @@ + + * + * 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\Bridge\Symfony\Bundle\Command; + +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\VarDumper\Cloner\ClonerInterface; + +final class DebugResourceCommand extends Command +{ + protected static $defaultName = 'debug:api-resource'; + + private $resourceMetadataCollectionFactory; + private $cloner; + private $dumper; + + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ClonerInterface $cloner, $dumper) + { + parent::__construct(); + $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; + $this->cloner = $cloner; + $this->dumper = $dumper; + } + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this + ->setDescription('Debug API Platform resources') + ->addArgument('class', InputArgument::REQUIRED, 'The class you want to debug'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $resourceClass = $input->getArgument('class'); + + $resourceCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + + if (0 === \count($resourceCollection)) { + $output->writeln(sprintf('No resources found for class %s', $resourceClass)); + + return Command::INVALID; + } + + $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass; + + $helper = $this->getHelper('question'); + + $resources = []; + foreach ($resourceCollection as $resource) { + if ($resource->getUriTemplate()) { + $resources[] = $resource->getUriTemplate(); + continue; + } + + foreach ($resource->getOperations() as $operation) { + if ($operation->getUriTemplate()) { + $resources[] = $operation->getUriTemplate(); + break; + } + } + } + + if (\count($resourceCollection) > 1) { + $questionResource = new ChoiceQuestion( + sprintf('There are %d resources declared on the class %s, which one do you want to debug ? ', \count($resourceCollection), $shortName).\PHP_EOL, + $resources + ); + + $answerResource = $helper->ask($input, $output, $questionResource); + $resourceIndex = array_search($answerResource, $resources, true); + $selectedResource = $resourceCollection[$resourceIndex]; + } else { + $selectedResource = $resourceCollection[0]; + $output->writeln(sprintf('Class %s declares 1 resource.', $shortName).\PHP_EOL); + } + + $operations = ['Debug the resource itself']; + foreach ($selectedResource->getOperations() as $operationName => $operation) { + $operations[] = $operationName; + } + + $questionOperation = new ChoiceQuestion( + sprintf('There are %d operation%s declared on the resource, which one do you want to debug ? ', $selectedResource->getOperations()->count(), $selectedResource->getOperations()->count() > 1 ? 's' : '').\PHP_EOL, + $operations + ); + + $answerOperation = $helper->ask($input, $output, $questionOperation); + if ('Debug the resource itself' === $answerOperation) { + $this->dumper->dump($this->cloner->cloneVar($selectedResource)); + $output->writeln('Successfully dumped the selected resource'); + + return Command::SUCCESS; + } + + $this->dumper->dump($this->cloner->cloneVar($resourceCollection->getOperation($answerOperation))); + $output->writeln('Successfully dumped the selected operation'); + + return Command::SUCCESS; + } +} diff --git a/src/Core/Bridge/Symfony/Bundle/Resources/config/debug.xml b/src/Core/Bridge/Symfony/Bundle/Resources/config/debug.xml index b1d0571a37f..9686cc415c8 100644 --- a/src/Core/Bridge/Symfony/Bundle/Resources/config/debug.xml +++ b/src/Core/Bridge/Symfony/Bundle/Resources/config/debug.xml @@ -20,5 +20,16 @@ + + + + + + + + + + + diff --git a/tests/Bridge/Symfony/Bundle/Command/DebugResourceCommandTest.php b/tests/Bridge/Symfony/Bundle/Command/DebugResourceCommandTest.php new file mode 100644 index 00000000000..4ac34b95a02 --- /dev/null +++ b/tests/Bridge/Symfony/Bundle/Command/DebugResourceCommandTest.php @@ -0,0 +1,108 @@ + + * + * 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 ApiPlatform\Bridge\Symfony\Bundle\Command\DebugResourceCommand; +use ApiPlatform\Core\Tests\ProphecyTrait; +use ApiPlatform\Exception\ResourceClassNotFoundException; +use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Dumper\DataDumperInterface; + +class DebugResourceCommandTest extends TestCase +{ + use ProphecyTrait; + + private function getCommandTester(DataDumperInterface $dumper = null): CommandTester + { + $application = new Application(); + $application->setCatchExceptions(false); + $application->setAutoExit(false); + + $application->add(new DebugResourceCommand(new AttributesResourceMetadataCollectionFactory(), new VarCloner(), $dumper ?? new CliDumper())); + + $command = $application->find('debug:api-resource'); + + return new CommandTester($command); + } + + /** + * @requires PHP 8.0 + */ + public function testDebugResource() + { + $varDumper = $this->prophesize(DataDumperInterface::class); + $commandTester = $this->getCommandTester($varDumper->reveal()); + $varDumper->dump(Argument::any())->shouldBeCalledTimes(1); + $commandTester->setInputs(['0', '0']); + $commandTester->execute([ + 'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource', + ]); + + $this->assertStringContainsString('Successfully dumped the selected resource', $commandTester->getDisplay()); + } + + /** + * @requires PHP 8.0 + */ + public function testDebugOperation() + { + $varDumper = $this->prophesize(DataDumperInterface::class); + $commandTester = $this->getCommandTester($varDumper->reveal()); + $varDumper->dump(Argument::any())->shouldBeCalledTimes(1); + $commandTester->setInputs(['0', '1']); + + $commandTester->execute([ + 'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource', + ]); + + $this->assertStringContainsString('Successfully dumped the selected operation', $commandTester->getDisplay()); + } + + /** + * @requires PHP 8.0 + */ + public function testWithOnlyOneResource() + { + $varDumper = $this->prophesize(DataDumperInterface::class); + $commandTester = $this->getCommandTester($varDumper->reveal()); + $varDumper->dump(Argument::any())->shouldBeCalledTimes(1); + $commandTester->setInputs(['1']); + + $commandTester->execute([ + 'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\AlternateResource', + ]); + + $this->assertStringContainsString('declares 1 resource', $commandTester->getDisplay()); + $this->assertStringContainsString('Successfully dumped the selected operation', $commandTester->getDisplay()); + } + + /** + * @requires PHP 8.0 + */ + public function testExecuteWithNotExistingClass() + { + $this->expectException(ResourceClassNotFoundException::class); + $commandTester = $this->getCommandTester(); + + $commandTester->execute([ + 'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\NotExisting', + ]); + } +} diff --git a/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index c9223d6668b..81f1a307e36 100644 --- a/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -291,6 +291,9 @@ public function testEnableProfilerWithDebug() $containerBuilderProphecy->setDefinition('debug.api_platform.item_data_provider', Argument::type(Definition::class))->shouldBeCalled(); $containerBuilderProphecy->setDefinition('debug.api_platform.subresource_data_provider', Argument::type(Definition::class))->shouldBeCalled(); $containerBuilderProphecy->setDefinition('debug.api_platform.data_persister', Argument::type(Definition::class))->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('debug.api_platform.debug_resource.command', Argument::type(Definition::class))->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('debug.var_dumper.cloner', Argument::type(Definition::class))->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('debug.var_dumper.cli_dumper', Argument::type(Definition::class))->shouldBeCalled(); $containerBuilder = $containerBuilderProphecy->reveal(); $config = self::DEFAULT_CONFIG; @@ -794,6 +797,9 @@ public function testKeepCachePoolClearerCacheWarmerWithDebug() $containerBuilderProphecy->setDefinition('debug.api_platform.item_data_provider', Argument::type(Definition::class))->will(function () {}); $containerBuilderProphecy->setDefinition('debug.api_platform.subresource_data_provider', Argument::type(Definition::class))->will(function () {}); $containerBuilderProphecy->setDefinition('debug.api_platform.data_persister', Argument::type(Definition::class))->will(function () {}); + $containerBuilderProphecy->setDefinition('debug.api_platform.debug_resource.command', Argument::type(Definition::class))->will(function () {}); + $containerBuilderProphecy->setDefinition('debug.var_dumper.cloner', Argument::type(Definition::class))->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('debug.var_dumper.cli_dumper', Argument::type(Definition::class))->shouldBeCalled(); $containerBuilder = $containerBuilderProphecy->reveal();