Skip to content

Conversation

VincentLanglet
Copy link
Contributor

Q A
Branch? main
Tickets Closes #..., closes #...
License MIT
Doc PR api-platform/docs#...

Hi @soyuka,

I wanted to bump PHPStan level to 6 and saw it was still using v1 ; so I started by bumping to v2.
Since there is lot of new errors I used the following strategy

  • Fix the easy ones
  • Ignore some of the new errors reported by PHPStan v2

Then I'll provide PR to fix some identifiers error, but I think it's better to split PR for readability.

@@ -17,7 +17,6 @@ parameters:
excludePaths:
# uses larastan
- src/Laravel
- src/Symfony/Bundle/Command/OpenApiCommand.php
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those files doesn't exists and are reported by PHPStan v2 now.

@@ -75,6 +71,15 @@ parameters:
# Expected, due to backward compatibility
- '#Method GraphQL\\Type\\Definition\\WrappingType::getWrappedType\(\) invoked with 1 parameter, 0 required\.#'
- '#Access to an undefined property GraphQL\\Type\\Definition\\NamedType&GraphQL\\Type\\Definition\\Type::\$name\.#'
- "#Call to function method_exists\\(\\) with GraphQL\\\\Type\\\\Definition\\\\Type&GraphQL\\\\Type\\\\Definition\\\\WrappingType and 'getInnermostType' will always evaluate to true\\.#"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of method_exists call added for BC, so I updated the ignored errors.

# Allow extra assertions in tests: https://github.com/phpstan/phpstan-strict-rules/issues/130
- '#^Call to (static )?method PHPUnit\\Framework\\Assert::.* will always evaluate to true\.$#'

# TODO For PHPStan 2.0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a list of errors which I'd like to fix in following PR.

@@ -56,10 +56,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$manager = $this->managerRegistry->getManagerForClass($entityClass);

$repository = $manager->getRepository($entityClass);
if (!method_exists($repository, 'createQueryBuilder')) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$repository is always an EntityRepository according to PHPStan and I think it's right.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes therefore we can remove that line instead of ignoring ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause some failure in tests because of testCannotCreateQueryBuilder

public function testCannotCreateQueryBuilder(): void
    {
        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('The repository class must have a "createQueryBuilder" method.');

        $repositoryProphecy = $this->prophesize(ObjectRepository::class);

        $managerProphecy = $this->prophesize(ObjectManager::class);
        $managerProphecy->getRepository(OperationResource::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled();

        $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class);
        $managerRegistryProphecy->getManagerForClass(OperationResource::class)->willReturn($managerProphecy->reveal())->shouldBeCalled();

        $dataProvider = new CollectionProvider($this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal(), $managerRegistryProphecy->reveal());
        $operation = (new GetCollection())->withClass(OperationResource::class)->withName('getCollection');
        $this->assertEquals([], $dataProvider->provide($operation));
    }

The fact is that

  • An entityManagerINterface always returns an EntityRepository which has the createQueryBuilder
  • The existing phpdoc say the manager for class is an EntityManagerInterface but it can theorically be a simple ObjectManager (which is the case tested in tests)
/** @var EntityManagerInterface|null $manager */
$manager = $this->managerRegistry->getManagerForClass($entityClass);

So I'm unsure how you want to solve/change this @soyuka

@@ -65,10 +65,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
}

$repository = $manager->getRepository($entityClass);
if (!method_exists($repository, 'createQueryBuilder')) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same idea

@@ -41,6 +41,7 @@ public function testParameterFactory(): void
$filterLocator->method('get')->willReturn(new class implements FilterInterface {
public function getDescription(string $resourceClass): array
{
// @phpstan-ignore-next-line return.type
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we're passing volontary a wrong type to check it's handle in the method.

@@ -101,7 +101,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
*/
protected function normalizeRawCollection(iterable $object, ?string $format = null, array $context = []): array|\ArrayObject
{
if (!$object && ($context[Serializer::EMPTY_ARRAY_AS_OBJECT] ?? false) && \is_array($object)) {
if ([] === $object && ($context[Serializer::EMPTY_ARRAY_AS_OBJECT] ?? false)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!$object already enforce it's [].

But I rewrote the condition to be explicit

@@ -1163,9 +1163,10 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value
}

if (
($t instanceof CollectionType && $collectionValueType instanceof ObjectType && (null !== ($className = $collectionValueType->getClassName())))
|| ($t instanceof LegacyType && $t->isCollection() && null !== $collectionValueType && null !== ($className = $collectionValueType->getClassName()))
($t instanceof CollectionType && $collectionValueType instanceof ObjectType)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this condition (null !== ($className = $collectionValueType->getClassName())) is always true cause getClassName returns a string.

So we need to remove it from the condition, and it force declaring the variable after the if.

@@ -91,9 +91,6 @@ private function normalizeJson(mixed $document): object|array
}

$document = json_encode($document, \JSON_THROW_ON_ERROR);
if (!\is_string($document)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$document is always a string because of JSON_THROW_ON_ERROR

@@ -47,7 +47,6 @@ public static function getResources(): array
public function testRequest(): void
{
$client = self::createClient();
$client->getKernelBrowser();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useless call

@VincentLanglet VincentLanglet force-pushed the bumpPhpstan branch 3 times, most recently from 299f4cb to 6837010 Compare June 20, 2025 16:56
Copy link
Member

@soyuka soyuka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these are fine I think it'd be a good start, just a few comments.

Also any reason we don't do this on the 4.1 branch? Can you also rebase on the latest main branch?

@@ -56,10 +56,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$manager = $this->managerRegistry->getManagerForClass($entityClass);

$repository = $manager->getRepository($entityClass);
if (!method_exists($repository, 'createQueryBuilder')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes therefore we can remove that line instead of ignoring ?

@VincentLanglet
Copy link
Contributor Author

Also any reason we don't do this on the 4.1 branch?

There is some extra changes in main like

  • New feature/simplification
  • Added @phpstan-ignore tags cf conflict when cherry-picking this on 4.1
        both modified:   src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php
        both modified:   src/JsonSchema/Tests/SchemaFactoryTest.php
        both modified:   src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php
        both modified:   src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php
        both modified:   src/Serializer/AbstractItemNormalizer.php
        both modified:   src/Serializer/Tests/AbstractItemNormalizerTest.php

Which means that bumping phpstan on 4.1, will give failure on main branch and require extra work. Especially if we do more PR to solve more PHPStan errors.

Wouldn't it be ok to plan a 4.2 release instead ?

@VincentLanglet VincentLanglet requested a review from soyuka June 26, 2025 08:24
@soyuka soyuka merged commit b48501a into api-platform:main Jun 27, 2025
111 of 114 checks passed
@soyuka
Copy link
Member

soyuka commented Jun 27, 2025

Thanks @VincentLanglet this is a good starting point!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants