diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 8e5386d..4a132a5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -33,6 +33,7 @@ jobs: fail-fast: false matrix: php-version: [ '8.1', '8.2', '8.3' ] + doctrine-version: [ '^2.19', '^3.2' ] dependency-version: [ prefer-lowest, prefer-stable ] steps: - @@ -44,8 +45,11 @@ jobs: with: php-version: ${{ matrix.php-version }} - - name: Update dependencies + name: Install dependencies run: composer update --no-progress --${{ matrix.dependency-version }} --prefer-dist --no-interaction + - + name: Install correct version of Doctrine + run: composer update --no-progress --${{ matrix.dependency-version }} --prefer-dist --no-interaction --with-all-dependencies doctrine/orm:${{ matrix.doctrine-version }} - name: Run tests run: composer check:tests diff --git a/composer.json b/composer.json index 402bd76..e6554e9 100644 --- a/composer.json +++ b/composer.json @@ -6,11 +6,12 @@ ], "require": { "php": "^8.1", - "doctrine/orm": "^3.2" + "doctrine/orm": "^2.19.7 || ^3.2" }, "require-dev": { "doctrine/collections": "^2.2", - "doctrine/dbal": "^4", + "doctrine/dbal": "^3.9 || ^4.0", + "doctrine/persistence": "^3.3", "editorconfig-checker/editorconfig-checker": "^10.6.0", "ergebnis/composer-normalize": "^2.42.0", "nette/utils": "^4", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 911f8cc..0b333e3 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -24,6 +24,16 @@ parameters: - ShipMonk\DoctrineEntityPreloader\Exception\RuntimeException ignoreErrors: + - + message: '#Strict comparison using === between ReflectionProperty and null will always evaluate to false#' + identifier: 'identical.alwaysFalse' + reportUnmatched: false + path: 'src/EntityPreloader.php' + - + message: '#Result of \|\| is always false#' + identifier: 'booleanOr.alwaysFalse' + reportUnmatched: false + path: 'src/EntityPreloader.php' - message: '#has an uninitialized property \$id#' identifier: 'property.uninitialized' @@ -39,4 +49,5 @@ parameters: path: 'tests/Fixtures/Synthetic' - identifier: 'property.unusedType' + reportUnmatched: false path: 'tests/Fixtures/Synthetic' diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index 49958aa..5c16955 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -2,11 +2,9 @@ namespace ShipMonk\DoctrineEntityPreloader; +use ArrayAccess; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; -use Doctrine\ORM\Mapping\OneToManyAssociationMapping; -use Doctrine\ORM\Mapping\ToManyAssociationMapping; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\QueryBuilder; use LogicException; @@ -56,19 +54,19 @@ public function preload( $associationMapping = $sourceClassMetadata->getAssociationMapping($sourcePropertyName); /** @var ClassMetadata $targetClassMetadata */ - $targetClassMetadata = $this->entityManager->getClassMetadata($associationMapping->targetEntity); + $targetClassMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']); - if ($associationMapping->isIndexed()) { + if (isset($associationMapping['indexBy'])) { throw new LogicException('Preloading of indexed associations is not supported'); } $maxFetchJoinSameFieldCount ??= 1; $sourceEntities = $this->loadProxies($sourceClassMetadata, $sourceEntities, $batchSize ?? self::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE, $maxFetchJoinSameFieldCount); - $preloader = match (true) { - $associationMapping->isToOne() => $this->preloadToOne(...), - $associationMapping->isToMany() => $this->preloadToMany(...), - default => throw new LogicException("Unsupported association mapping type {$associationMapping->type()}"), + $preloader = match ($associationMapping['type']) { + ClassMetadata::ONE_TO_ONE, ClassMetadata::MANY_TO_ONE => $this->preloadToOne(...), + ClassMetadata::ONE_TO_MANY, ClassMetadata::MANY_TO_MANY => $this->preloadToMany(...), + default => throw new LogicException("Unsupported association mapping type {$associationMapping['type']}"), }; return $preloader($sourceEntities, $sourceClassMetadata, $sourcePropertyName, $targetClassMetadata, $batchSize, $maxFetchJoinSameFieldCount); @@ -201,13 +199,9 @@ private function preloadToMany( $associationMapping = $sourceClassMetadata->getAssociationMapping($sourcePropertyName); - if (!$associationMapping instanceof ToManyAssociationMapping) { - throw new LogicException('Unsupported association mapping type'); - } - - $innerLoader = match (true) { - $associationMapping instanceof OneToManyAssociationMapping => $this->preloadOneToManyInner(...), - $associationMapping instanceof ManyToManyAssociationMapping => $this->preloadManyToManyInner(...), + $innerLoader = match ($associationMapping['type']) { + ClassMetadata::ONE_TO_MANY => $this->preloadOneToManyInner(...), + ClassMetadata::MANY_TO_MANY => $this->preloadManyToManyInner(...), default => throw new LogicException('Unsupported association mapping type'), }; @@ -238,6 +232,7 @@ private function preloadToMany( } /** + * @param array|ArrayAccess $associationMapping * @param ClassMetadata $sourceClassMetadata * @param ClassMetadata $targetClassMetadata * @param list $uninitializedSourceEntityIdsChunk @@ -248,7 +243,7 @@ private function preloadToMany( * @template T of E */ private function preloadOneToManyInner( - ToManyAssociationMapping $associationMapping, + array|ArrayAccess $associationMapping, ClassMetadata $sourceClassMetadata, ReflectionProperty $sourceIdentifierReflection, string $sourcePropertyName, @@ -272,7 +267,7 @@ private function preloadOneToManyInner( $targetPropertyName, $uninitializedSourceEntityIdsChunk, $maxFetchJoinSameFieldCount, - $associationMapping->orderBy(), + $associationMapping['orderBy'] ?? [], ); foreach ($targetEntitiesList as $targetEntity) { @@ -288,6 +283,7 @@ private function preloadOneToManyInner( } /** + * @param array|ArrayAccess $associationMapping * @param ClassMetadata $sourceClassMetadata * @param ClassMetadata $targetClassMetadata * @param list $uninitializedSourceEntityIdsChunk @@ -298,7 +294,7 @@ private function preloadOneToManyInner( * @template T of E */ private function preloadManyToManyInner( - ToManyAssociationMapping $associationMapping, + array|ArrayAccess $associationMapping, ClassMetadata $sourceClassMetadata, ReflectionProperty $sourceIdentifierReflection, string $sourcePropertyName, @@ -309,7 +305,7 @@ private function preloadManyToManyInner( int $maxFetchJoinSameFieldCount, ): array { - if (count($associationMapping->orderBy()) > 0) { + if (count($associationMapping['orderBy'] ?? []) > 0) { throw new LogicException('Many-to-many associations with order by are not supported'); } @@ -458,11 +454,11 @@ private function addFetchJoinsToPreventFetchDuringHydration( } /** @var ClassMetadata $targetClassMetadata */ - $targetClassMetadata = $this->entityManager->getClassMetadata($associationMapping->targetEntity); + $targetClassMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']); - $isToOne = ($associationMapping->type() & ClassMetadata::TO_ONE) !== 0; - $isToOneInversed = $isToOne && !$associationMapping->isOwningSide(); - $isToOneAbstract = $isToOne && $associationMapping->isOwningSide() && count($targetClassMetadata->subClasses) > 0; + $isToOne = ($associationMapping['type'] & ClassMetadata::TO_ONE) !== 0; + $isToOneInversed = $isToOne && $associationMapping['isOwningSide'] === false; + $isToOneAbstract = $isToOne && $associationMapping['isOwningSide'] === true && count($targetClassMetadata->subClasses) > 0; if (!$isToOneInversed && !$isToOneAbstract) { continue; diff --git a/tests/EntityPreloadBlogOneHasManyTest.php b/tests/EntityPreloadBlogOneHasManyTest.php index 6a2648f..e5881fd 100644 --- a/tests/EntityPreloadBlogOneHasManyTest.php +++ b/tests/EntityPreloadBlogOneHasManyTest.php @@ -2,11 +2,13 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Composer\InstalledVersions; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\QueryException; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; +use function str_starts_with; class EntityPreloadBlogOneHasManyTest extends TestCase { @@ -54,21 +56,27 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void $categories = $this->getEntityManager()->getRepository(Category::class)->findAll(); - // partial no longer works in doctrine 3.0 - self::assertException( - QueryException::class, - null, - function () use ($categories): void { - $this->getEntityManager()->createQueryBuilder() - ->select('PARTIAL category.{id}', 'article') - ->from(Category::class, 'category') - ->leftJoin('category.articles', 'article') - ->where('category IN (:categories)') - ->setParameter('categories', $categories) - ->getQuery() - ->getResult(); - }, - ); + $query = $this->getEntityManager()->createQueryBuilder() + ->select('PARTIAL category.{id}', 'article') + ->from(Category::class, 'category') + ->leftJoin('category.articles', 'article') + ->where('category IN (:categories)') + ->setParameter('categories', $categories) + ->getQuery(); + + if (str_starts_with(InstalledVersions::getVersion('doctrine/orm') ?? 'unknown', '3.')) { + self::assertException(QueryException::class, null, static fn() => $query->getResult()); + + } else { + $query->getResult(); + + $this->readArticleTitles($categories); + + self::assertAggregatedQueries([ + ['count' => 1, 'query' => 'SELECT * FROM category t0'], + ['count' => 1, 'query' => 'SELECT * FROM category c0_ LEFT JOIN article a1_ ON c0_.id = a1_.category_id WHERE c0_.id IN (?, ?, ?, ?, ?)'], + ]); + } } public function testOneHasManyWithFetchJoin(): void