diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1eed8517c85..cd8e43ed7b5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,6 +12,51 @@ env: fail-fast: true jobs: + phpunit-unstable: + name: "PHPUnit with unstable dependencies" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "8.1" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 2 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + extensions: "pdo, pdo_sqlite" + coverage: "pcov" + ini-values: "zend.assertions=1" + + - name: "Require specific doctrine/persistence version" + run: "composer require doctrine/persistence '3.0.x-dev as 2.4.0' --no-update" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + + - name: "Run PHPUnit" + run: "vendor/bin/phpunit -c ci/github/phpunit/sqlite.xml --coverage-clover=coverage-no-cache.xml" + env: + ENABLE_SECOND_LEVEL_CACHE: 0 + + - name: "Run PHPUnit with Second Level Cache" + run: "vendor/bin/phpunit -c ci/github/phpunit/sqlite.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml" + env: + ENABLE_SECOND_LEVEL_CACHE: 1 + + - name: "Upload coverage file" + uses: "actions/upload-artifact@v2" + with: + name: "phpunit-sqlite-${{ matrix.php-version }}-unstable-coverage" + path: "coverage*.xml" + phpunit-smoke-check: name: "PHPUnit with SQLite" runs-on: "ubuntu-20.04" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 9dace40e8f7..ee006893f4d 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -21,7 +21,15 @@ jobs: - "8.1" dbal-version: - "default" - - "2.13" + persistence-version: + - "default" + include: + - php-version: "8.1" + dbal-version: "2.13" + persistence-version: "default" + - php-version: "8.1" + dbal-version: "default" + persistence-version: "'3.0.x-dev as 2.4.0'" steps: - name: "Checkout code" @@ -37,6 +45,10 @@ jobs: run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update" if: "${{ matrix.dbal-version != 'default' }}" + - name: "Require specific persistence version" + run: "composer require doctrine/persistence ${{ matrix.persistence-version }} --no-update" + if: "${{ matrix.persistence-version != 'default' }}" + - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v1" with: @@ -44,12 +56,16 @@ jobs: - name: "Run a static analysis with phpstan/phpstan" run: "vendor/bin/phpstan analyse" - if: "${{ matrix.dbal-version == 'default' }}" + if: "${{ matrix.dbal-version == 'default' && matrix.persistence-version == 'default'}}" - name: "Run a static analysis with phpstan/phpstan" run: "vendor/bin/phpstan analyse -c phpstan-dbal2.neon" if: "${{ matrix.dbal-version == '2.13' }}" + - name: "Run a static analysis with phpstan/phpstan" + run: "vendor/bin/phpstan analyse -c phpstan-persistence3.neon" + if: "${{ matrix.dbal-version == 'default' && matrix.persistence-version != 'default'}}" + static-analysis-psalm: name: "Static Analysis with Psalm" runs-on: "ubuntu-20.04" diff --git a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php index 4a0502de808..1bd4c5c58cb 100644 --- a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php +++ b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php @@ -5,6 +5,7 @@ namespace Doctrine\ORM\Decorator; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Persistence\ObjectManagerDecorator; @@ -43,6 +44,28 @@ public function getExpressionBuilder() return $this->wrapped->getExpressionBuilder(); } + /** + * {@inheritdoc} + * + * @psalm-param class-string $className + * + * @psalm-return EntityRepository + * + * @template T of object + */ + public function getRepository($className) + { + return $this->wrapped->getRepository($className); + } + + /** + * {@inheritdoc} + */ + public function getClassMetadata($className) + { + return $this->wrapped->getClassMetadata($className); + } + /** * {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index caf8b4527bf..2f9b1c49feb 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -8,16 +8,19 @@ use Doctrine\Common\Collections\AbstractLazyCollection; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Selectable; +use Doctrine\Common\Persistence\PersistentObject; use Doctrine\DBAL\LockMode; use Doctrine\Deprecations\Deprecation; use Doctrine\Inflector\Inflector; use Doctrine\Inflector\InflectorFactory; +use Doctrine\ORM\Exception\NotSupported; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall; use Doctrine\Persistence\ObjectRepository; use function array_slice; +use function class_exists; use function lcfirst; use function sprintf; use function str_starts_with; @@ -168,6 +171,13 @@ public function clear() __METHOD__ ); + if (! class_exists(PersistentObject::class)) { + throw NotSupported::createForPersistence3(sprintf( + 'Partial clearing of entities for class %s', + $this->_class->rootEntityName + )); + } + $this->_em->clear($this->_class->rootEntityName); } diff --git a/lib/Doctrine/ORM/Exception/NotSupported.php b/lib/Doctrine/ORM/Exception/NotSupported.php index 8669aa17a9d..2437d99753b 100644 --- a/lib/Doctrine/ORM/Exception/NotSupported.php +++ b/lib/Doctrine/ORM/Exception/NotSupported.php @@ -4,6 +4,8 @@ namespace Doctrine\ORM\Exception; +use function sprintf; + final class NotSupported extends ORMException { public static function create(): self @@ -15,4 +17,16 @@ public static function createForDbal3(): self { return new self('Feature was deprecated in doctrine/dbal 2.x and is not supported by installed doctrine/dbal:3.x, please see the doctrine/deprecations logs for new alternative approaches.'); } + + public static function createForPersistence3(string $context): self + { + return new self(sprintf( + <<<'EXCEPTION' +Context: %s +Problem: Feature was deprecated in doctrine/persistence 2.x and is not supported by installed doctrine/persistence:3.x +Solution: See the doctrine/deprecations logs for new alternative approaches. +EXCEPTION, + $context + )); + } } diff --git a/phpstan-persistence3.neon b/phpstan-persistence3.neon new file mode 100644 index 00000000000..decd7213030 --- /dev/null +++ b/phpstan-persistence3.neon @@ -0,0 +1,8 @@ +includes: + - phpstan.neon + +parameters: + ignoreErrors: + - + message: '/clear.*invoked with 1 parameter/' + path: lib/Doctrine/ORM/EntityRepository.php diff --git a/psalm.xml b/psalm.xml index 348034f00a7..36013d125d8 100644 --- a/psalm.xml +++ b/psalm.xml @@ -31,6 +31,7 @@ + diff --git a/tests/Doctrine/Tests/Models/Company/CompanyFlexContract.php b/tests/Doctrine/Tests/Models/Company/CompanyFlexContract.php index 61bb7e1ab5e..b68b92f35ac 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyFlexContract.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyFlexContract.php @@ -11,6 +11,8 @@ use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\EntityResult; use Doctrine\ORM\Mapping\FieldResult; +use Doctrine\ORM\Mapping\GeneratedValue; +use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\JoinTable; use Doctrine\ORM\Mapping\ManyToMany; @@ -65,6 +67,14 @@ #[ORM\Entity] class CompanyFlexContract extends CompanyContract { + /** + * @Id + * @GeneratedValue + * @Column(type="integer") + * @var int + */ + public $id; + /** * @Column(type="integer") * @var int diff --git a/tests/Doctrine/Tests/ORM/Decorator/EntityManagerDecoratorTest.php b/tests/Doctrine/Tests/ORM/Decorator/EntityManagerDecoratorTest.php index 4600871065d..7a77d2be8f9 100644 --- a/tests/Doctrine/Tests/ORM/Decorator/EntityManagerDecoratorTest.php +++ b/tests/Doctrine/Tests/ORM/Decorator/EntityManagerDecoratorTest.php @@ -7,13 +7,18 @@ use Doctrine\ORM\Decorator\EntityManagerDecorator; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\ResultSetMapping; +use Generator; +use LogicException; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionMethod; +use ReflectionNamedType; +use stdClass; -use function array_fill; +use function assert; use function in_array; +use function sprintf; class EntityManagerDecoratorTest extends TestCase { @@ -40,21 +45,18 @@ protected function setUp(): void $this->wrapped = $this->createMock(EntityManagerInterface::class); } - /** @psalm-return array */ - public function getMethodParameters(): array + /** @psalm-return Generator */ + public function getMethodParameters(): Generator { - $class = new ReflectionClass(EntityManagerInterface::class); - $methods = []; + $class = new ReflectionClass(EntityManagerInterface::class); foreach ($class->getMethods() as $method) { if ($method->isConstructor() || $method->isStatic() || ! $method->isPublic()) { continue; } - $methods[$method->getName()] = $this->getParameters($method); + yield $method->getName() => $this->getParameters($method); } - - return $methods; } /** @@ -77,19 +79,34 @@ static function (): void { ]; } - if ($method->getNumberOfRequiredParameters() === 0) { - return [$method->getName(), []]; - } + $parameters = []; - if ($method->getNumberOfRequiredParameters() > 0) { - return [$method->getName(), array_fill(0, $method->getNumberOfRequiredParameters(), 'req') ?: []]; - } + foreach ($method->getParameters() as $parameter) { + if ($parameter->getType() === null) { + $parameters[] = 'mixed'; + continue; + } - if ($method->getNumberOfParameters() !== $method->getNumberOfRequiredParameters()) { - return [$method->getName(), array_fill(0, $method->getNumberOfParameters(), 'all') ?: []]; + $type = $parameter->getType(); + assert($type instanceof ReflectionNamedType); + switch ($type->getName()) { + case 'string': + $parameters[] = 'parameter'; + break; + + case 'object': + $parameters[] = new stdClass(); + break; + + default: + throw new LogicException(sprintf( + 'Type %s is not handled yet', + (string) $parameter->getType() + )); + } } - return []; + return [$method->getName(), $parameters]; } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index 777679216b9..b2a7313579c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -7,6 +7,7 @@ use BadMethodCallException; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Persistence\PersistentObject; use Doctrine\DBAL\Connection; use Doctrine\DBAL\LockMode; use Doctrine\DBAL\Logging\Middleware as LoggingMiddleware; @@ -14,6 +15,7 @@ use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Exception\InvalidEntityRepository; +use Doctrine\ORM\Exception\NotSupported; use Doctrine\ORM\Exception\ORMException; use Doctrine\ORM\Exception\UnrecognizedIdentifierFields; use Doctrine\ORM\Mapping\MappingException; @@ -1155,7 +1157,12 @@ public function testDeprecatedClear(): void { $repository = $this->_em->getRepository(CmsAddress::class); - $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/8460'); + if (class_exists(PersistentObject::class)) { + $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/8460'); + } else { + $this->expectException(NotSupported::class); + $this->expectExceptionMessage(CmsAddress::class); + } $repository->clear(); } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2359Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2359Test.php index db23318b16e..32af0bbf530 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2359Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2359Test.php @@ -47,22 +47,22 @@ public function testIssue(): void $connection = $this->createMock(Connection::class); $configuration - ->expects(self::any()) ->method('getMetadataDriverImpl') ->will(self::returnValue($mockDriver)); $entityManager->expects(self::any())->method('getConfiguration')->will(self::returnValue($configuration)); $entityManager->expects(self::any())->method('getConnection')->will(self::returnValue($connection)); $entityManager - ->expects(self::any()) ->method('getEventManager') ->will(self::returnValue($this->createMock(EventManager::class))); - $metadataFactory->expects(self::any())->method('newClassMetadataInstance')->will(self::returnValue($mockMetadata)); + $metadataFactory->method('newClassMetadataInstance')->will(self::returnValue($mockMetadata)); $metadataFactory->expects(self::once())->method('wakeupReflection'); $metadataFactory->setEntityManager($entityManager); + $mockMetadata->method('getName')->willReturn(DDC2359Foo::class); + self::assertSame($mockMetadata, $metadataFactory->getMetadataFor(DDC2359Foo::class)); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7512Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7512Test.php index d390d50899c..204dcb7bf3f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7512Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7512Test.php @@ -35,9 +35,9 @@ protected function setUp(): void public function testFindEntityByAssociationPropertyJoinedChildWithClearMetadata(): void { - // unset metadata for entity B as though it hasn't been touched yet in application lifecycle. - $this->_em->getMetadataFactory()->setMetadataFor(GH7512EntityB::class, null); - $result = $this->_em->getRepository(GH7512EntityC::class)->findBy([ + // pretend we are starting afresh + $this->_em = $this->getEntityManager(); + $result = $this->_em->getRepository(GH7512EntityC::class)->findBy([ 'entityA' => new GH7512EntityB(), ]); $this->assertEmpty($result);