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

Throw hydrator exceptions when encountering invalid types #2073

Merged
merged 1 commit into from
Oct 2, 2019

Conversation

alcaeus
Copy link
Member

@alcaeus alcaeus commented Oct 1, 2019

Q A
Type bug
BC Break no
Fixed issues #2044

Summary

This adds type checks for associations to the hydrator, to prevent errors when receiving invalid data from the database. For most associations, this resulted in a TypeError being thrown, but for references this could result in invalid offsets when accessing the reference "array".

To fix this problem, there are new HydratorException methods to account for these errors. We print the class name, field name of the offending data, as well as the expected and actual types. Since we are in the middle of hydration, we can't reliably print a full field path (we don't know if this is a root hydration or already within an embedded document), which also means that we can't print an identifier for the document being hydrated. However, the exception message better explains the problem than a PHP error does. More importantly, this makes it look less like a bug in ODM and more like an issue with the data (which it actually is).

@alcaeus alcaeus added the Bug label Oct 1, 2019
@alcaeus alcaeus added this to the 2.0.0-RC3 milestone Oct 1, 2019
@alcaeus alcaeus self-assigned this Oct 1, 2019
@alcaeus alcaeus requested a review from malarzm October 1, 2019 08:34
@@ -245,6 +245,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla
/** @ReferenceOne */
if (isset(\$data['%1\$s'])) {
\$reference = \$data['%1\$s'];

if (\$this->class->fieldMappings['%2\$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array(\$reference)) {
throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$reference));
Copy link
Member

Choose a reason for hiding this comment

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

I was originally confused as to the value of %3\$s, and assumed it was literal PHP code based on the previous usage. Further inspection revealed that this is in a completely separate sprintf() call and it's the class name string, which is also added 18 lines below. 👍

It's a shame we can't use more meaningful labels for these things.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that is very unfortunate. We may be able to improve this using Heredoc, but that's a change for a different release.

@@ -245,6 +245,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla
/** @ReferenceOne */
if (isset(\$data['%1\$s'])) {
\$reference = \$data['%1\$s'];

if (\$this->class->fieldMappings['%2\$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array(\$reference)) {
Copy link
Member

Choose a reason for hiding this comment

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

If I understand correctly, REFERENCE_STORE_AS_ID is the only strategy that stores an arbitrary value. All others nest the reference in an embedded document (either a DBRef or ODM's newer style that omits the $-prefixed field names), correct?

Copy link
Member

Choose a reason for hiding this comment

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

Correct :)

if (! $embeddedDocuments) {
return;
}

if ($owner === null) {
throw PersistentCollectionException::ownerRequiredToLoadCollection();
Copy link
Member

Choose a reason for hiding this comment

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

This exception already existed but just wasn't used here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep. This was introduced whenever we require the owner to be set (PHPStan found a few instances where we assumed it would be set). Since we now need the owner to generate the correct exception message, we have to also throw it here.

$sorted = isset($mapping['sort']) && $mapping['sort'];

foreach ($collection->getMongoData() as $key => $reference) {
$className = $this->uow->getClassNameForAssociation($mapping, $reference);
$className = $this->uow->getClassNameForAssociation($mapping, $reference);
Copy link
Member

Choose a reason for hiding this comment

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

Noting this is just a whitespace change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep - assignment operator alignment strikes again. See doctrine/coding-standard#107.

Copy link
Member

Choose a reason for hiding this comment

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

Oh yeah, I totally forgot about that PR


$this->dm->getHydratorFactory()->hydrate($user, [
'_id' => 1,
'embedOne' => 'jon',
Copy link
Member

Choose a reason for hiding this comment

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

Glad to see we're still using @jwage as a data fixture.

Copy link
Member Author

Choose a reason for hiding this comment

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

The beauty of copy/paste 😅


$this->dm->getHydratorFactory()->hydrate($user, [
'_id' => 1,
'referenceOne' => 'jon',
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, do we bother testing for malformed DBRef or ODM-style reference objects? I realize that's probably out of scope for this PR.

Copy link
Member

Choose a reason for hiding this comment

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

I believe we have no tests for such scenarios

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't have these, but adding them would probably make sense. Although I'd argue that we may want to be less strict when reading these references (e.g., accept ['id' => 'foo', 'collection' => 'foo']) when reading a DBRef object.

Copy link
Member

Choose a reason for hiding this comment

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

On one hand it'd be nice but on the other it could give false impression that changing one storage strategy to another works "just like that" which can bite back when using aggregations

Copy link
Member

Choose a reason for hiding this comment

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

Opened #2077 to track this.

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