diff --git a/src/State/Processor/ObjectMapperProcessor.php b/src/State/Processor/ObjectMapperProcessor.php index cfb13766f42..67511e67cea 100644 --- a/src/State/Processor/ObjectMapperProcessor.php +++ b/src/State/Processor/ObjectMapperProcessor.php @@ -38,6 +38,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = if ( !$this->objectMapper || !$operation->canWrite() + || null === $data || !is_a($data, $operation->getClass(), true) || !(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class) ) { diff --git a/src/State/Tests/Processor/ObjectMapperProcessorTest.php b/src/State/Tests/Processor/ObjectMapperProcessorTest.php new file mode 100644 index 00000000000..514a153cdfd --- /dev/null +++ b/src/State/Tests/Processor/ObjectMapperProcessorTest.php @@ -0,0 +1,107 @@ + + * + * 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\Tests\Processor; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use ApiPlatform\State\Processor\ObjectMapperProcessor; +use ApiPlatform\State\ProcessorInterface; +use PHPUnit\Framework\TestCase; +use Symfony\Component\ObjectMapper\Attribute\Map; +use Symfony\Component\ObjectMapper\ObjectMapperInterface; + +class ObjectMapperProcessorTest extends TestCase +{ + public function testProcessBypassesWhenNoObjectMapper(): void + { + $data = new DummyResourceWithoutMap(); + $operation = new Post(class: DummyResourceWithoutMap::class); + $decorated = $this->createMock(ProcessorInterface::class); + $decorated->expects($this->once()) + ->method('process') + ->with($data, $operation, [], []) + ->willReturn($data); + + $processor = new ObjectMapperProcessor(null, $decorated); + $this->assertEquals($data, $processor->process($data, $operation)); + } + + public function testProcessBypassesOnNonWriteOperation(): void + { + $data = new DummyResourceWithoutMap(); + $operation = new Get(class: DummyResourceWithoutMap::class); + $objectMapper = $this->createMock(ObjectMapperInterface::class); + $decorated = $this->createMock(ProcessorInterface::class); + $decorated->expects($this->once()) + ->method('process') + ->with($data, $operation, [], []) + ->willReturn($data); + + $processor = new ObjectMapperProcessor($objectMapper, $decorated); + $this->assertEquals($data, $processor->process($data, $operation)); + } + + public function testProcessBypassesWithNullData(): void + { + $operation = new Post(class: DummyResourceWithoutMap::class); + $objectMapper = $this->createMock(ObjectMapperInterface::class); + $decorated = $this->createMock(ProcessorInterface::class); + $decorated->expects($this->once()) + ->method('process') + ->with(null, $operation, [], []) + ->willReturn(null); + + $processor = new ObjectMapperProcessor($objectMapper, $decorated); + $this->assertNull($processor->process(null, $operation)); + } + + public function testProcessBypassesWithMismatchedDataType(): void + { + $data = new \stdClass(); + $operation = new Post(class: DummyResourceWithMap::class); + $objectMapper = $this->createMock(ObjectMapperInterface::class); + $decorated = $this->createMock(ProcessorInterface::class); + $decorated->expects($this->once()) + ->method('process') + ->with($data, $operation, [], []) + ->willReturn($data); + + $processor = new ObjectMapperProcessor($objectMapper, $decorated); + $this->assertEquals($data, $processor->process($data, $operation)); + } + + public function testProcessBypassesWithoutMapAttribute(): void + { + $data = new DummyResourceWithoutMap(); + $operation = new Post(class: DummyResourceWithoutMap::class); + $objectMapper = $this->createMock(ObjectMapperInterface::class); + $decorated = $this->createMock(ProcessorInterface::class); + $decorated->expects($this->once()) + ->method('process') + ->with($data, $operation, [], []) + ->willReturn($data); + + $processor = new ObjectMapperProcessor($objectMapper, $decorated); + $this->assertEquals($data, $processor->process($data, $operation)); + } +} + +class DummyResourceWithoutMap +{ +} + +#[Map] +class DummyResourceWithMap +{ +} diff --git a/src/State/composer.json b/src/State/composer.json index bb7f0c234de..020640c8e17 100644 --- a/src/State/composer.json +++ b/src/State/composer.json @@ -35,11 +35,12 @@ "symfony/translation-contracts": "^3.0" }, "require-dev": { - "api-platform/validator": "^4.1", "api-platform/serializer": "^4.1", - "symfony/type-info": "^7.3", + "api-platform/validator": "^4.1", "phpunit/phpunit": "11.5.x-dev", "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/object-mapper": "^7.3", + "symfony/type-info": "^7.3", "symfony/web-link": "^6.4 || ^7.1", "willdurand/negotiation": "^3.1" },