Skip to content
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

Allow using enums directly in queries #2447

Merged
merged 1 commit into from
May 12, 2022

Conversation

IonBazan
Copy link
Member

@IonBazan IonBazan commented May 10, 2022

Q A
Type bug/improvement
BC Break no

Summary

This change allows using BackedEnums directly in the query builder instead of using ->value:

// Before:
$this->createQueryBuilder()->field('status')->equals(Status::APPROVED->value)->getQuery();

// Now
$this->createQueryBuilder()->field('status')->equals(Status::APPROVED)->getQuery();

This supports arrays too. Please do note that if enumType is not set, the value is passed directly to the Type without any changes to allow developers to customize the behaviour in their custom types if needed (see testQueryWithMappedNonEnumFieldIsPassedToTypeDirectly() where value is passed directly to StringType and producing an error).

I've added the tests in EnumTest instead of DocumentPersisterTest to avoid conditional mapping loading there (this feature requires PHP 8.1+).

Not sure if this should be treated as bugfix or improvement so feel free to assign to correct milestone.

@IonBazan IonBazan requested review from jmikola, franmomu and malarzm May 10, 2022 09:16
@IonBazan IonBazan self-assigned this May 10, 2022
Copy link
Member

@malarzm malarzm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do note that if enumType is not set, the value is passed directly to the Type without any changes

Thanks to this I'm inclined to release this PR as a bugfix :instead of hacing people wait until next minor 🎉 If anybody asks let's say we thought it'll work out of the box 🙈

@@ -1144,6 +1149,10 @@ private function convertToDatabaseValue(string $fieldName, $value)
);
}

if ($value instanceof BackedEnum && $mapping['enumType']) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given $mapping['enumType'] can be either null or a backed enum's FQCN (guaranteed by metadata validation) we should be OK to do following check (maybe backed by an \assert call to satisfy SA) which should be cheaper

Suggested change
if ($value instanceof BackedEnum && $mapping['enumType']) {
if (isset($mapping['enumType'])) {

Copy link
Member

@malarzm malarzm May 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I'm not sure about the call being here instead of after next if statement. Actually I started wondering if we shouldn't ensure that enumType can be combined with type=int|string (or not set which allows autoconfiguration to kick in). Right now

/** @ODM\Field(type="collection", enumType=Suit::class) */
private array $enums = [];

looks OK from user's perspective but I believe it'll blow up?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This $value instanceof BackedEnum is necessary because the value might be the enum's backed value too:

$this->dm->createQueryBuilder(Card::class)
            ->field('suit')->equals(Suit::Spades->value)
            ->field('nullableSuit')->equals('C');

I'll add a test for that.

Regarding mixing enumType with arrays, it will probably blow up if you map it this way. Not sure how could we solve it here though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how could we solve it here though.

Not necessarily here, I'll hack a PR later today to throw a mapping exception if enumType is combined with something else than int or string types. Then we'll have a clear "it's not working message" for userland instead of weird blow ups under the hood.

@jmikola
Copy link
Member

jmikola commented May 11, 2022

Is this PR in any way related to the driver? We recently had a bug report come in about using enums in conjunction with the driver's Persistable interface. While that was mainly an issue with unserializing BSON into an enum, I wonder if there's also another problem with how enums are serialized to BSON (ignoring anything to do with Persistable).

I'm tracking this enum/Persistable issue in PHPC-2083 and have started working on a PR.

I haven't actually tested standard BSON serialization for enums, but based on this issue it seems like they get encoded as BSON documents with name field. And for BackedEnum instances, there's would be an additional value field. According to the docs for BackedEnum, that type is intended for cases where an enum does need to get stored in a database (since a pure enum wouldn't have a scalar value).

Would it be preferable for the driver to raise an exception when the user attempts to serialize a pure enum (without Persistable) and encode a BackedEnum as its value (i.e. BSON integer or string)?

@malarzm malarzm added this to the 2.4.2 milestone May 11, 2022
@malarzm malarzm added Bug and removed Enhancement labels May 11, 2022
@malarzm
Copy link
Member

malarzm commented May 11, 2022

Is this PR in any way related to the driver?

No, we ended with our own (ok ok, ORM's 🙄) way of handling enums (#2412): user must either specify an enumClass property in the mapping or use an enum as property's type and have ODM figure out correct mapping (#2411). Under the hood we use a special reflection with which we get enum's backing value and then treat it as a normal string/int. That's also what is stored in the database

Would it be preferable for the driver to raise an exception when the user attempts to serialize a pure enum (without Persistable)

With ODM we've decided to deal only with BackedEnums, using pure one ends with a mapping exception

and encode a BackedEnum as its value (i.e. BSON integer or string)?

Encoding any backed enum as its backing value would save us some headaches that we are yet to discover, maybe they'd render this PR not needed as well. But is the behaviour safe from driver's perspective? I mean we're cool saving just the value because we have a mapping to know what enum instatiate later. I guess same could be done with an explicit typeMap

@IonBazan IonBazan force-pushed the bugfix/enum-queries branch 2 times, most recently from 21f0c9e to 9c9f90f Compare May 11, 2022 08:49
@malarzm
Copy link
Member

malarzm commented May 11, 2022

@IonBazan please squash commits ;)

@IonBazan IonBazan force-pushed the bugfix/enum-queries branch from 9c9f90f to 237766a Compare May 11, 2022 15:09
@malarzm malarzm merged commit 9066f03 into doctrine:2.4.x May 12, 2022
@malarzm
Copy link
Member

malarzm commented May 12, 2022

Thanks @IonBazan!

@jmikola
Copy link
Member

jmikola commented May 13, 2022

Encoding any backed enum as its backing value would save us some headaches that we are yet to discover, maybe they'd render this PR not needed as well. But is the behaviour safe from driver's perspective?

@IonBazan: I haven't worked enough with enums to say with any certainty, but the documentation does say that backed enums exist to allow for database round-tripping. I assume that is alluding to ORM logic that would re-construct an enum from mapping information given a value (using the from() or tryFrom() methods).

IMO, allowing a pure or backed enum to be serialized to BSON is preferable from a driver perspective (assuming users opt in via the Persistable interface), since it's less for the application or an ORM to worry about; however, that does invite possible issues converting BSON to PHP if the document data references a class that no longer exists in the application. Enums just introduce some additional ways that code changes could break if a name or value is no longer defined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants