Repository: api-platform/core (php)

Issue:
Custom dto output return tons of @context 
**API Platform version(s) affected**: 4.0.7 +

**Description**  
With custom resource and use parameter "output" for custom dto return in response full "@ context" . In version 4.0.6 and below not.
If not use "output" everything works fine.

**How to reproduce**  
Just make custom output DTO.

**Possible Solution**  
Is this bug ? I couldn't find any mention of this in the changes. 

**Additional Context**  
custom api resource
<img width="397" alt="image" src="https://github.com/user-attachments/assets/428f579c-9e6c-42cf-9df8-94733305e960">

in response tons of "@ context" (around 1k lines)
<img width="624" alt="image" src="https://github.com/user-attachments/assets/85eadf54-357a-4eb9-8f90-20f8795f3218">



Pull request:
fix(jsonld): anonymous context hydra_prefix value
fixes #6810

Patch:
diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php
index a3d638f5a..4dc9dda40 100644
--- a/src/JsonLd/ContextBuilder.php
+++ b/src/JsonLd/ContextBuilder.php
@@ -115,14 +115,24 @@ final class ContextBuilder implements AnonymousContextBuilderInterface
     public function getAnonymousResourceContext(object $object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array
     {
         $outputClass = $this->getObjectClass($object);
-        $operation = $context['operation'] ?? new Get(shortName: (new \ReflectionClass($outputClass))->getShortName());
+        $operation = $context['operation'] ?? new Get(
+            shortName: (new \ReflectionClass($outputClass))->getShortName(),
+            normalizationContext: [
+                self::HYDRA_CONTEXT_HAS_PREFIX => $context[self::HYDRA_CONTEXT_HAS_PREFIX] ?? $this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true,
+                'groups' => [],
+            ],
+            denormalizationContext: [
+                'groups' => [],
+            ]
+        );
         $shortName = $operation->getShortName();
 
         $jsonLdContext = [
             '@context' => $this->getResourceContextWithShortname(
                 $outputClass,
                 $referenceType,
-                $shortName
+                $shortName,
+                $operation
             ),
             '@type' => $shortName,
         ];
@@ -184,10 +194,6 @@ final class ContextBuilder implements AnonymousContextBuilderInterface
             }
         }
 
-        if (false === ($this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true)) {
-            return [HYDRA_CONTEXT, $context];
-        }
-
         return $context;
     }
 }
diff --git a/src/JsonLd/Serializer/JsonLdContextTrait.php b/src/JsonLd/Serializer/JsonLdContextTrait.php
index 2e0cef8a4..7b2d98b2c 100644
--- a/src/JsonLd/Serializer/JsonLdContextTrait.php
+++ b/src/JsonLd/Serializer/JsonLdContextTrait.php
@@ -14,6 +14,7 @@ declare(strict_types=1);
 namespace ApiPlatform\JsonLd\Serializer;
 
 use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
