-
-
Notifications
You must be signed in to change notification settings - Fork 504
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was originally confused as to the value of It's a shame we can't use more meaningful labels for these things. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
|
||
\$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference); | ||
\$identifier = ClassMetadata::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']); | ||
\$targetMetadata = \$this->dm->getClassMetadata(\$className); | ||
|
@@ -257,7 +262,8 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla | |
EOF | ||
, | ||
$mapping['name'], | ||
$mapping['fieldName'] | ||
$mapping['fieldName'], | ||
$class->getName() | ||
); | ||
} elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) { | ||
if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) { | ||
|
@@ -305,6 +311,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla | |
|
||
/** @Many */ | ||
\$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null; | ||
|
||
if (\$mongoData !== null && ! is_array(\$mongoData)) { | ||
throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$mongoData)); | ||
} | ||
|
||
\$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']); | ||
\$return->setHints(\$hints); | ||
\$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']); | ||
|
@@ -318,7 +329,8 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla | |
EOF | ||
, | ||
$mapping['name'], | ||
$mapping['fieldName'] | ||
$mapping['fieldName'], | ||
$class->getName() | ||
); | ||
} elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) { | ||
$code .= sprintf( | ||
|
@@ -327,6 +339,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla | |
/** @EmbedOne */ | ||
if (isset(\$data['%1\$s'])) { | ||
\$embeddedDocument = \$data['%1\$s']; | ||
|
||
if (! is_array(\$embeddedDocument)) { | ||
throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$embeddedDocument)); | ||
} | ||
|
||
\$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument); | ||
\$embeddedMetadata = \$this->dm->getClassMetadata(\$className); | ||
\$return = \$embeddedMetadata->newInstance(); | ||
|
@@ -347,7 +364,8 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla | |
EOF | ||
, | ||
$mapping['name'], | ||
$mapping['fieldName'] | ||
$mapping['fieldName'], | ||
$class->getName() | ||
); | ||
} | ||
} | ||
|
@@ -360,6 +378,7 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla | |
namespace $namespace; | ||
|
||
use Doctrine\ODM\MongoDB\DocumentManager; | ||
use Doctrine\ODM\MongoDB\Hydrator\HydratorException; | ||
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface; | ||
use Doctrine\ODM\MongoDB\Query\Query; | ||
use Doctrine\ODM\MongoDB\UnitOfWork; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
use DateTime; | ||
use Doctrine\Common\Persistence\Mapping\MappingException; | ||
use Doctrine\ODM\MongoDB\DocumentManager; | ||
use Doctrine\ODM\MongoDB\Hydrator\HydratorException; | ||
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory; | ||
use Doctrine\ODM\MongoDB\Iterator\CachingIterator; | ||
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator; | ||
|
@@ -48,6 +49,7 @@ | |
use function explode; | ||
use function get_class; | ||
use function get_object_vars; | ||
use function gettype; | ||
use function implode; | ||
use function in_array; | ||
use function is_array; | ||
|
@@ -664,15 +666,24 @@ private function loadEmbedManyCollection(PersistentCollectionInterface $collecti | |
$embeddedDocuments = $collection->getMongoData(); | ||
$mapping = $collection->getMapping(); | ||
$owner = $collection->getOwner(); | ||
|
||
if (! $embeddedDocuments) { | ||
return; | ||
} | ||
|
||
if ($owner === null) { | ||
throw PersistentCollectionException::ownerRequiredToLoadCollection(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This exception already existed but just wasn't used here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
|
||
foreach ($embeddedDocuments as $key => $embeddedDocument) { | ||
$className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument); | ||
$embeddedMetadata = $this->dm->getClassMetadata($className); | ||
$embeddedDocumentObject = $embeddedMetadata->newInstance(); | ||
|
||
if (! is_array($embeddedDocument)) { | ||
throw HydratorException::associationItemTypeMismatch(get_class($owner), $mapping['name'], $key, 'array', gettype($embeddedDocument)); | ||
} | ||
|
||
$this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key); | ||
|
||
$data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints()); | ||
|
@@ -693,12 +704,22 @@ private function loadReferenceManyCollectionOwningSide(PersistentCollectionInter | |
{ | ||
$hints = $collection->getHints(); | ||
$mapping = $collection->getMapping(); | ||
$owner = $collection->getOwner(); | ||
$groupedIds = []; | ||
|
||
if ($owner === null) { | ||
throw PersistentCollectionException::ownerRequiredToLoadCollection(); | ||
} | ||
|
||
$sorted = isset($mapping['sort']) && $mapping['sort']; | ||
|
||
foreach ($collection->getMongoData() as $key => $reference) { | ||
$className = $this->uow->getClassNameForAssociation($mapping, $reference); | ||
$className = $this->uow->getClassNameForAssociation($mapping, $reference); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noting this is just a whitespace change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep - assignment operator alignment strikes again. See doctrine/coding-standard#107. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah, I totally forgot about that PR |
||
|
||
if ($mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array($reference)) { | ||
throw HydratorException::associationItemTypeMismatch(get_class($owner), $mapping['name'], $key, 'array', gettype($reference)); | ||
} | ||
|
||
$identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']); | ||
$id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,8 +5,10 @@ | |
namespace Doctrine\ODM\MongoDB\Tests; | ||
|
||
use DateTime; | ||
use Doctrine\ODM\MongoDB\Hydrator\HydratorException; | ||
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; | ||
use Doctrine\ODM\MongoDB\PersistentCollection; | ||
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; | ||
use Doctrine\ODM\MongoDB\Query\Query; | ||
use ProxyManager\Proxy\GhostObjectInterface; | ||
|
||
|
@@ -65,6 +67,92 @@ public function testReadOnly() | |
$this->assertFalse($this->uow->isInIdentityMap($user->embedOne)); | ||
$this->assertFalse($this->uow->isInIdentityMap($user->embedMany[0])); | ||
} | ||
|
||
public function testEmbedOneWithWrongType() | ||
{ | ||
$user = new HydrationClosureUser(); | ||
|
||
$this->expectException(HydratorException::class); | ||
$this->expectExceptionMessage('Expected association for field "embedOne" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); | ||
|
||
$this->dm->getHydratorFactory()->hydrate($user, [ | ||
'_id' => 1, | ||
'embedOne' => 'jon', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Glad to see we're still using @jwage as a data fixture. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The beauty of copy/paste 😅 |
||
]); | ||
} | ||
|
||
public function testEmbedManyWithWrongType() | ||
{ | ||
$user = new HydrationClosureUser(); | ||
|
||
$this->expectException(HydratorException::class); | ||
$this->expectExceptionMessage('Expected association for field "embedMany" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); | ||
|
||
$this->dm->getHydratorFactory()->hydrate($user, [ | ||
'_id' => 1, | ||
'embedMany' => 'jon', | ||
]); | ||
} | ||
|
||
public function testEmbedManyWithWrongElementType() | ||
{ | ||
$user = new HydrationClosureUser(); | ||
|
||
$this->dm->getHydratorFactory()->hydrate($user, [ | ||
'_id' => 1, | ||
'embedMany' => ['jon'], | ||
]); | ||
|
||
$this->assertInstanceOf(PersistentCollectionInterface::class, $user->embedMany); | ||
|
||
$this->expectException(HydratorException::class); | ||
$this->expectExceptionMessage('Expected association item with key "0" for field "embedMany" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); | ||
|
||
$user->embedMany->initialize(); | ||
} | ||
|
||
public function testReferenceOneWithWrongType() | ||
{ | ||
$user = new HydrationClosureUser(); | ||
|
||
$this->expectException(HydratorException::class); | ||
$this->expectExceptionMessage('Expected association for field "referenceOne" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); | ||
|
||
$this->dm->getHydratorFactory()->hydrate($user, [ | ||
'_id' => 1, | ||
'referenceOne' => 'jon', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we have no tests for such scenarios There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opened #2077 to track this. |
||
]); | ||
} | ||
|
||
public function testReferenceManyWithWrongType() | ||
{ | ||
$user = new HydrationClosureUser(); | ||
|
||
$this->expectException(HydratorException::class); | ||
$this->expectExceptionMessage('Expected association for field "referenceMany" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); | ||
|
||
$this->dm->getHydratorFactory()->hydrate($user, [ | ||
'_id' => 1, | ||
'referenceMany' => 'jon', | ||
]); | ||
} | ||
|
||
public function testReferenceManyWithWrongElementType() | ||
{ | ||
$user = new HydrationClosureUser(); | ||
|
||
$this->dm->getHydratorFactory()->hydrate($user, [ | ||
'_id' => 1, | ||
'referenceMany' => ['jon'], | ||
]); | ||
|
||
$this->assertInstanceOf(PersistentCollectionInterface::class, $user->referenceMany); | ||
|
||
$this->expectException(HydratorException::class); | ||
$this->expectExceptionMessage('Expected association item with key "0" for field "referenceMany" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); | ||
|
||
$user->referenceMany->initialize(); | ||
} | ||
} | ||
|
||
/** @ODM\Document */ | ||
|
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 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?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.
Correct :)