Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
fail-fast: false
matrix:
php-version: [ '8.1', '8.2', '8.3', '8.4' ]
doctrine-version: [ '^2.19', '^3.2' ]
doctrine-version: [ '^2.19', '^3' ]
dependency-version: [ prefer-lowest, prefer-stable ]
steps:
-
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
],
"require": {
"php": "^8.1",
"doctrine/dbal": "^3.7 || ^4.0",
"doctrine/orm": "^2.19.7 || ^3.2"
},
"require-dev": {
"composer/semver": "^3.0",
"doctrine/collections": "^2.2",
"doctrine/dbal": "^3.9 || ^4.0",
"doctrine/persistence": "^3.3",
"editorconfig-checker/editorconfig-checker": "^10.6.0",
"ergebnis/composer-normalize": "^2.42.0",
Expand Down Expand Up @@ -60,7 +61,7 @@
"check:dependencies": "composer-dependency-analyser",
"check:ec": "ec src tests",
"check:tests": "phpunit tests",
"check:types": "phpstan analyse -vvv",
"check:types": "phpstan analyse -vv --ansi",
"fix:cs": "phpcbf"
}
}
11 changes: 7 additions & 4 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ parameters:
paths:
- src
- tests
excludePaths:
analyse:
- tests/Fixtures/Compat
checkMissingCallableSignature: true
checkUninitializedProperties: true
checkTooWideReturnTypesInProtectedAndPublicMethods: true
Expand All @@ -29,15 +32,15 @@ parameters:
identifier: 'identical.alwaysFalse'
reportUnmatched: false
path: 'src/EntityPreloader.php'
-
identifier: shipmonk.defaultMatchArmWithEnum
reportUnmatched: false # only new dbal issue
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'
path: 'tests/Fixtures/Blog'
-
identifier: 'property.onlyWritten'
path: 'tests/Fixtures/Synthetic'
Expand Down
54 changes: 50 additions & 4 deletions src/EntityPreloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace ShipMonk\DoctrineEntityPreloader;

use ArrayAccess;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
Expand Down Expand Up @@ -141,7 +144,7 @@ private function loadProxies(
}

foreach (array_chunk($uninitializedIds, $batchSize) as $idsChunk) {
$this->loadEntitiesBy($classMetadata, $identifierName, $idsChunk, $maxFetchJoinSameFieldCount);
$this->loadEntitiesBy($classMetadata, $identifierName, $classMetadata, $idsChunk, $maxFetchJoinSameFieldCount);
}

