-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure consistent original data with enums #10088
Ensure consistent original data with enums #10088
Conversation
I think this is the last piece of the puzzle to have consistent treating of enums inside the internals. The changes are compatible with the last PR https://github.com/doctrine/orm/pull/10058/files merged into 2.13.3, Id like to merge this into the next 2.13 point release together with #10074 @derrabus |
Can you please rebase on 2.13.x? I'd like to see that this PR does not break with the changes of #10074. |
8022b52
to
b41f5ec
Compare
@derrabus Done EDIT: |
@@ -347,15 +349,10 @@ public function testEnumWithNonMatchingDatabaseValueThrowsException(string $card | |||
[$metadata->fieldMappings['id']['columnName'] => $card->id] | |||
); | |||
|
|||
$this->expectException(MappingException::class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to change the expectation here? This could qualify as a breaking change if calling code expects a MappingException
in this scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@derrabus Before the change the string was converted to an enum in the ReflectionEnumProperty
class
orm/lib/Doctrine/ORM/Mapping/ReflectionEnumProperty.php
Lines 88 to 107 in 1ed0057
private function initializeEnumValue($object, $value): BackedEnum | |
{ | |
if ($value instanceof BackedEnum) { | |
return $value; | |
} | |
$enumType = $this->enumType; | |
try { | |
return $enumType::from($value); | |
} catch (ValueError $e) { | |
throw MappingException::invalidEnumValue( | |
get_class($object), | |
$this->originalReflectionProperty->getName(), | |
(string) $value, | |
$enumType, | |
$e | |
); | |
} | |
} |
Now it's converted earlier in the
AbstractHydrator
class insteadorm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php
Lines 700 to 709 in 1ed0057
private function buildEnum($value, string $enumType) | |
{ | |
if (is_array($value)) { | |
return array_map(static function ($value) use ($enumType): BackedEnum { | |
return $enumType::from($value); | |
}, $value); | |
} | |
return $enumType::from($value); | |
} |
Since the AbstractHydrator
class throws a different exception the test fails. I'm not sure if this inconsistency in exceptions is a bug itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HypeMC we should wrap with the same try-catch block and throw MappingException I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HypeMC can you please incorporate this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I've already started, unfortunately it's not as trivial as I thought. The current exception message contains the class name, which is currently not always available. I'll try to finish the PR in the following days.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replace
orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php
Lines 143 to 145 in b41f5ec
if ($value !== null && isset($cacheKeyInfo['enumType'])) { | |
$value = $this->buildEnum($value, $cacheKeyInfo['enumType']); | |
} |
with
if ($value !== null && isset($cacheKeyInfo['enumType'])) {
try {
$value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
} catch (ValueError $e) {
throw MappingException::invalidEnumValue(
$entityName,
$cacheKeyInfo['fieldName'],
(string) $value,
$cacheKeyInfo['enumType'],
$e
);
}
}
One issue though - the AbstractHydrator::hydrateRowData()
throws only HydrationException
, so throwing MappingException
from here will result in psalm errors. @derrabus what do you suggest, should we update the @throws
PHPDoc or should we wrap the Mapping exception into HydrationException (this by the same logic could be considered BC break)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michnovka Good point, I wanted to change the buildEnum
method to throw a MappingException
, but that would actually also be a BC break, so this makes the most sense.
b41f5ec
to
3480c5d
Compare
What's the status of this PR? It's not clear to me what should be done to get this merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think its OK now
@derrabus can we add this to the 2.13.4 milestone? Thanks |
Can we write a functional test that reproduces the problem we're attempting to fix here? |
@derrabus Well this really does not fix any actual functional bug, its just inconsistency in how originalEntityData is populated. Some hydrators store enums, some store strings (enum values). See original post - #10074 (comment)
With this fix, all hydrators store originalEntityData enum fields as native enums. |
So it's really just about the internal state of the persisters? Why's that so important to you that you want to push it into a bugfix release? |
well we introduced this inconsistency with other enum-related PRs that were part of 2.13.x ( #10041 and #10058 ) and this issue did have functional impact prior to #10074 . So I wanted to close this last piece of related issues. TBH I did think it does have functional implication as I didnt debug myself the actual effects of #10074 on this issue. Its clear now it doesnt, but still worth a merge into 2.13.x for the above mentioned reasons |
It has functionnal implications, our application does not work with 2.13.2 & 2.13.3 due do enum related bugs. For instance this issue will cause false positive changeset (same logical enum value but different type). |
@NoiseByNorthwest Would you be able to provide a functional test that reproduces the issue your application has? |
@NoiseByNorthwest changeset computations are fixed by #10074 which is planned for 2.13.4 release |
I have a FooEntity type with a (many-to-one) relationship to a BarEntity type. The BarEntity type has an enum property. When I create a FooEntity from an existing BarEntity, the resolved changeset will consider the BarEntity as updated since its enum property will be wrongly considered as changed. That causes, among other annoyances, the BarEntity::updatedAt property to be updated. |
People are starting to ask me an ETA for the next release. Should I release 2.13.4 now, or should we wait for somebody to transform @NoiseByNorthwest 's summary above into a test? |
@greg0ire I can take a look at it today. I'll let you know how it goes. |
I have (eventually) a test-case for you #10245 :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commit message consists in a title.
This is fine for trivial changes, but here, to understand this change, one has to have prior knowledge of whatever was discussed in #10074, which itself mentions #10071, which is unclear to me… #10073 is a bit more clear though.
Long story short, I don't want to have to read 3 issues and 4 PRs before deciding whether to merge this so if you want this merged, you'll have to:
- create a crystal clear commit message explaining what is broken, and why your changes fix the current situation
- write a test that shows the current bug using high level APIs, not only plumbing APIs
I will release without this for now.
$hydrator = new SimpleObjectHydrator($this->entityManager); | ||
$result = $hydrator->hydrateAll($stmt, $rsm)[0]; | ||
|
||
self::assertEquals(Suit::Clubs, $result->suit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assertion passes without your changes.
$result = $hydrator->hydrateAll($stmt, $rsm)[0]; | ||
|
||
self::assertEquals(Suit::Clubs, $result->suit); | ||
self::assertEquals(Suit::Clubs, $this->entityManager->getUnitOfWork()->getOriginalEntityData($result)['suit']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one doesn't, but it uses the API of UnitOfWork, which users are not really suppose to deal with. This makes it hard for me to understand where you're driving at with this test.
/** | ||
* @requires PHP 8.1 | ||
*/ | ||
public function testEnumsAreBuilt(): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what this method name means.
{ | ||
$statement = $this->persister->getSelectSQL([]); | ||
self::assertEquals('SELECT t0.id AS id_1, t0.suit AS suit_2 FROM Card t0', $statement); | ||
self::assertEquals(['suit_2' => Suit::class], $this->persister->getResultSetMapping()->enumMappings); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, who uses that API directly? I don't understand what you are fixing here…
Ok so remove the tests, and target 2.14.x, since this is more likely to break stuff than to fix it. |
@@ -697,7 +697,7 @@ protected function registerManaged(ClassMetadata $class, $entity, array $data) | |||
* | |||
* @return BackedEnum|array<BackedEnum> | |||
*/ | |||
private function buildEnum($value, string $enumType) | |||
protected function buildEnum($value, string $enumType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are going to expose this to other classes, let's at least make it final
protected function buildEnum($value, string $enumType) | |
final protected function buildEnum($value, string $enumType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Please reuse whatever you see fit as a commit message body. I will read that. |
Ensure consistent |
I would really get rid of the tests @greg0ire , you are right it makes no sense since only "plumbing" APIs are affected |
If there was a test to be added, it would be in |
whats the decision here, do we remove the tests? |
I'd say either remove them or promote them to |
I'll take a look to see what can be done. |
fb85326
3480c5d
to
fb85326
Compare
Previously different hydrators would store the original data for enum fields in different ways, the SimpleObjectHydrator would keep them as strings while other hydrators would convert then to native php enums. This would make the data in the internal UnitOfWork::$originalEntityData array inconsistent which could've caused problems in the long run. Now, all hydrators convert enum fields to native php enums ensuring the original data is always consistent regardless of the hydrator used.
fb85326
to
9d5ab4c
Compare
@greg0ire I've removed the tests as I didn't see a way to promote them to the I've also switched the target branch to 2.14.x & updated the commit message. |
If we keep the test and somebody changes other hydrators, we would also reintroduce an inconsistency, that's why I recommended we either promote this test as an abstract test or remove it. It gives a false sense of security on top of being hard to understand. |
This was not merged into 2.14.0. However I really dont think this is a new feature, as it is only changing internal states. Do we need to retarget 2.15.x or can we merge into 2.14.1? |
Seems fair, let's merge as is. |
As mentioned in #10074, the
SimpleObjectHydrator
doesn't convert strings into enums, while theObjectHydrator
does, causing inconsistent values in theUnitOfWork::$originalEntityData
array. This PR aims to fix that.