Skip to content

PHPC-2083: Allow enums to be instantiated during BSON decoding #1317

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

Merged
merged 5 commits into from
Aug 26, 2022

Conversation

jmikola
Copy link
Member

@jmikola jmikola commented May 5, 2022

@jmikola jmikola mentioned this pull request Jun 7, 2022
@jmikola jmikola marked this pull request as ready for review July 27, 2022 16:36
@jmikola jmikola requested a review from alcaeus July 27, 2022 16:36
}

?>
===DONE===
Copy link
Member Author

Choose a reason for hiding this comment

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

When executed with TEST_PHP_ARGS=-m, both bson-enum-002.phpt and bson-enum-003.phpt fail the following assertion:

php: /tmp/build_php-8.1.8.FWj/php-8.1.8/Zend/zend_objects_API.c:195: zend_objects_store_del: Assertion `(!(((zend_uintptr_t)((executor_globals.objects_store).object_buckets[handle])) & (1<<0)))' failed.
Aborted

Termsig=6

The corresponding .mem file reports many leaks (none include phongo_bson.c in the stack trace), but I believe those are just because of the abort failure immediately terminating the process without allowing PHP to clean up internally allocated memory.

The assertion failure corresponds to this line in LXR.

Copy link
Member Author

Choose a reason for hiding this comment

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

I spent some time investigating this and realized it was caused by using ZVAL_COPY_VALUE instead of ZVAL_COPY on the resolved enum case. These are discussed in more detail in Copying Zvals in the internals book.

ZVAL_COPY_VALUE was previously used on the return value from bsonSerialize(), which made sense there since we didn't need to increment the ref count (the value was being moved). But for enums, this left the original enum case in a bad state, which triggered the assertion failure in zend_objects_store_del(). ZVAL_COPY correctly increments the case's refcount, which allows all tests to pass w/o leaks.

@jmikola jmikola changed the title PHPC-2083: Allow enum types to implement Persistable PHPC-2083: Allow enums to be instantiated during BSON decoding Jul 27, 2022
Copy link
Member

@alcaeus alcaeus left a comment

Choose a reason for hiding this comment

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

Logic looks good, I do have a couple of questions around making this easier to use. Requiring Unserializable isn't really necessary IMO, and a trait to ease implementing enums that implement Persistable would reduce the number of foot-guns we provide to users.

Comment on lines 37 to 45
public function bsonSerialize(): array
{
return (array) $this;
}

public function bsonUnserialize(array $data)
{
printf("%s called with: %s\n", __METHOD__, json_encode($data));
}
Copy link
Member

Choose a reason for hiding this comment

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

We might want to provide a PersistableEnum trait to reduce copy-pasta and make this less error-prone for users:

enum MyBackedEnum: string implements Persistable
{
    use PersistableEnum;

    case foo = 'bar';
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I see a few options, which may not be mutually exclusive:

  • Allow an enum to be specified in a typeMap without requiring it to implement Unserializable (since we know the method is irrelevant). In this case, I suppose we'd still invoke bsonUnserialize() if it is available on the enum, just to preserve expected behavior for BSON decoding. Since enforcement of Unserializable happens elsewhere, this will require some additional code changes.
  • Create an UnserializableEnum trait
  • Create a PersistableEnum trait

Presumably, the traits would exist in PHPC.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a PersistableEnum trait, which includes a bsonSerialize() method that returns the enum object cast as an array and a bsonUnserialize() method that does nothing. Presumably, this can be used just fine with an enum implementing any combination of Serializable, Unserializable, and/or Persistable.

If users implement Persistable, the encoded BSON is going to include __pclass; otherwise, they'll need to use a typemap and implement Unserializable (as demonstrated in the tests). We'd want to document both approaches, and I suppose the PersistableEnum docs would be a good place to do that.

I don't think there's any reason to create UnserializableEnum, as it'd just be a subset of PersistableEnum with no bsonSerialize() method. And if users actually want a different implementation for bsonSerialize() (we're well in edge case territory here), they can avoid the trait and just roll their own implementations.

Lastly, I looked into whether we could restrict use of this trait to enum objects (e.g. throw an exception if a non-enum imported the trait) but found no easy way to do so after digging through zend_compile_use_trait() in PHP core. I expect this would be possible with some compiler hacking, but I have no idea how to do that and it'd probably be a waste of @sgolemon's time for relatively little benefit.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with only providing PersistableEnum - also agree with not restricting the use to traits. No matter how much effort you'll put in it, people will always find a way to shoot themselves in the foot.

goto cleanup;
}

ZVAL_COPY_VALUE(&obj, enum_case);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering what you expect to happen in the bsonUnserialize call for a typical unserializable enum case. Since these are essentially singleton instances any side effects will be essentially global.

Copy link
Member Author

@jmikola jmikola Jul 28, 2022

Choose a reason for hiding this comment

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

I mentioned this in one of the test file comments, but there's really no practical purpose for bsonUnserialize. Since enums have no state, there's nothing that would be initialized. We're just invoking it because of the existing contract. The example implementation I used in the test file was to log a message that bsonUnserialize was called.

I think it sucks that we need to require enums to implement an interface, but I'd be more concerned about implicitly encoding their class name (i.e. pretending they're Persistable w/o the explicit implementation). The trait that @alcaeus suggested is probably the best compromise.

Beyond that, I don't think an ODM like Doctrine would even care about this. Enums would never be used as root documents and if they appeared as a field within a modeled class, that class itself could just re-instantiate the enum (e.g. calling BackedEnum::tryFrom() with the value property, or reading name and fetching the case constant directly).

Copy link
Member Author

Choose a reason for hiding this comment

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

FYI: a PersistableEnum trait with a no-op bsonUnserialize() method has been created.

jmikola and others added 3 commits August 24, 2022 17:24
Enums serialize like PHP objects with a "name" property (BackedEnum instance will also have a "value") and thus become BSON documents. In order for a document to unserialize back to an enum, it must implement either Unserializable or Persistable. The bsonUnserialize() method serves no purpose for initialization, but it will still be invoked.
Co-authored-by: Sara Golemon <pollita@php.net>
@jmikola jmikola requested a review from alcaeus August 25, 2022 00:52
Copy link
Member

@alcaeus alcaeus left a comment

Choose a reason for hiding this comment

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

LGTM!


namespace MongoDB\BSON;

trait PersistableEnum
Copy link
Member

Choose a reason for hiding this comment

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

Seeing these changes makes me appreciate the work people put into gen_stub.php for PHP core - it's just so much easier to define classes now than it was before when everything had to be done manually...

Copy link
Member Author

Choose a reason for hiding this comment

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

@jmikola jmikola merged commit de5f1e5 into mongodb:master Aug 26, 2022
@jmikola jmikola deleted the phpc-2083 branch August 26, 2022 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants