Compare commits

...

31 Commits
3.5.1 ... 3.5.2

Author SHA1 Message Date
Grégoire Paris
5a541b8b3a Merge pull request #12121 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-08-08 19:00:40 +02:00
Grégoire Paris
9fb9cc46e4 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-08-08 09:03:30 +02:00
Grégoire Paris
c322c71cd4 Merge pull request #12120 from greg0ire/fix-broken-comments
Fix broken comments
2025-08-08 08:55:44 +02:00
Grégoire Paris
3c733a2fee Add missing phpdoc 2025-08-08 08:34:07 +02:00
Grégoire Paris
5984ad586a Fix broken phpdoc comments 2025-08-08 08:33:39 +02:00
Grégoire Paris
ee2c3a506b Merge pull request #12097 from mvorisek/add_fixed_id_insert_count_query_test
Add 2nd level cache test for insert without post-inserted ID
2025-08-08 08:26:23 +02:00
Grégoire Paris
781ed30926 Merge pull request #12108 from greg0ire/address-deprecations
Address deprecations
2025-08-08 08:25:29 +02:00
Grégoire Paris
04694a9f7b Merge pull request #11835 from gseidel/fix-pre-persist-call-persist
fix: calling scheduleForInsert twice
2025-08-07 06:25:35 +02:00
Grégoire Paris
257c5094c4 Use not nullable columns when part of the primary key
Using a nullable column that references another table as part of a
primary key makes no sense, and is ignored by DBAL. Let us ignore it at
the ORM level.
2025-08-06 23:06:28 +02:00
Alexander M. Turek
66e0e92816 Don't partially mock the AbstractPlatform class (#12114) 2025-08-06 18:12:23 +02:00
Michael Voříšek
5b2060e25f Add 2nd level cache test for insert without post-inserted ID
Inserts without post-inserted ID can be sent to DB grouped together
hence the extra test.
2025-08-06 08:22:48 +02:00
Grégoire Paris
39e35fc06c Merge pull request #12099 from alexislefebvre/2.20.x-update-supported-branches-on-README
doc: update supported branches on README (2.20.x)
2025-08-04 16:59:49 +02:00
Alexis Lefebvre
7f061c3870 doc: update supported branches on README 2025-08-04 16:38:55 +02:00
Grégoire Paris
74495711fb Merge pull request #11934 from mvorisek/fix_joined_subclass_persister_insert_of_multiple_entities
Fix JoinedSubclassPersister when multiple entities are inserted
2025-08-02 08:34:29 +02:00
Michael Voříšek
97a7cb8d2f Unify JoinedSubclassPersister dequeue
Fix JoinedSubclassPersister as BasicEntityPersister was already fixed in GH-10735.

The fix can be verified by modifying UnitOfWork to execute `BasicEntityPersister::executeInserts()` for multiple entities at once for the same entity class/persister instance - https://github.com/doctrine/orm/blob/2.20.3/src/UnitOfWork.php#L1186 - then reproducible on `Doctrine\Tests\ORM\Functional\Ticket\GH10531Test::testInserts` test.

