Skip to content

Commit 334914a

Browse files
authored
feat(debug): add a debug resource command (api-platform#4401)
1 parent 031e737 commit 334914a

File tree

4 files changed

+246
-0
lines changed

4 files changed

+246
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Bridge\Symfony\Bundle\Command;
15+
16+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
17+
use Symfony\Component\Console\Command\Command;
18+
use Symfony\Component\Console\Input\InputArgument;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Question\ChoiceQuestion;
22+
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
23+
24+
final class DebugResourceCommand extends Command
25+
{
26+
protected static $defaultName = 'debug:api-resource';
27+
28+
private $resourceMetadataCollectionFactory;
29+
private $cloner;
30+
private $dumper;
31+
32+
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ClonerInterface $cloner, $dumper)
33+
{
34+
parent::__construct();
35+
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
36+
$this->cloner = $cloner;
37+
$this->dumper = $dumper;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
protected function configure(): void
44+
{
45+
$this
46+
->setDescription('Debug API Platform resources')
47+
->addArgument('class', InputArgument::REQUIRED, 'The class you want to debug');
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
protected function execute(InputInterface $input, OutputInterface $output)
54+
{
55+
$resourceClass = $input->getArgument('class');
56+
57+
$resourceCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
58+
59+
if (0 === \count($resourceCollection)) {
60+
$output->writeln(sprintf('<error>No resources found for class %s</error>', $resourceClass));
61+
62+
return Command::INVALID;
63+
}
64+
65+
$shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
66+
67+
$helper = $this->getHelper('question');
68+
69+
$resources = [];
70+
foreach ($resourceCollection as $resource) {
71+
if ($resource->getUriTemplate()) {
72+
$resources[] = $resource->getUriTemplate();
73+
continue;
74+
}
75+
76+
foreach ($resource->getOperations() as $operation) {
77+
if ($operation->getUriTemplate()) {
78+
$resources[] = $operation->getUriTemplate();
79+
break;
80+
}
81+
}
82+
}
83+
84+
if (\count($resourceCollection) > 1) {
85+
$questionResource = new ChoiceQuestion(
86+
sprintf('There are %d resources declared on the class %s, which one do you want to debug ? ', \count($resourceCollection), $shortName).\PHP_EOL,
87+
$resources
88+
);
89+
90+
$answerResource = $helper->ask($input, $output, $questionResource);
91+
$resourceIndex = array_search($answerResource, $resources, true);
92+
$selectedResource = $resourceCollection[$resourceIndex];
93+
} else {
94+
$selectedResource = $resourceCollection[0];
95+
$output->writeln(sprintf('Class %s declares 1 resource.', $shortName).\PHP_EOL);
96+
}
97+
98+
$operations = ['Debug the resource itself'];
99+
foreach ($selectedResource->getOperations() as $operationName => $operation) {
100+
$operations[] = $operationName;
101+
}
102+
103+
$questionOperation = new ChoiceQuestion(
104+
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,
105+
$operations
106+
);
107+
108+
$answerOperation = $helper->ask($input, $output, $questionOperation);
109+
if ('Debug the resource itself' === $answerOperation) {
110+
$this->dumper->dump($this->cloner->cloneVar($selectedResource));
111+
$output->writeln('Successfully dumped the selected resource');
112+
113+
return Command::SUCCESS;
114+
}
115+
116+
$this->dumper->dump($this->cloner->cloneVar($resourceCollection->getOperation($answerOperation)));
117+
$output->writeln('Successfully dumped the selected operation');
118+
119+
return Command::SUCCESS;
120+
}
121+
}

src/Core/Bridge/Symfony/Bundle/Resources/config/debug.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,16 @@
2020
<service id="debug.api_platform.data_persister" class="ApiPlatform\Core\Bridge\Symfony\Bundle\DataPersister\TraceableChainDataPersister" decorates="api_platform.data_persister">
2121
<argument type="service" id="debug.api_platform.data_persister.inner" />
2222
</service>
23+
24+
<service id="debug.var_dumper.cloner" class="Symfony\Component\VarDumper\Cloner\VarCloner" />
25+
26+
<service id="debug.var_dumper.cli_dumper" class="Symfony\Component\VarDumper\Dumper\CliDumper" />
27+
28+
<service id="debug.api_platform.debug_resource.command" class="ApiPlatform\Bridge\Symfony\Bundle\Command\DebugResourceCommand">
29+
<tag name="console.command" />
30+
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
31+
<argument type="service" id="debug.var_dumper.cloner" />
32+
<argument type="service" id="debug.var_dumper.cli_dumper" />
33+
</service>
2334
</services>
2435
</container>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Bridge\Symfony\Bundle\Command;
15+
16+
use ApiPlatform\Bridge\Symfony\Bundle\Command\DebugResourceCommand;
17+
use ApiPlatform\Core\Tests\ProphecyTrait;
18+
use ApiPlatform\Exception\ResourceClassNotFoundException;
19+
use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory;
20+
use PHPUnit\Framework\TestCase;
21+
use Prophecy\Argument;
22+
use Symfony\Component\Console\Application;
23+
use Symfony\Component\Console\Tester\CommandTester;
24+
use Symfony\Component\VarDumper\Cloner\VarCloner;
25+
use Symfony\Component\VarDumper\Dumper\CliDumper;
26+
use Symfony\Component\VarDumper\Dumper\DataDumperInterface;
27+
28+
class DebugResourceCommandTest extends TestCase
29+
{
30+
use ProphecyTrait;
31+
32+
private function getCommandTester(DataDumperInterface $dumper = null): CommandTester
33+
{
34+
$application = new Application();
35+
$application->setCatchExceptions(false);
36+
$application->setAutoExit(false);
37+
38+
$application->add(new DebugResourceCommand(new AttributesResourceMetadataCollectionFactory(), new VarCloner(), $dumper ?? new CliDumper()));
39+
40+
$command = $application->find('debug:api-resource');
41+
42+
return new CommandTester($command);
43+
}
44+
45+
/**
46+
* @requires PHP 8.0
47+
*/
48+
public function testDebugResource()
49+
{
50+
$varDumper = $this->prophesize(DataDumperInterface::class);
51+
$commandTester = $this->getCommandTester($varDumper->reveal());
52+
$varDumper->dump(Argument::any())->shouldBeCalledTimes(1);
53+
$commandTester->setInputs(['0', '0']);
54+
$commandTester->execute([
55+
'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource',
56+
]);
57+
58+
$this->assertStringContainsString('Successfully dumped the selected resource', $commandTester->getDisplay());
59+
}
60+
61+
/**
62+
* @requires PHP 8.0
63+
*/
64+
public function testDebugOperation()
65+
{
66+
$varDumper = $this->prophesize(DataDumperInterface::class);
67+
$commandTester = $this->getCommandTester($varDumper->reveal());
68+
$varDumper->dump(Argument::any())->shouldBeCalledTimes(1);
69+
$commandTester->setInputs(['0', '1']);
70+
71+
$commandTester->execute([
72+
'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource',
73+
]);
74+
75+
$this->assertStringContainsString('Successfully dumped the selected operation', $commandTester->getDisplay());
76+
}
77+
78+
/**
79+
* @requires PHP 8.0
80+
*/
81+
public function testWithOnlyOneResource()
82+
{
83+
$varDumper = $this->prophesize(DataDumperInterface::class);
84+
$commandTester = $this->getCommandTester($varDumper->reveal());
85+
$varDumper->dump(Argument::any())->shouldBeCalledTimes(1);
86+
$commandTester->setInputs(['1']);
87+
88+
$commandTester->execute([
89+
'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\AlternateResource',
90+
]);
91+
92+
$this->assertStringContainsString('declares 1 resource', $commandTester->getDisplay());
93+
$this->assertStringContainsString('Successfully dumped the selected operation', $commandTester->getDisplay());
94+
}
95+
96+
/**
97+
* @requires PHP 8.0
98+
*/
99+
public function testExecuteWithNotExistingClass()
100+
{
101+
$this->expectException(ResourceClassNotFoundException::class);
102+
$commandTester = $this->getCommandTester();
103+
104+
$commandTester->execute([
105+
'class' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\NotExisting',
106+
]);
107+
}
108+
}

tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ public function testEnableProfilerWithDebug()
291291
$containerBuilderProphecy->setDefinition('debug.api_platform.item_data_provider', Argument::type(Definition::class))->shouldBeCalled();
292292
$containerBuilderProphecy->setDefinition('debug.api_platform.subresource_data_provider', Argument::type(Definition::class))->shouldBeCalled();
293293
$containerBuilderProphecy->setDefinition('debug.api_platform.data_persister', Argument::type(Definition::class))->shouldBeCalled();
294+
$containerBuilderProphecy->setDefinition('debug.api_platform.debug_resource.command', Argument::type(Definition::class))->shouldBeCalled();
295+
$containerBuilderProphecy->setDefinition('debug.var_dumper.cloner', Argument::type(Definition::class))->shouldBeCalled();
296+
$containerBuilderProphecy->setDefinition('debug.var_dumper.cli_dumper', Argument::type(Definition::class))->shouldBeCalled();
294297
$containerBuilder = $containerBuilderProphecy->reveal();
295298

296299
$config = self::DEFAULT_CONFIG;
@@ -797,6 +800,9 @@ public function testKeepCachePoolClearerCacheWarmerWithDebug()
797800
$containerBuilderProphecy->setDefinition('debug.api_platform.item_data_provider', Argument::type(Definition::class))->will(function () {});
798801
$containerBuilderProphecy->setDefinition('debug.api_platform.subresource_data_provider', Argument::type(Definition::class))->will(function () {});
799802
$containerBuilderProphecy->setDefinition('debug.api_platform.data_persister', Argument::type(Definition::class))->will(function () {});
803+
$containerBuilderProphecy->setDefinition('debug.api_platform.debug_resource.command', Argument::type(Definition::class))->will(function () {});
804+
$containerBuilderProphecy->setDefinition('debug.var_dumper.cloner', Argument::type(Definition::class))->shouldBeCalled();
805+
$containerBuilderProphecy->setDefinition('debug.var_dumper.cli_dumper', Argument::type(Definition::class))->shouldBeCalled();
800806

801807
$containerBuilder = $containerBuilderProphecy->reveal();
802808

0 commit comments

Comments
 (0)