mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a541b8b3a | ||
|
|
9fb9cc46e4 | ||
|
|
c322c71cd4 | ||
|
|
3c733a2fee | ||
|
|
5984ad586a | ||
|
|
ee2c3a506b | ||
|
|
781ed30926 | ||
|
|
04694a9f7b | ||
|
|
257c5094c4 | ||
|
|
66e0e92816 | ||
|
|
5b2060e25f | ||
|
|
39e35fc06c | ||
|
|
7f061c3870 | ||
|
|
74495711fb | ||
|
|
97a7cb8d2f | ||
|
|
e0052390e1 | ||
|
|
8c6419e0e0 | ||
|
|
6f5ce1aca2 | ||
|
|
98e7a53b42 | ||
|
|
3aaaf37dfb | ||
|
|
154a4652ee | ||
|
|
ae7489ff19 | ||
|
|
528b8837e1 | ||
|
|
4fb044d5f6 | ||
|
|
2a953c5e2b | ||
|
|
abc6a40ccb | ||
|
|
73e68f3c7d | ||
|
|
73777d0bd4 | ||
|
|
e89b58a13f | ||
|
|
2b94ec18b9 | ||
|
|
2a662149f4 |
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
93
tests/Tests/ORM/Functional/PrePersistEventTest.php
Normal file
93
tests/Tests/ORM/Functional/PrePersistEventTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
167
tests/Tests/ORM/Functional/SecondLevelCacheCountQueriesTest.php
Normal file
167
tests/Tests/ORM/Functional/SecondLevelCacheCountQueriesTest.php
Normal 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],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_',
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user