diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2abe509c757..f56716bd336 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -753,3 +753,90 @@ jobs: run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction + + phpunit-symfony-lowest: + name: PHPUnit (PHP ${{ matrix.php }}) (Symfony lowest) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + php: + - '8.1' + - '8.2' + fail-fast: false + # env: + # See https://github.com/doctrine/DoctrineMongoDBBundle/pull/673 + #SYMFONY_DEPRECATIONS_HELPER: max[direct]=0 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: pecl, composer + extensions: intl, bcmath, curl, openssl, mbstring + coverage: none + ini-values: memory_limit=-1 + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Remove cache + run: rm -Rf tests/Fixtures/app/var/cache/* + - name: Update project dependencies + run: composer update --prefer-lowest --no-interaction --no-progress --ansi + - name: Install PHPUnit + run: vendor/bin/simple-phpunit --version + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/simple-phpunit + + behat-symfony-lowest: + name: Behat (PHP ${{ matrix.php }}) (Symfony lowest) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + php: + - '8.1' + - '8.2' + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: pecl, composer + extensions: intl, bcmath, curl, openssl, mbstring + coverage: none + ini-values: memory_limit=-1 + - name: Install additional packages + run: sudo apt-get install moreutils + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Remove cache + run: rm -Rf tests/Fixtures/app/var/cache/* + - name: Update project dependencies + run: composer update --prefer-lowest --no-interaction --no-progress --ansi + - name: Install PHPUnit + run: vendor/bin/simple-phpunit --version + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi + - name: Run Behat tests + run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction diff --git a/composer.json b/composer.json index 621ef2022e2..2d2f8455fe3 100644 --- a/composer.json +++ b/composer.json @@ -24,13 +24,13 @@ "symfony/property-info": "^6.1", "symfony/serializer": "^6.1", "symfony/web-link": "^6.1", - "willdurand/negotiation": "^2.0.3 || ^3.0" + "willdurand/negotiation": "^3.0" }, "require-dev": { "behat/behat": "^3.1", "behat/mink": "^1.9@dev", "doctrine/cache": "^1.11 || ^2.1", - "doctrine/common": "^2.11 || ^3.0", + "doctrine/common": "^2.11 || ^3.1", "doctrine/data-fixtures": "^1.2.2", "doctrine/dbal": "^2.6 || ^3.0", "doctrine/doctrine-bundle": "^1.12 || ^2.0", @@ -94,6 +94,9 @@ "doctrine/dbal": "<2.10", "doctrine/mongodb-odm": "<2.2", "doctrine/persistence": "<1.3", + "symfony/service-contracts": "<3", + "phpunit/phpunit": "<9.5", + "phpspec/prophecy": "<1.15", "elasticsearch/elasticsearch": ">=8.0" }, "suggest": { diff --git a/features/graphql/query.feature b/features/graphql/query.feature index 4463a717b87..ce4c7c2b0e5 100644 --- a/features/graphql/query.feature +++ b/features/graphql/query.feature @@ -68,6 +68,33 @@ Feature: GraphQL query support And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[0].node.name" should be equal to "RelatedOneToManyDummy12" And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[2].node.name" should be equal to "RelatedOneToManyDummy32" + @createSchema @!mongodb + Scenario: Retrieve an item with child relation to the same resource + Given there are tree dummies + When I send the following GraphQL request: + """ + { + treeDummies { + edges { + node { + id + children { + totalCount + } + } + } + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/json" + And the JSON node "errors" should not exist + And the JSON node "data.treeDummies.edges[0].node.id" should be equal to "/tree_dummies/1" + And the JSON node "data.treeDummies.edges[0].node.children.totalCount" should be equal to "1" + And the JSON node "data.treeDummies.edges[1].node.id" should be equal to "/tree_dummies/2" + And the JSON node "data.treeDummies.edges[1].node.children.totalCount" should be equal to "0" + @createSchema Scenario: Retrieve a Relay Node Given there are 2 dummy objects with relatedDummy diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 26a9c7d42c3..c6bc5bc14a8 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -262,7 +262,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field } $resourceOperation = $rootOperation; - if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { + if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) { $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); } diff --git a/src/Symfony/Bundle/ArgumentResolver/CompatibleValueResolverInterface.php b/src/Symfony/Bundle/ArgumentResolver/CompatibleValueResolverInterface.php new file mode 100644 index 00000000000..34242eb8f79 --- /dev/null +++ b/src/Symfony/Bundle/ArgumentResolver/CompatibleValueResolverInterface.php @@ -0,0 +1,29 @@ + + * + * 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\Symfony\Bundle\ArgumentResolver; + +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + +if (interface_exists(ValueResolverInterface::class)) { + /** @internal */ + interface CompatibleValueResolverInterface extends ValueResolverInterface + { + } +} else { + /** @internal */ + interface CompatibleValueResolverInterface extends ArgumentValueResolverInterface + { + } +} diff --git a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php index 3c4d538e8f9..3c66690951d 100644 --- a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php +++ b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php @@ -21,7 +21,7 @@ use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -final class PayloadArgumentResolver implements ValueResolverInterface +final class PayloadArgumentResolver implements CompatibleValueResolverInterface { use OperationRequestInitiatorTrait; @@ -59,8 +59,10 @@ public function supports(Request $request, ArgumentMetadata $argument): bool public function resolve(Request $request, ArgumentMetadata $argument): \Generator { - if (!$this->supports($request, $argument)) { - yield []; + if (interface_exists(ValueResolverInterface::class)) { + if (!$this->supports($request, $argument)) { + yield []; + } } yield $request->attributes->get('data'); diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 7237a47359e..0cf1a057471 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -167,6 +167,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SymfonyUuidDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Taxon; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\TreeDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\UrlEncodedId; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy; @@ -799,6 +800,22 @@ public function thereAreMultiRelationsDummyObjectsHavingEachAManyToOneRelationMa $this->manager->flush(); } + /** + * @Given there are tree dummies + */ + public function thereAreTreeDummies(): void + { + $parentDummy = new TreeDummy(); + $this->manager->persist($parentDummy); + + $childDummy = new TreeDummy(); + $childDummy->setParent($parentDummy); + + $this->manager->persist($childDummy); + + $this->manager->flush(); + } + /** * @Given there are :nb dummy objects with dummyDate * @Given there is :nb dummy object with dummyDate diff --git a/tests/Fixtures/TestBundle/Entity/TreeDummy.php b/tests/Fixtures/TestBundle/Entity/TreeDummy.php new file mode 100644 index 00000000000..ae5a44713c3 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/TreeDummy.php @@ -0,0 +1,63 @@ + + * + * 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\Entity; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource(graphQlOperations: [new Query(), new QueryCollection()])] +#[ORM\Entity] +class TreeDummy +{ + #[ORM\Column(type: 'integer', nullable: true)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + public ?TreeDummy $parent = null; + + /** @var Collection */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + public Collection $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function setParent(?self $parent): void + { + $this->parent = $parent; + } + + public function getChildren(): Collection + { + return $this->children; + } +}