Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 11 additions & 1 deletion src/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ public function computeChangeSet(ClassMetadata $class, object $entity): void
$this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
}

$isNew = ! isset($this->originalEntityData[$oid]);
$actualData = [];

foreach ($class->propertyAccessors as $name => $refProp) {
Expand Down Expand Up @@ -626,18 +627,26 @@ public function computeChangeSet(ClassMetadata $class, object $entity): void
}

if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
if (! $isNew && isset($class->fieldMappings[$name]->notUpdatable)) {
continue;
}

$actualData[$name] = $value;
}
}

if (! isset($this->originalEntityData[$oid])) {
if ($isNew) {
// Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
// These result in an INSERT.
$this->originalEntityData[$oid] = $actualData;
$changeSet = [];

foreach ($actualData as $propName => $actualValue) {
if (! isset($class->associationMappings[$propName])) {
if (isset($class->fieldMappings[$propName]->notInsertable)) {
continue;
}

$changeSet[$propName] = [null, $actualValue];

continue;
Expand Down Expand Up @@ -990,6 +999,7 @@ public function recomputeSingleEntityChangeSet(ClassMetadata $class, object $ent
( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
&& ($name !== $class->versionField)
&& ! $class->isCollectionValuedAssociation($name)
&& ! isset($class->fieldMappings[$name]->notUpdatable)
) {
$actualData[$name] = $refProp->getValue($entity);
}
Expand Down
125 changes: 125 additions & 0 deletions tests/Tests/ORM/Functional/Ticket/GH12017ChangeSetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;

class GH12017ChangeSetTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(GH12017ChangeSetNonUpdatableEntity::class, GH12017ChangeSetNonInsertableEntity::class);
}

public function testNonInsertableFieldShouldNotAppearInChangeSetOnInsert(): void
{
$entity = new GH12017ChangeSetNonInsertableEntity();

$this->_em->persist($entity);
$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

$changeSet = $uow->getEntityChangeSet($entity);
self::assertArrayHasKey('other', $changeSet);
self::assertArrayNotHasKey('tested', $changeSet, 'non-insertable field should not appear in change set on insert');
}

public function testNonInsertableFieldShouldAppearInChangeSetOnUpdate(): void
{
$entity = new GH12017ChangeSetNonInsertableEntity();
$this->_em->persist($entity);
$this->_em->flush();

$entity->tested = new DateTimeImmutable();
$entity->other = 1;

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

$changeSet = $uow->getEntityChangeSet($entity);
self::assertArrayHasKey('other', $changeSet);
self::assertArrayHasKey('tested', $changeSet, 'Non-insertable field should still appear in change set on update');
}

public function testNonUpdatableFieldShouldAppearInChangeSetOnInsert(): void
{
$entity = new GH12017ChangeSetNonUpdatableEntity();
$this->_em->persist($entity);

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

$changeSet = $uow->getEntityChangeSet($entity);
self::assertArrayHasKey('other', $changeSet);
self::assertArrayHasKey('tested', $changeSet, 'Non-updatable field should still appear in change set on insert');
}

public function testNonUpdatableFieldShouldNotAppearInChangeSetOnUpdate(): void
{
$entity = new GH12017ChangeSetNonUpdatableEntity();
$this->_em->persist($entity);
$this->_em->flush();

$entity->tested = new DateTimeImmutable();
$entity->other = 1;

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

$changeSet = $uow->getEntityChangeSet($entity);

self::assertArrayHasKey('other', $changeSet);
self::assertArrayNotHasKey('tested', $changeSet, 'Non-updatable field should not appear in change set on update');
}
}

#[ORM\Entity]
#[ORM\Table(name: 'gh12017_changeset_non_insertable')]
class GH12017ChangeSetNonInsertableEntity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int|null $id = null;

#[ORM\Column(
type: Types::DATETIME_IMMUTABLE,
nullable: true,
insertable: false,
updatable: true,
options: ['default' => 'CURRENT_TIMESTAMP'],
)]
public DateTimeImmutable|null $tested = null;

#[ORM\Column(type: Types::INTEGER)]
public int $other = 0;
}

#[ORM\Entity]
#[ORM\Table(name: 'gh12017_changeset_non_updatable')]
class GH12017ChangeSetNonUpdatableEntity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int|null $id = null;

#[ORM\Column(
type: Types::DATETIME_IMMUTABLE,
nullable: true,
insertable: true,
updatable: false,
options: ['default' => 'CURRENT_TIMESTAMP'],
)]
public DateTimeImmutable|null $tested = null;

#[ORM\Column(type: Types::INTEGER)]
public int $other = 0;
}
106 changes: 106 additions & 0 deletions tests/Tests/ORM/Functional/Ticket/GH12017SubClassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\DiscriminatorColumn;
use Doctrine\ORM\Mapping\DiscriminatorMap;
use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\Tests\OrmFunctionalTestCase;