As extending/modifying UnitOfWork in tests in not easily possible, I submit this fix for v2.x without a test.
2025-08-01 15:31:18 +02:00
Grégoire Paris
e0052390e1 Merge pull request #12087 from mvorisek/improve_basic_entity_persister
Improve BasicEntityPersister to be more flexible and cleaner
2025-07-30 09:44:24 +02:00
Michael Voříšek
8c6419e0e0 Prefer strict empty-array comparison over empty() call 2025-07-29 15:15:31 +02:00
Michael Voříšek
6f5ce1aca2 BasicEntityPersister: refactor $values variable into $placeholders
The new variable name is much more clearer.
2025-07-29 15:15:31 +02:00
Michael Voříšek
98e7a53b42 Remove BasicEntityPersister::$insertSql cache property
When the persister is extended to do a multi update, the caching is not
wanted. The impact is minimal as the CPU/time overhead per query is
much bigger and the prepared statement is not cached anyway.
2025-07-29 15:15:31 +02:00
Gerhard Seidel
3aaaf37dfb fix: PrePersistEventTest typos and unnecessary comments 2025-07-29 14:40:20 +02:00
Grégoire Paris
154a4652ee Merge pull request #12086 from mvorisek/add_cache_rw_strict_locking_test
Add functional strict-locking 2nd level cache test
2025-07-29 11:48:25 +02:00
Michael Voříšek
ae7489ff19 Add functional strict-locking 2nd level cache test 2025-07-28 12:14:50 +02:00
Grégoire Paris
528b8837e1 Merge pull request #11929 from doctrine/2.20.x-merge-up-into-2.21.x_KkdqS0u7
Merge release 2.20.3 into 2.21.x
2025-05-02 21:57:23 +02:00
Gerhard Seidel
4fb044d5f6 fix: cs 2025-02-20 10:01:35 +08:00
Gerhard Seidel
2a953c5e2b fix: PrePersistEventTest and cs 2025-02-17 14:01:08 +08:00
Gerhard Seidel
abc6a40ccb fix: calling scheduleForInsert twice
If scheduleForInsert was called in prePersist hook already, then persistNew need to check this case first, otherwise a ORMInvalidArgumentException will be thrown
2025-02-14 12:45:13 +08:00
Grégoire Paris
73e68f3c7d Merge pull request #11821 from doctrine/2.20.x-merge-up-into-2.21.x_8O8nHxqC
Merge release 2.20.2 into 2.21.x
2025-02-04 20:24:01 +01:00
Alexander M. Turek
73777d0bd4 Merge branch '2.20.x' into 2.21.x
* 2.20.x:
  Introduce testNotListedValueInEnumArray
  Fix documentation for JoinColumn nullable (#11798)
  Ignore deprecations from doctrine/common
  Fix fields of transient classes being considered duplicate with `reportFieldsWhereDeclared`
2025-01-26 19:56:20 +01:00
Grégoire Paris
e89b58a13f Merge pull request #11771 from doctrine/2.20.x-merge-up-into-2.21.x_3Yg2ZYgM
Merge release 2.20.1 into 2.21.x
2024-12-19 08:16:04 +01:00
Grégoire Paris
2b94ec18b9 Merge pull request #11759 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-12-08 14:33:31 +01:00
Grégoire Paris
2a662149f4 Merge pull request #11754 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-12-07 15:39:29 +01:00
15 changed files with 487 additions and 61 deletions

View File

@@ -12,7 +12,7 @@ trait JoinColumnProperties
public readonly string|null $referencedColumnName = null,
public readonly bool $deferrable = false,
public readonly bool $unique = false,
public readonly bool $nullable = true,
public readonly bool|null $nullable = null,
public readonly mixed $onDelete = null,
public readonly string|null $columnDefinition = null,
public readonly string|null $fieldName = null,

View File

@@ -127,6 +127,8 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
$mapping->joinTableColumns = [];
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
$joinColumn->nullable = false;
if (empty($joinColumn->referencedColumnName)) {
$joinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
}
@@ -150,6 +152,8 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
}
foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) {
$inverseJoinColumn->nullable = false;
if (empty($inverseJoinColumn->referencedColumnName)) {
$inverseJoinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
}

View File

@@ -130,6 +130,12 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
$uniqueConstraintColumns = [];
foreach ($mapping->joinColumns as $joinColumn) {
if ($mapping->id) {
$joinColumn->nullable = false;
} elseif ($joinColumn->nullable === null) {
$joinColumn->nullable = true;
}
if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) {
if (count($mapping->joinColumns) === 1) {
if (empty($mapping->id)) {

View File

@@ -153,12 +153,6 @@ class BasicEntityPersister implements EntityPersister
*/
protected array $quotedColumns = [];
/**
* The INSERT SQL statement used for entities handled by this persister.
* This SQL is only generated once per request, if at all.
*/
private string|null $insertSql = null;
/**
* The quote strategy.
*/
@@ -273,8 +267,8 @@ class BasicEntityPersister implements EntityPersister
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
// Unset this queued insert, so that the prepareUpdateData() method knows right away
// (for the next entity already) that the current entity has been written to the database
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
// knows right away (for the next entity already) that the current entity has been written to the database
// and no extra updates need to be scheduled to refer to it.
//
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
@@ -1418,22 +1412,17 @@ class BasicEntityPersister implements EntityPersister
public function getInsertSQL(): string
{
if ($this->insertSql !== null) {
return $this->insertSql;
}
$columns = $this->getInsertColumnList();
$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
if (empty($columns)) {
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
$this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
if ($columns === []) {
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
return $this->insertSql;
return $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
}
$values = [];
$columns = array_unique($columns);
$placeholders = [];
$columns = array_unique($columns);
foreach ($columns as $column) {
$placeholder = '?';
@@ -1447,15 +1436,13 @@ class BasicEntityPersister implements EntityPersister
$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
}
$values[] = $placeholder;
$placeholders[] = $placeholder;
}
$columns = implode(', ', $columns);
$values = implode(', ', $values);
$columns = implode(', ', $columns);
$placeholders = implode(', ', $placeholders);
$this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
return $this->insertSql;
return sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);
}
/**

View File

@@ -134,7 +134,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
// Execute all inserts. For each entity:
// 1) Insert on root table
// 2) Insert on sub tables
foreach ($this->queuedInserts as $entity) {
foreach ($this->queuedInserts as $key => $entity) {
$insertData = $this->prepareInsertData($entity);
// Execute insert on root table
@@ -179,9 +179,16 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
}
$this->queuedInserts = [];
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
// knows right away (for the next entity already) that the current entity has been written to the database
// and no extra updates need to be scheduled to refer to it.
//
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
// from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
// were given to our addInsert() method.
unset($this->queuedInserts[$key]);
}
}
public function update(object $entity): void

View File

@@ -934,7 +934,9 @@ class UnitOfWork implements PropertyChangedListener
$this->entityStates[$oid] = self::STATE_MANAGED;
$this->scheduleForInsert($entity);
if (! isset($this->entityInsertions[$oid])) {
$this->scheduleForInsert($entity);
}
}
/** @param mixed[] $idValue */

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\Tests\OrmFunctionalTestCase;
use function uniqid;
class PrePersistEventTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
EntityWithUnmappedEntity::class,
EntityWithCascadeAssociation::class,
);
}
public function testCallingPersistInPrePersistHook(): void
{
$entityWithUnmapped = new EntityWithUnmappedEntity();
$entityWithCascade = new EntityWithCascadeAssociation();
$entityWithUnmapped->unmapped = $entityWithCascade;
$entityWithCascade->cascaded = $entityWithUnmapped;
$this->_em->getEventManager()->addEventListener(Events::prePersist, new PrePersistUnmappedPersistListener());
$this->_em->persist($entityWithUnmapped);
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithCascade));
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithUnmapped));
}
}
class PrePersistUnmappedPersistListener
{
public function prePersist(PrePersistEventArgs $args): void
{
$object = $args->getObject();
if ($object instanceof EntityWithUnmappedEntity) {
$uow = $args->getObjectManager()->getUnitOfWork();
if ($object->unmapped && ! $uow->isInIdentityMap($object->unmapped) && ! $uow->isScheduledForInsert($object->unmapped)) {
$args->getObjectManager()->persist($object->unmapped);
}
}
}
}
#[Entity]
class EntityWithUnmappedEntity
{
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public string $id;
public EntityWithCascadeAssociation|null $unmapped = null;
public function __construct()
{
$this->id = uniqid(self::class, true);
}
}
#[Entity]
class EntityWithCascadeAssociation
{
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public string $id;
#[ManyToOne(targetEntity: EntityWithUnmappedEntity::class, cascade: ['persist'])]
public EntityWithUnmappedEntity|null $cascaded = null;
public function __construct()
{
$this->id = uniqid(self::class, true);
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Tests\Models\Cache\Country;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use ReflectionProperty;
use function array_diff;
use function array_filter;
use function file_exists;
use function rmdir;
use function scandir;
use function strpos;
use function sys_get_temp_dir;
use const DIRECTORY_SEPARATOR;
/** @phpstan-type SupportedCacheUsage 0|ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE|ClassMetadata::CACHE_USAGE_READ_WRITE */
#[Group('DDC-2183')]
class SecondLevelCacheCountQueriesTest extends SecondLevelCacheFunctionalTestCase
{
/** @var string */
private $tmpDir;
protected function tearDown(): void
{
if ($this->tmpDir !== null && file_exists($this->tmpDir)) {
foreach (array_diff(scandir($this->tmpDir), ['.', '..']) as $f) {
rmdir($this->tmpDir . DIRECTORY_SEPARATOR . $f);
}
rmdir($this->tmpDir);
}
parent::tearDown();
}
/** @param SupportedCacheUsage $cacheUsage */
private function setupCountryModel(int $cacheUsage): void
{
$metadata = $this->_em->getClassMetaData(Country::class);
if ($cacheUsage === 0) {
$metadataCacheReflection = new ReflectionProperty(ClassMetadata::class, 'cache');
$metadataCacheReflection->setAccessible(true);
$metadataCacheReflection->setValue($metadata, null);
return;
}
if ($cacheUsage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
$this->tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::class;
$this->secondLevelCacheFactory->setFileLockRegionDirectory($this->tmpDir);
}
$metadata->enableCache(['usage' => $cacheUsage]);
}
private function loadFixturesCountriesWithoutPostInsertIdentifier(): void
{
$metadata = $this->_em->getClassMetaData(Country::class);
$metadata->setIdGenerator(new AssignedGenerator());
$c1 = new Country('Brazil');
$c1->setId(10);
$c2 = new Country('Germany');
$c2->setId(20);
$this->countries[] = $c1;
$this->countries[] = $c2;
$this->_em->persist($c1);
$this->_em->persist($c2);
$this->_em->flush();
}
/** @param 'INSERT'|'UPDATE'|'DELETE' $type */
private function assertQueryCountByType(string $type, int $expectedCount): void
{
$queries = array_filter($this->getQueryLog()->queries, static function (array $entry) use ($type): bool {
return strpos($entry['sql'], $type) === 0;
});
self::assertCount($expectedCount, $queries);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testInsertWithPostInsertIdentifier(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
self::assertQueryCountByType('INSERT', 0);
$this->loadFixturesCountries();
self::assertCount(2, $this->countries);
self::assertQueryCountByType('INSERT', 2);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testInsertWithoutPostInsertIdentifier(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
self::assertQueryCountByType('INSERT', 0);
$this->loadFixturesCountriesWithoutPostInsertIdentifier();
self::assertCount(2, $this->countries);
self::assertQueryCountByType('INSERT', 2);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testDelete(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
$this->loadFixturesCountries();
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
$this->_em->remove($c1);
$this->_em->remove($c2);
$this->_em->flush();
self::assertQueryCountByType('DELETE', 2);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testUpdate(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
$this->loadFixturesCountries();
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
$c1->setName('Czech Republic');
$c2->setName('Hungary');
$this->_em->persist($c1);
$this->_em->persist($c2);
$this->_em->flush();
self::assertQueryCountByType('UPDATE', 2);
}
/** @return list<array{SupportedCacheUsage}> */
public static function cacheUsageProvider(): array
{
return [
[0],
[ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE],
[ClassMetadata::CACHE_USAGE_READ_WRITE],
];
}
}

View File

@@ -66,7 +66,7 @@ class AttributeDriverTest extends MappingDriverTestCase
'name' => 'assoz_id',
'referencedColumnName' => 'assoz_id',
'unique' => false,
'nullable' => true,
'nullable' => null,
'onDelete' => null,
'columnDefinition' => null,
]),
@@ -80,7 +80,7 @@ class AttributeDriverTest extends MappingDriverTestCase
'name' => 'inverse_assoz_id',
'referencedColumnName' => 'inverse_assoz_id',
'unique' => false,
'nullable' => true,
'nullable' => null,
'onDelete' => null,
'columnDefinition' => null,
]),

View File

@@ -364,7 +364,7 @@ class ClassMetadataBuilderTest extends OrmTestCase
[
'name' => 'group_id',
'referencedColumnName' => 'id',
'nullable' => true,
'nullable' => false,
'unique' => false,
'onDelete' => 'CASCADE',
'columnDefinition' => null,
@@ -469,7 +469,7 @@ class ClassMetadataBuilderTest extends OrmTestCase
[
'name' => 'group_id',
'referencedColumnName' => 'id',
'nullable' => true,
'nullable' => false,
'unique' => false,
'onDelete' => 'CASCADE',
'columnDefinition' => null,
@@ -539,7 +539,7 @@ class ClassMetadataBuilderTest extends OrmTestCase
[
'name' => 'group_id',
'referencedColumnName' => 'id',
'nullable' => true,
'nullable' => false,
'unique' => false,
'onDelete' => 'CASCADE',
'columnDefinition' => null,
@@ -551,7 +551,7 @@ class ClassMetadataBuilderTest extends OrmTestCase
[
'name' => 'user_id',
'referencedColumnName' => 'id',
'nullable' => true,
'nullable' => false,
'unique' => false,
'onDelete' => null,
'columnDefinition' => null,
@@ -742,7 +742,7 @@ class ClassMetadataBuilderTest extends OrmTestCase
0 => [
'name' => 'group_id',
'referencedColumnName' => 'id',
'nullable' => true,
'nullable' => false,
'unique' => false,
'onDelete' => 'CASCADE',
'columnDefinition' => null,

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
use Doctrine\ORM\Mapping\JoinTableMapping;
use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use function assert;
@@ -35,4 +37,57 @@ final class ManyToManyOwningSideMappingTest extends TestCase
self::assertSame(['foo' => 'bar'], $resurrectedMapping->relationToSourceKeyColumns);
self::assertSame(['bar' => 'baz'], $resurrectedMapping->relationToTargetKeyColumns);
}
#[DataProvider('mappingsProvider')]
public function testNullableDefaults(bool $expectedValue, ManyToManyOwningSideMapping $mapping): void
{
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
self::assertSame($expectedValue, $joinColumn->nullable);
}
}
/** @return iterable<string, array{bool, ManyToManyOwningSideMapping}> */
public static function mappingsProvider(): iterable
{
$namingStrategy = new DefaultNamingStrategy();
yield 'defaults to false' => [
false,
ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinTable' => [
'name' => 'bar',
'joinColumns' => [
['name' => 'bar_id', 'referencedColumnName' => 'id'],
],
'inverseJoinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id'],
],
],
], $namingStrategy),
];
yield 'explicitly marked as nullable' => [
false, // user's intent is ignored at the ORM level
ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinTable' => [
'name' => 'bar',
'joinColumns' => [
['name' => 'bar_id', 'referencedColumnName' => 'id', 'nullable' => true],
],
'inverseJoinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => true],
],
],
'id' => true,
], $namingStrategy),
];
}
}

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
use Doctrine\ORM\Mapping\JoinColumnMapping;
use Doctrine\ORM\Mapping\ManyToOneAssociationMapping;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use function assert;
@@ -35,4 +37,60 @@ final class ManyToOneAssociationMappingTest extends TestCase
self::assertSame(['foo' => 'bar'], $resurrectedMapping->sourceToTargetKeyColumns);
self::assertSame(['bar' => 'foo'], $resurrectedMapping->targetToSourceKeyColumns);
}
#[DataProvider('mappingsProvider')]
public function testNullableDefaults(bool $expectedValue, ManyToOneAssociationMapping $mapping): void
{
foreach ($mapping->joinColumns as $joinColumn) {
self::assertSame($expectedValue, $joinColumn->nullable);
}
}
/** @return iterable<string, array{bool, ManyToOneAssociationMapping}> */
public static function mappingsProvider(): iterable
{
$namingStrategy = new DefaultNamingStrategy();
yield 'not part of the identifier' => [
true,
ManyToOneAssociationMapping::fromMappingArrayAndName([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id'],
],
'id' => false,
], $namingStrategy, self::class, null, false),
];
yield 'part of the identifier' => [
false,
ManyToOneAssociationMapping::fromMappingArrayAndName([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id'],
],
'id' => true,
], $namingStrategy, self::class, null, false),
];
yield 'part of the identifier, but explicitly marked as nullable' => [
false, // user's intent is ignored at the ORM level
ManyToOneAssociationMapping::fromMappingArrayAndName([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => true],
],
'id' => true,
], $namingStrategy, self::class, null, false),
];
}
}

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
use Doctrine\ORM\Mapping\JoinColumnMapping;
use Doctrine\ORM\Mapping\OneToOneOwningSideMapping;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use function assert;
@@ -35,4 +37,60 @@ final class OneToOneOwningSideMappingTest extends TestCase
self::assertSame(['foo' => 'bar'], $resurrectedMapping->sourceToTargetKeyColumns);
self::assertSame(['bar' => 'foo'], $resurrectedMapping->targetToSourceKeyColumns);
}
#[DataProvider('mappingsProvider')]
public function testNullableDefaults(bool $expectedValue, OneToOneOwningSideMapping $mapping): void
{
foreach ($mapping->joinColumns as $joinColumn) {
self::assertSame($expectedValue, $joinColumn->nullable);
}
}
/** @return iterable<string, array{bool, OneToOneOwningSideMapping}> */
public static function mappingsProvider(): iterable
{
$namingStrategy = new DefaultNamingStrategy();
yield 'not part of the identifier' => [
true,
OneToOneOwningSideMapping::fromMappingArrayAndName([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id'],
],
'id' => false,
], $namingStrategy, self::class, null, false),
];
yield 'part of the identifier' => [
false,
OneToOneOwningSideMapping::fromMappingArrayAndName([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id'],
],
'id' => true,
], $namingStrategy, self::class, null, false),
];
yield 'part of the identifier, but explicitly marked as nullable' => [
false, // user's intent ignored at the ORM level
OneToOneOwningSideMapping::fromMappingArrayAndName([
'fieldName' => 'foo',
'sourceEntity' => self::class,
'targetEntity' => self::class,
'isOwningSide' => true,
'joinColumns' => [
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => true],
],
'id' => true,
], $namingStrategy, self::class, null, false),
];
}
}

