diff --git a/src/Serializer/OperationContextTrait.php b/src/Serializer/OperationContextTrait.php index 515d2a0ded8..1ea99dd1b7e 100644 --- a/src/Serializer/OperationContextTrait.php +++ b/src/Serializer/OperationContextTrait.php @@ -39,7 +39,7 @@ protected function createOperationContext(array $context, ?string $resourceClass // At some point we should merge the jsonld context here, there's a TODO to simplify this somewhere else if ($propertyMetadata) { $context['output'] ??= []; - $context['output']['gen_id'] = $propertyMetadata->getGenId() ?? true; + $context['output']['gen_id'] = $propertyMetadata->getGenId() ?? ($context['gen_id'] ?? true); } if (!$resourceClass) { diff --git a/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/GenIdDefault.php b/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/GenIdDefault.php new file mode 100644 index 00000000000..4093f3fb44b --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/GenIdDefault.php @@ -0,0 +1,43 @@ + + * + * 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\Fixtures\TestBundle\ApiResource\GenIdFalse; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; + +#[Get(uriTemplate: '/gen_id_default', provider: [self::class, 'getData'], normalizationContext: ['hydra_prefix' => false])] +class GenIdDefault +{ + public function __construct( + public string $id, + #[ApiProperty] public Collection $subresources, + ) { + } + + public static function getData(Operation $operation, array $uriVariables = [], array $context = []): self + { + return new self( + '1', + new ArrayCollection( + [ + new Subresource('foo'), + new Subresource('bar'), + ] + ) + ); + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/GenIdTrue.php b/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/GenIdTrue.php new file mode 100644 index 00000000000..3b89a5a8b6c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/GenIdTrue.php @@ -0,0 +1,43 @@ + + * + * 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\Fixtures\TestBundle\ApiResource\GenIdFalse; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; + +#[Get(uriTemplate: '/gen_id_truthy', provider: [self::class, 'getData'], normalizationContext: ['hydra_prefix' => false])] +class GenIdTrue +{ + public function __construct( + public string $id, + #[ApiProperty(genId: true)] public Collection $subresources, + ) { + } + + public static function getData(Operation $operation, array $uriVariables = [], array $context = []): self + { + return new self( + '1', + new ArrayCollection( + [ + new Subresource('foo'), + new Subresource('bar'), + ] + ) + ); + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/Subresource.php b/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/Subresource.php new file mode 100644 index 00000000000..a7a3e50e513 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/GenIdFalse/Subresource.php @@ -0,0 +1,24 @@ + + * + * 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\Fixtures\TestBundle\ApiResource\GenIdFalse; + +use ApiPlatform\Metadata\ApiProperty; + +class Subresource +{ + public function __construct( + #[ApiProperty] public string $title, + ) { + } +} diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index 139b87aee8d..0f0a235c468 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -57,12 +57,15 @@ class AppKernel extends Kernel { use MicroKernelTrait; - public function __construct(string $environment, bool $debug) + private $genIdDefault; + + public function __construct(string $environment, bool $debug, ?bool $genIdDefault = null) { parent::__construct($environment, $debug); // patch for behat/symfony2-extension not supporting %env(APP_ENV)% $this->environment = $_SERVER['APP_ENV'] ?? $environment; + $this->genIdDefault = $genIdDefault ?? $_SERVER['GEN_ID_DEFAULT'] ?? null; } public function registerBundles(): array @@ -106,6 +109,11 @@ public function getProjectDir(): string return __DIR__; } + public function getCacheDir(): string + { + return parent::getCacheDir().(null !== $this->genIdDefault ? '_gen_id_'.$this->genIdDefault : ''); + } + protected function configureRoutes($routes): void { $routes->import(__DIR__."/config/routing_{$this->getEnvironment()}.yml"); @@ -114,6 +122,7 @@ protected function configureRoutes($routes): void protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void { $c->setParameter('kernel.project_dir', __DIR__); + $c->setParameter('app.gen_id_default', $this->genIdDefault); $loader->load(__DIR__."/config/config_{$this->getEnvironment()}.yml"); @@ -274,7 +283,12 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [ 'vary' => ['Accept', 'Cookie'], 'public' => true, ], - 'normalization_context' => ['skip_null_values' => false], + 'normalization_context' => null !== $this->genIdDefault ? [ + 'skip_null_values' => false, + 'gen_id' => (bool) $this->genIdDefault, + ] : [ + 'skip_null_values' => false, + ], 'operations' => [ Get::class, GetCollection::class, diff --git a/tests/Functional/GenIdGlobalOptionTest.php b/tests/Functional/GenIdGlobalOptionTest.php new file mode 100644 index 00000000000..729a1f9d309 --- /dev/null +++ b/tests/Functional/GenIdGlobalOptionTest.php @@ -0,0 +1,101 @@ + + * + * 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\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\GenIdFalse\AggregateRating; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\GenIdFalse\GenIdDefault; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\GenIdFalse\GenIdTrue; +use ApiPlatform\Tests\SetupClassResourcesTrait; + +final class GenIdGlobalOptionTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = true; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ + AggregateRating::class, + GenIdDefault::class, + GenIdTrue::class, + ]; + } + + protected function setUp(): void + { + parent::setUp(); + unset($_SERVER['GEN_ID_DEFAULT']); + } + + protected function tearDown(): void + { + unset($_SERVER['GEN_ID_DEFAULT']); + parent::tearDown(); + } + + /** + * When gen_id is globally false and no #[ApiProperty(genId: ...)] on the property, + * the nested object must not expose an @id. + */ + public function testGlobalGenIdFalseDisablesSkolemIdByDefaultOnProperties(): void + { + $_SERVER['GEN_ID_DEFAULT'] = 0; // simulate global defaults.normalization_context.gen_id: false + + $response = self::createClient()->request( + 'GET', + '/gen_id_default' + ); + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + + $this->assertArrayNotHasKey('@id', $data['subresources'][0]); + } + + /** + * #[ApiProperty(genId: true)] on the property must take precedence. + */ + public function testApiPropertyGenIdTrueTakesPrecedenceOverGlobalFalse(): void + { + $_SERVER['GEN_ID_DEFAULT'] = 0; // simulate global defaults.normalization_context.gen_id: false + + $response = self::createClient()->request( + 'GET', + '/gen_id_truthy' + ); + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + + $this->assertArrayHasKey('@id', $data['subresources'][0]); + } + + /** + * Without a global option and without an attribute, genId must be true by default. + */ + public function testWhenNoGlobalOptionAndNoAttributeGenIdIsTrueByDefault(): void + { + $response = self::createClient()->request( + 'GET', + '/gen_id_default' + ); + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + + $this->assertArrayHasKey('@id', $data['subresources'][0]); + } +}