+use ApiPlatform\JsonLd\ContextBuilder;
 use ApiPlatform\JsonLd\ContextBuilderInterface;
 
 /**
@@ -49,13 +50,19 @@ trait JsonLdContextTrait
 
     private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, $object, array &$context): array
     {
+        $anonymousContext = ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null];
+
+        if (isset($context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX])) {
+            $anonymousContext[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX] = $context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX];
+        }
+
         // We're in a collection, don't add the @context part
         if (isset($context['jsonld_has_context'])) {
-            return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null, 'has_context' => true]);
+            return $contextBuilder->getAnonymousResourceContext($object, ['has_context' => true] + $anonymousContext);
         }
 
         $context['jsonld_has_context'] = true;
 
-        return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]);
+        return $contextBuilder->getAnonymousResourceContext($object, $anonymousContext);
     }
 }
diff --git a/src/Laravel/Eloquent/State/LinksHandler.php b/src/Laravel/Eloquent/State/LinksHandler.php
index efcefc41d..a3930fd5f 100644
--- a/src/Laravel/Eloquent/State/LinksHandler.php
+++ b/src/Laravel/Eloquent/State/LinksHandler.php
@@ -109,9 +109,10 @@ final class LinksHandler implements LinksHandlerInterface
                 $relation_query = $relation->{$from}();
 
                 return $builder->getModel()->join(
-                    $relation_query->getTable(), $relation->{$from}()->getQualifiedForeignPivotKeyName(), $builder->getModel()->getQualifiedKeyName())
+                    $relation_query->getTable(), $relation->{$from}()->getQualifiedRelatedPivotKeyName(), $builder->getModel()->getQualifiedKeyName())
                     ->where($relation->{$from}()->getQualifiedForeignPivotKeyName(),
-                        $identifier);
+                        $identifier)
+                    ->select($builder->getModel()->getTable().'.*');
             }
 
             return $builder->getModel()->where($relation->{$from}()->getQualifiedForeignKeyName(), $identifier);


Test patch:
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6810/JsonLdContextOutput.php b/tests/Fixtures/TestBundle/ApiResource/Issue6810/JsonLdContextOutput.php
new file mode 100644
index 000000000..fe1ee0d55
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6810/JsonLdContextOutput.php
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * 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\Issue6810;
+
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Operation;
+
+#[Get('/json_ld_context_output', provider: [self::class, 'getData'], output: Output::class, normalizationContext: ['hydra_prefix' => false])]
+class JsonLdContextOutput
+{
+    public function __construct(public string $id)
+    {
+    }
+
+    public static function getData(Operation $operation, array $uriVariables = [], array $context = []): Output
+    {
+        return new Output();
+    }
+}
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6810/Output.php b/tests/Fixtures/TestBundle/ApiResource/Issue6810/Output.php
new file mode 100644
index 000000000..0a3a8d89a
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6810/Output.php
@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * 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\Issue6810;
+
+class Output
+{
+    public string $foo = 'bar';
+}
diff --git a/tests/Functional/JsonLdTest.php b/tests/Functional/JsonLdTest.php
index 4cb0b9780..3be83e5c0 100644
--- a/tests/Functional/JsonLdTest.php
+++ b/tests/Functional/JsonLdTest.php
@@ -14,6 +14,7 @@ declare(strict_types=1);
 namespace ApiPlatform\Tests\Functional;
 
 use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6810\JsonLdContextOutput;
 use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Bar;
 use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Foo;
 use ApiPlatform\Tests\SetupClassResourcesTrait;
@@ -29,7 +30,7 @@ class JsonLdTest extends ApiTestCase
      */
     public static function getResources(): array
     {
-        return [Foo::class, Bar::class];
+        return [Foo::class, Bar::class, JsonLdContextOutput::class];
     }
 
     /**
@@ -50,6 +51,20 @@ class JsonLdTest extends ApiTestCase
         $this->assertEquals('Bar two', $res['title']);
     }
 
+    public function testContextWithOutput(): void
+    {
+        $response = self::createClient()->request(
+            'GET',
+            '/json_ld_context_output',
+        );
+        $res = $response->toArray();
+        $this->assertEquals($res['@context'], [
+            '@vocab' => 'http://localhost/docs.jsonld#',
+            'hydra' => 'http://www.w3.org/ns/hydra/core#',
+            'foo' => 'Output/foo',
+        ]);
+    }
+
     protected function setUp(): void
     {
         self::bootKernel();
@@ -66,8 +81,12 @@ class JsonLdTest extends ApiTestCase
             $classes[] = $manager->getClassMetadata($entityClass);
         }
 
-        $schemaTool = new SchemaTool($manager);
-        @$schemaTool->createSchema($classes);
+        try {
+            $schemaTool = new SchemaTool($manager);
+            @$schemaTool->createSchema($classes);
+        } catch (\Exception $e) {
+            return;
+        }
 
         $foo = new Foo();
         $foo->title = 'Foo';
diff --git a/tests/JsonLd/ContextBuilderTest.php b/tests/JsonLd/ContextBuilderTest.php
index e5ba3a593..7a64f7d3b 100644
--- a/tests/JsonLd/ContextBuilderTest.php
+++ b/tests/JsonLd/ContextBuilderTest.php
@@ -35,8 +35,6 @@ use Prophecy\PhpUnit\ProphecyTrait;
 use Prophecy\Prophecy\ObjectProphecy;
 use Symfony\Component\PropertyInfo\Type;
 
-use const ApiPlatform\JsonLd\HYDRA_CONTEXT;
-
 /**
  * @author Markus Mächler <markus.maechler@bithost.ch>
  */
@@ -293,12 +291,9 @@ class ContextBuilderTest extends TestCase
         $contextBuilder = new ContextBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->urlGeneratorProphecy->reveal(), null, null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false]);
 
         $expected = [
-            HYDRA_CONTEXT,
-            [
-                '@vocab' => '#',
-                'hydra' => 'http://www.w3.org/ns/hydra/core#',
-                'dummyPropertyA' => 'DummyEntity/dummyPropertyA',
-            ],
+            '@vocab' => '#',
+            'hydra' => 'http://www.w3.org/ns/hydra/core#',
+            'dummyPropertyA' => 'DummyEntity/dummyPropertyA',
         ];
 
         $this->assertEquals($expected, $contextBuilder->getResourceContext($this->entityClass));