View File

@@ -452,6 +452,8 @@ class SelectSqlGenerationTest extends OrmTestCase
public function testSupportsTrimFunction(): void
{
$this->entityManager = $this->createTestEntityManagerWithPlatform(new MySQLPlatform());
$this->assertSqlGeneration(
"SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE TRIM(TRAILING ' ' FROM u.name) = 'someone'",
"SELECT c0_.name AS name_0 FROM cms_users c0_ WHERE TRIM(TRAILING ' ' FROM c0_.name) = 'someone'",
@@ -461,6 +463,8 @@ class SelectSqlGenerationTest extends OrmTestCase
#[Group('DDC-2668')]
public function testSupportsTrimLeadingZeroString(): void
{
$this->entityManager = $this->createTestEntityManagerWithPlatform(new MySQLPlatform());
$this->assertSqlGeneration(
"SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE TRIM(TRAILING '0' FROM u.name) != ''",
"SELECT c0_.name AS name_0 FROM cms_users c0_ WHERE TRIM(TRAILING '0' FROM c0_.name) <> ''",
@@ -584,6 +588,8 @@ class SelectSqlGenerationTest extends OrmTestCase
#[Group('DDC-1802')]
public function testSupportsNotInExpressionForModFunction(): void
{
$this->entityManager = $this->createTestEntityManagerWithPlatform(new MySQLPlatform());
$this->assertSqlGeneration(
'SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE MOD(u.id, 5) NOT IN(1,3,4)',
'SELECT c0_.name AS name_0 FROM cms_users c0_ WHERE MOD(c0_.id, 5) NOT IN (1, 3, 4)',
@@ -1925,6 +1931,8 @@ SQL,
#[Group('DDC-2268')]
public function testCaseThenFunction(): void
{
$this->entityManager = $this->createTestEntityManagerWithPlatform(new PostgreSQLPlatform());
$this->assertSqlGeneration(
'SELECT CASE WHEN LENGTH(u.name) <> 0 THEN CONCAT(u.id, u.name) ELSE u.id END AS name FROM Doctrine\Tests\Models\CMS\CmsUser u',
'SELECT CASE WHEN LENGTH(c0_.name) <> 0 THEN c0_.id || c0_.name ELSE c0_.id END AS sclr_0 FROM cms_users c0_',

View File

@@ -8,9 +8,7 @@ use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Name\UnquotedIdentifierFolding;
use Doctrine\DBAL\Schema\SchemaConfig;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Cache\CacheFactory;
use Doctrine\ORM\Cache\DefaultCacheFactory;
@@ -22,11 +20,14 @@ use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use function enum_exists;
use function class_exists;
use function method_exists;
use function realpath;
use function sprintf;
// DBAL 3 compatibility
class_exists('Doctrine\\DBAL\\Platforms\\SqlitePlatform');
/**
* Base testcase class for all ORM testcases.
*/
@@ -71,9 +72,7 @@ abstract class OrmTestCase extends TestCase
*/
protected function getTestEntityManager(): EntityManagerMock
{
return $this->buildTestEntityManagerWithPlatform(
$this->createConnectionMock($this->createPlatformMock()),
);
return $this->createTestEntityManagerWithPlatform(new SQLitePlatform());
}
protected function createTestEntityManagerWithConnection(Connection $connection): EntityManagerMock
@@ -153,24 +152,6 @@ abstract class OrmTestCase extends TestCase
return $connection;
}
private function createPlatformMock(): AbstractPlatform
{
$schemaManager = $this->createMock(AbstractSchemaManager::class);
$schemaManager->method('createSchemaConfig')
->willReturn(new SchemaConfig());
$platform = $this->getMockBuilder(AbstractPlatform::class)
->setConstructorArgs(enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : [])
->onlyMethods(['supportsIdentityColumns', 'createSchemaManager'])
->getMockForAbstractClass();
$platform->method('supportsIdentityColumns')
->willReturn(true);
$platform->method('createSchemaManager')
->willReturn($schemaManager);
return $platform;
}
private function createDriverMock(AbstractPlatform $platform): Driver
{
$result = $this->createMock(Result::class);