return array_values($uniqueEntities);
Expand Down Expand Up @@ -270,6 +273,7 @@ private function preloadOneToManyInner(
$targetEntitiesList = $this->loadEntitiesBy(
$targetClassMetadata,
$targetPropertyName,
$sourceClassMetadata,
$uninitializedSourceEntityIdsChunk,
$maxFetchJoinSameFieldCount,
$associationMapping['orderBy'] ?? [],
Expand Down Expand Up @@ -318,12 +322,18 @@ private function preloadManyToManyInner(
$sourceIdentifierName = $sourceClassMetadata->getSingleIdentifierFieldName();
$targetIdentifierName = $targetClassMetadata->getSingleIdentifierFieldName();

$sourceIdentifierType = $this->getIdentifierFieldType($sourceClassMetadata);

$manyToManyRows = $this->entityManager->createQueryBuilder()
->select("source.{$sourceIdentifierName} AS sourceId", "target.{$targetIdentifierName} AS targetId")
->from($sourceClassMetadata->getName(), 'source')
->join("source.{$sourcePropertyName}", 'target')
->andWhere('source IN (:sourceEntityIds)')
->setParameter('sourceEntityIds', $uninitializedSourceEntityIdsChunk)
->setParameter(
'sourceEntityIds',
$uninitializedSourceEntityIdsChunk,
$this->deduceArrayParameterType($sourceIdentifierType),
)
->getQuery()
->getResult();

Expand All @@ -345,7 +355,7 @@ private function preloadManyToManyInner(
$uninitializedTargetEntityIds[$targetEntityKey] = $targetEntityId;
}

foreach ($this->loadEntitiesBy($targetClassMetadata, $targetIdentifierName, array_values($uninitializedTargetEntityIds), $maxFetchJoinSameFieldCount) as $targetEntity) {
foreach ($this->loadEntitiesBy($targetClassMetadata, $targetIdentifierName, $sourceClassMetadata, array_values($uninitializedTargetEntityIds), $maxFetchJoinSameFieldCount) as $targetEntity) {
$targetEntityKey = (string) $targetIdentifierReflection->getValue($targetEntity);
$targetEntities[$targetEntityKey] = $targetEntity;
}
Expand Down Expand Up @@ -404,15 +414,18 @@ private function preloadToOne(
/**
* @param ClassMetadata<T> $targetClassMetadata
* @param list<mixed> $fieldValues
* @param ClassMetadata<R> $referencedClassMetadata
* @param non-negative-int $maxFetchJoinSameFieldCount
* @param array<string, 'asc'|'desc'> $orderBy
* @return list<T>
*
* @template T of E
* @template R of E
*/
private function loadEntitiesBy(
ClassMetadata $targetClassMetadata,
string $fieldName,
ClassMetadata $referencedClassMetadata,
array $fieldValues,
int $maxFetchJoinSameFieldCount,
array $orderBy = [],
Expand All @@ -422,13 +435,18 @@ private function loadEntitiesBy(
return [];
}

$referencedType = $this->getIdentifierFieldType($referencedClassMetadata);
$rootLevelAlias = 'e';

$queryBuilder = $this->entityManager->createQueryBuilder()
->select($rootLevelAlias)
->from($targetClassMetadata->getName(), $rootLevelAlias)
->andWhere("{$rootLevelAlias}.{$fieldName} IN (:fieldValues)")
->setParameter('fieldValues', $fieldValues);
->setParameter(
'fieldValues',
$fieldValues,
$this->deduceArrayParameterType($referencedType),
);

$this->addFetchJoinsToPreventFetchDuringHydration($rootLevelAlias, $queryBuilder, $targetClassMetadata, $maxFetchJoinSameFieldCount);

Expand All @@ -439,6 +457,34 @@ private function loadEntitiesBy(
return $queryBuilder->getQuery()->getResult();
}

private function deduceArrayParameterType(Type $dbalType): ArrayParameterType|int|null // @phpstan-ignore return.unusedType (old dbal compat)
{
return match ($dbalType->getBindingType()) {
ParameterType::INTEGER => ArrayParameterType::INTEGER,
ParameterType::STRING => ArrayParameterType::STRING,
ParameterType::ASCII => ArrayParameterType::ASCII,
ParameterType::BINARY => ArrayParameterType::BINARY,
default => null,
};
}

/**
* @param ClassMetadata<C> $classMetadata
*
* @template C of E
*/
private function getIdentifierFieldType(ClassMetadata $classMetadata): Type
{
$identifierName = $classMetadata->getSingleIdentifierFieldName();
$sourceIdTypeName = $classMetadata->getTypeOfField($identifierName);

if ($sourceIdTypeName === null) {
throw new LogicException("Identifier field '{$identifierName}' for class '{$classMetadata->getName()}' has unknown field type.");
}

return Type::getType($sourceIdTypeName);
}

/**
* @param ClassMetadata<S> $sourceClassMetadata
* @param array<string, array<string, int>> $alreadyPreloadedJoins
Expand Down
8 changes: 7 additions & 1 deletion tests/EntityPreloadBlogManyHasManyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace ShipMonkTests\DoctrineEntityPreloader;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\ORM\Mapping\ClassMetadata;
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article;
use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase;
use function array_map;

class EntityPreloadBlogManyHasManyTest extends TestCase
{
Expand All @@ -29,13 +31,17 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void
$this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5);

$articles = $this->getEntityManager()->getRepository(Article::class)->findAll();
$rawArticleIds = array_map(
static fn (Article $article): string => $article->getId()->getBytes(),
$articles,
);

$this->getEntityManager()->createQueryBuilder()
->select('PARTIAL article.{id}', 'tag')
->from(Article::class, 'article')
->leftJoin('article.tags', 'tag')
->where('article IN (:articles)')
->setParameter('articles', $articles)
->setParameter('articles', $rawArticleIds, ArrayParameterType::BINARY)
->getQuery()
->getResult();

Expand Down
14 changes: 8 additions & 6 deletions tests/EntityPreloadBlogManyHasOneDeepTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ShipMonkTests\DoctrineEntityPreloader;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\ORM\Mapping\ClassMetadata;
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article;
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category;
Expand Down Expand Up @@ -35,27 +36,27 @@ public function testManyHasOneDeepWithManualPreload(): void

$articles = $this->getEntityManager()->getRepository(Article::class)->findAll();

$categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId(), $articles);
$categoryIds = array_filter($categoryIds, static fn (?int $id) => $id !== null);
$categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId()->getBytes(), $articles);
$categoryIds = array_filter($categoryIds, static fn (?string $id) => $id !== null);

if (count($categoryIds) > 0) {
$categories = $this->getEntityManager()->createQueryBuilder()
->select('category')
->from(Category::class, 'category')
->where('category.id IN (:ids)')
->setParameter('ids', array_values(array_unique($categoryIds)))
->setParameter('ids', array_values(array_unique($categoryIds)), ArrayParameterType::BINARY)
->getQuery()
->getResult();

$parentCategoryIds = array_map(static fn (Category $category) => $category->getParent()?->getId(), $categories);
$parentCategoryIds = array_filter($parentCategoryIds, static fn (?int $id) => $id !== null);
$parentCategoryIds = array_map(static fn (Category $category) => $category->getParent()?->getId()->getBytes(), $categories);
$parentCategoryIds = array_filter($parentCategoryIds, static fn (?string $id) => $id !== null);

if (count($parentCategoryIds) > 0) {
$this->getEntityManager()->createQueryBuilder()
->select('category')
->from(Category::class, 'category')
->where('category.id IN (:ids)')
->setParameter('ids', array_values(array_unique($parentCategoryIds)))
->setParameter('ids', array_values(array_unique($parentCategoryIds)), ArrayParameterType::BINARY)
->getQuery()
->getResult();
}
Expand Down Expand Up @@ -90,6 +91,7 @@ public function testManyHasOneDeepWithFetchJoin(): void

public function testManyHasOneDeepWithEagerFetchMode(): void
{
$this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase();
$this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5);

$articles = $this->getEntityManager()->createQueryBuilder()
Expand Down
8 changes: 5 additions & 3 deletions tests/EntityPreloadBlogManyHasOneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ShipMonkTests\DoctrineEntityPreloader;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\ORM\Mapping\ClassMetadata;
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article;
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category;
Expand Down Expand Up @@ -35,15 +36,15 @@ public function testManyHasOneWithManualPreload(): void

$articles = $this->getEntityManager()->getRepository(Article::class)->findAll();

$categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId(), $articles);
$categoryIds = array_filter($categoryIds, static fn (?int $id) => $id !== null);
$categoryIds = array_map(static fn (Article $article): ?string => $article->getCategory()?->getId()->getBytes(), $articles);
$categoryIds = array_filter($categoryIds, static fn (?string $id) => $id !== null);

if (count($categoryIds) > 0) {
$this->getEntityManager()->createQueryBuilder()
->select('category')
->from(Category::class, 'category')
->where('category.id IN (:ids)')
->setParameter('ids', array_values(array_unique($categoryIds)))
->setParameter('ids', array_values(array_unique($categoryIds)), ArrayParameterType::BINARY)
->getQuery()
->getResult();
}
Expand Down Expand Up @@ -76,6 +77,7 @@ public function testManyHasOneWithFetchJoin(): void

public function testManyHasOneWithEagerFetchMode(): void
{
$this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase();
$this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5);

$articles = $this->getEntityManager()->createQueryBuilder()
Expand Down
1 change: 1 addition & 0 deletions tests/EntityPreloadBlogOneHasManyAbstractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function testOneHasManyAbstractWithFetchJoin(): void

public function testOneHasManyAbstractWithEagerFetchMode(): void
{
$this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase();
$this->createDummyBlogData(categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5);

$articles = $this->getEntityManager()->createQueryBuilder()
Expand Down
10 changes: 8 additions & 2 deletions tests/EntityPreloadBlogOneHasManyDeepTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ShipMonkTests\DoctrineEntityPreloader;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\ORM\Mapping\ClassMetadata;
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category;
use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase;
Expand Down Expand Up @@ -42,22 +43,26 @@ public function testOneHasManyDeepWithWithManualPreloadUsingPartial(): void
->getQuery()
->getResult();

$rawRootCategoryIds = array_map(static fn (Category $category) => $category->getId()->getBytes(), $rootCategories);

$this->getEntityManager()->createQueryBuilder()
->select('PARTIAL category.{id}', 'subCategory')
->from(Category::class, 'category')
->leftJoin('category.children', 'subCategory')
->where('category IN (:categories)')
->setParameter('categories', $rootCategories)
->setParameter('categories', $rawRootCategoryIds, ArrayParameterType::BINARY)
->getQuery()
->getResult();

$subCategories = array_merge(...array_map(static fn (Category $category) => $category->getChildren()->toArray(), $rootCategories));
$rawSubCategoryIds = array_map(static fn (Category $category) => $category->getId()->getBytes(), $subCategories);

$this->getEntityManager()->createQueryBuilder()
->select('PARTIAL subCategory.{id}', 'subSubCategory')
->from(Category::class, 'subCategory')
->leftJoin('subCategory.children', 'subSubCategory')
->where('subCategory IN (:subCategories)')
->setParameter('subCategories', $subCategories)
->setParameter('subCategories', $rawSubCategoryIds, ArrayParameterType::BINARY)
->getQuery()
->getResult();

Expand Down Expand Up @@ -92,6 +97,7 @@ public function testOneHasManyDeepWithFetchJoin(): void

public function testOneHasManyDeepWithEagerFetchMode(): void
{
$this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase();
$this->createCategoryTree(depth: 5, branchingFactor: 5);

$rootCategories = $this->getEntityManager()->createQueryBuilder()
Expand Down
Loading