class GH12017SubClassTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(GH12017ParentEntity::class, GH12017ChildEntity::class);
}

public function testGeneratedFieldFromChildShouldNotBeDetectedAsChangeAfterInsert(): void
{
$entity = new GH12017ChildEntity();

$this->_em->persist($entity);
$this->_em->flush();

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

self::assertFalse(
$uow->isScheduledForUpdate($entity),
'Entity should not be scheduled for update after a generated field was refreshed from the DB',
);
}

public function testGeneratedStringFieldShouldNotBeDetectedAsChangeAfterInsert(): void
{
$entity = new GH12017ChildEntity();
$this->_em->persist($entity);
$this->_em->flush();

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

self::assertFalse(
$uow->isScheduledForUpdate($entity),
'Entity should not be scheduled for update after a generated string field was refreshed from the DB',
);
}

public function testGeneratedFieldShouldNotBeDetectedAsChangeAfterUpdate(): void
{
$entity = new GH12017ChildEntity();
$this->_em->persist($entity);
$this->_em->flush();

$entity->other = 1;
$this->_em->flush();

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();
self::assertFalse(
$uow->isScheduledForUpdate($entity),
'Entity should not be scheduled for update after a generated field was refreshed from the DB',
);
}
}

#[ORM\MappedSuperclass]
#[InheritanceType('JOINED')]
#[DiscriminatorColumn(name: 'discr', type: 'string')]
#[DiscriminatorMap(['child' => GH12017ChildEntity::class])]
#[ORM\Entity]
#[ORM\Table(name: 'gh12017_parent')]
class GH12017ParentEntity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int $id;

#[ORM\Column(
type: Types::INTEGER,
nullable: false,
options: ['default' => 0],
)]
public int $other = 0;
}

#[ORM\Entity]
#[ORM\Table(name: 'gh12017_child')]
class GH12017ChildEntity extends GH12017ParentEntity
{
#[ORM\Column(
type: Types::DATETIME_IMMUTABLE,
insertable: false,
updatable: false,
options: ['default' => 'CURRENT_TIMESTAMP'],
generated: 'ALWAYS',
)]
public DateTimeImmutable|null $tested = null;
}
118 changes: 118 additions & 0 deletions tests/Tests/ORM/Functional/Ticket/GH12017Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;

class GH12017Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(GH12017Entity::class, GH12017EntityWithStringField::class);
}

public function testGeneratedFieldShouldNotBeDetectedAsChangeAfterFlush(): void
{
$entity = new GH12017Entity();

$this->_em->persist($entity);
$this->_em->flush();

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

self::assertFalse(
$uow->isScheduledForUpdate($entity),
'Entity should not be scheduled for update after a generated field was refreshed from the DB',
);
}

public function testGeneratedStringFieldShouldNotBeDetectedAsChangeAfterFlush(): void
{
$entity = new GH12017EntityWithStringField();

$this->_em->persist($entity);
$this->_em->flush();

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();

self::assertFalse(
$uow->isScheduledForUpdate($entity),
'Entity should not be scheduled for update after a generated string field was refreshed from the DB',
);
}

public function testGeneratedFieldShouldNotBeDetectedAsChangeAfterUpdate(): void
{
$entity = new GH12017Entity();

$this->_em->persist($entity);
$this->_em->flush();
$this->_em->clear();

$entity = $this->_em->find(GH12017Entity::class, $entity->id);
$entity->other = 1;
$this->_em->flush();

$uow = $this->_em->getUnitOfWork();
$uow->computeChangeSets();
self::assertFalse(
$uow->isScheduledForUpdate($entity),
'Entity should not be scheduled for update after a generated field was refreshed from the DB',
);
}
}

#[ORM\Entity]
#[ORM\Table(name: 'gh12017')]
class GH12017Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int|null $id = null;

#[ORM\Column(
type: Types::DATETIME_IMMUTABLE,
nullable: false,
insertable: false,
updatable: false,
options: ['default' => 'CURRENT_TIMESTAMP'],
generated: 'ALWAYS',
)]
public DateTimeImmutable|null $tested = null;

#[ORM\Column(
type: Types::INTEGER,
nullable: false,
options: ['default' => 0],
)]
public int $other = 0;
}

#[ORM\Entity]
#[ORM\Table(name: 'gh12017_string')]
class GH12017EntityWithStringField
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int|null $id = null;

#[ORM\Column(
type: Types::STRING,
insertable: false,
updatable: false,
options: ['default' => 'generated'],
generated: 'ALWAYS',
)]
public string|null $tested = null;
}
Loading