Skip to content

Commit bcfc1c4

Browse files
authored
Error if references are not stored correctly for lookups (#2465)
* Error if references are not stored correctly for lookups * Fix tests * Throw repositoryMethodLookupNotAllowed exception and only allow id ref
1 parent e10d658 commit bcfc1c4

File tree

15 files changed

+127
-17
lines changed

15 files changed

+127
-17
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ jobs:
195195
name: Run Behat tests
196196
command: |-
197197
mkdir -p build/logs/tmp build/cov
198-
for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' | circleci tests split --split-by=timings); do
198+
for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' -not -path 'features/mongodb/*' | circleci tests split --split-by=timings); do
199199
_f=${f//\//_}
200200
FEATURE="${_f}" phpdbg -qrr vendor/bin/behat --profile=coverage --suite=default --format=progress --out=std --format=junit --out=build/logs/tmp/"${_f}" "$f"
201201
done

behat.yml.dist

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ default:
1818
- 'Behat\MinkExtension\Context\MinkContext'
1919
- 'Behatch\Context\RestContext'
2020
filters:
21-
tags: '~@postgres&&~@elasticsearch'
21+
tags: '~@postgres&&~@mongodb&&~@elasticsearch'
2222
postgres:
2323
contexts:
2424
- 'DoctrineContext':
@@ -37,7 +37,7 @@ default:
3737
- 'Behat\MinkExtension\Context\MinkContext'
3838
- 'Behatch\Context\RestContext'
3939
filters:
40-
tags: '~@sqlite&&~@elasticsearch'
40+
tags: '~@sqlite&&~@mongodb&&~@elasticsearch'
4141
mongodb:
4242
contexts:
4343
- 'DoctrineContext':
@@ -106,4 +106,4 @@ coverage:
106106
- 'Behat\MinkExtension\Context\MinkContext'
107107
- 'Behatch\Context\RestContext'
108108
filters:
109-
tags: '~@postgres&&~@elasticsearch'
109+
tags: '~@postgres&&~@mongodb&&~@elasticsearch'

features/doctrine/date_filter.feature

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ Feature: Date filter on collections
404404
},
405405
"hydra:search": {
406406
"@type": "hydra:IriTemplate",
407-
"hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],properties[]}",
407+
"hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],properties[]}",
408408
"hydra:variableRepresentation": "BasicRepresentation",
409409
"hydra:mapping": [
410410
{
@@ -701,6 +701,30 @@ Feature: Date filter on collections
701701
"property": "relatedDummy.thirdLevel.fourthLevel.level",
702702
"required": false
703703
},
704+
{
705+
"@type": "IriTemplateMapping",
706+
"variable": "relatedDummy.thirdLevel.badFourthLevel.level",
707+
"property": "relatedDummy.thirdLevel.badFourthLevel.level",
708+
"required": false
709+
},
710+
{
711+
"@type": "IriTemplateMapping",
712+
"variable": "relatedDummy.thirdLevel.badFourthLevel.level[]",
713+
"property": "relatedDummy.thirdLevel.badFourthLevel.level",
714+
"required": false
715+
},
716+
{
717+
"@type": "IriTemplateMapping",
718+
"variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level",
719+
"property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level",
720+
"required": false
721+
},
722+
{
723+
"@type": "IriTemplateMapping",
724+
"variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[]",
725+
"property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level",
726+
"required": false
727+
},
704728
{
705729
"@type": "IriTemplateMapping",
706730
"variable": "properties[]",

features/main/crud.feature

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Feature: Create-Retrieve-Update-Delete
44
I need to be able to retrieve, create, update and delete JSON-LD encoded resources.
55

66
@createSchema
7-
@mongodb
87
Scenario: Create a resource
98
When I add "Content-Type" header equal to "application/ld+json"
109
And I send a "POST" request to "/dummies" with body:
@@ -56,7 +55,6 @@ Feature: Create-Retrieve-Update-Delete
5655
}
5756
"""
5857

59-
@mongodb
6058
Scenario: Get a resource
6159
When I send a "GET" request to "/dummies/1"
6260
Then the response status code should be 200
@@ -93,7 +91,6 @@ Feature: Create-Retrieve-Update-Delete
9391
}
9492
"""
9593

96-
@mongodb
9794
Scenario: Get a not found exception
9895
When I send a "GET" request to "/dummies/42"
9996
Then the response status code should be 404
@@ -140,7 +137,7 @@ Feature: Create-Retrieve-Update-Delete
140137
"hydra:totalItems": 1,
141138
"hydra:search": {
142139
"@type": "hydra:IriTemplate",
143-
"hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],properties[]}",
140+
"hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],properties[]}",
144141
"hydra:variableRepresentation": "BasicRepresentation",
145142
"hydra:mapping": [
146143
{
@@ -437,6 +434,30 @@ Feature: Create-Retrieve-Update-Delete
437434
"property": "relatedDummy.thirdLevel.fourthLevel.level",
438435
"required": false
439436
},
437+
{
438+
"@type": "IriTemplateMapping",
439+
"variable": "relatedDummy.thirdLevel.badFourthLevel.level",
440+
"property": "relatedDummy.thirdLevel.badFourthLevel.level",
441+
"required": false
442+
},
443+
{
444+
"@type": "IriTemplateMapping",
445+
"variable": "relatedDummy.thirdLevel.badFourthLevel.level[]",
446+
"property": "relatedDummy.thirdLevel.badFourthLevel.level",
447+
"required": false
448+
},
449+
{
450+
"@type": "IriTemplateMapping",
451+
"variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level",
452+
"property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level",
453+
"required": false
454+
},
455+
{
456+
"@type": "IriTemplateMapping",
457+
"variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[]",
458+
"property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level",
459+
"required": false
460+
},
440461
{
441462
"@type": "IriTemplateMapping",
442463
"variable": "properties[]",
@@ -448,7 +469,6 @@ Feature: Create-Retrieve-Update-Delete
448469
}
449470
"""
450471

451-
@mongodb
452472
Scenario: Update a resource
453473
When I add "Content-Type" header equal to "application/ld+json"
454474
And I send a "PUT" request to "/dummies/1" with body:
@@ -502,7 +522,6 @@ Feature: Create-Retrieve-Update-Delete
502522
}
503523
"""
504524

505-
@mongodb
506525
Scenario: Update a resource with empty body
507526
When I add "Content-Type" header equal to "application/ld+json"
508527
And I send a "PUT" request to "/dummies/1"
@@ -543,7 +562,6 @@ Feature: Create-Retrieve-Update-Delete
543562
}
544563
"""
545564

546-
@mongodb
547565
Scenario: Delete a resource
548566
When I send a "DELETE" request to "/dummies/1"
549567
Then the response status code should be 204

features/main/relation.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Feature: Relations support
2020
"@id": "/third_levels/1",
2121
"@type": "ThirdLevel",
2222
"fourthLevel": null,
23+
"badFourthLevel": null,
2324
"id": 1,
2425
"level": 3,
2526
"test": true

features/main/subresource.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ Feature: Subresource support
245245
"@id": "/third_levels/1",
246246
"@type": "ThirdLevel",
247247
"fourthLevel": "/fourth_levels/1",
248+
"badFourthLevel": null,
248249
"id": 1,
249250
"level": 3,
250251
"test": true
@@ -262,6 +263,7 @@ Feature: Subresource support
262263
"@context": "/contexts/FourthLevel",
263264
"@id": "/fourth_levels/1",
264265
"@type": "FourthLevel",
266+
"badThirdLevel": [],
265267
"id": 1,
266268
"level": 4
267269
}

features/mongodb/filters.feature

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
@mongodb
2+
Feature: Filters on collections
3+
In order to retrieve large collections of resources
4+
As a client software developer
5+
I need to retrieve collections with filters
6+
7+
@createSchema
8+
Scenario: Error when getting collection with nested properties if references are not correctly stored (owning side)
9+
Given there is a dummy object with a fourth level relation
10+
When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.badFourthLevel.level=4"
11+
Then the response status code should be 500
12+
And the response should be in JSON
13+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
14+
And the JSON node "@context" should be equal to "/contexts/Error"
15+
And the JSON node "@type" should be equal to "hydra:Error"
16+
And the JSON node "hydra:title" should be equal to "An error occurred"
17+
And the JSON node "hydra:description" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported."
18+
And the JSON node "trace" should exist
19+
20+
Scenario: Error when getting collection with nested properties if references are not correctly stored (not owning side)
21+
When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level=3"
22+
Then the response status code should be 500
23+
And the response should be in JSON
24+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
25+
And the JSON node "@context" should be equal to "/contexts/Error"
26+
And the JSON node "@type" should be equal to "hydra:Error"
27+
And the JSON node "hydra:title" should be equal to "An error occurred"
28+
And the JSON node "hydra:description" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported."
29+
And the JSON node "trace" should exist

src/Bridge/Doctrine/MongoDbOdm/PropertyHelperTrait.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
1818
use Doctrine\ODM\MongoDB\Aggregation\Builder;
1919
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as MongoDbOdmClassMetadata;
20+
use Doctrine\ODM\MongoDB\Mapping\MappingException;
2021

2122
/**
2223
* Helper trait regarding a property in a MongoDB document using the resource metadata.
@@ -41,6 +42,7 @@ abstract protected function getClassMetadata(string $resourceClass): ClassMetada
4142
* Adds the necessary lookups for a nested property.
4243
*
4344
* @throws InvalidArgumentException If property is not nested
45+
* @throws MappingException
4446
*
4547
* @return array An array where the first element is the $alias of the lookup,
4648
* the second element is the $field name
@@ -66,9 +68,23 @@ protected function addLookupsForNestedProperty(string $property, Builder $aggreg
6668
$alias .= $propertyAlias;
6769
$referenceMapping = $classMetadata->getFieldMapping($association);
6870

71+
if (($isOwningSide = $referenceMapping['isOwningSide']) && MongoDbOdmClassMetadata::REFERENCE_STORE_AS_ID !== $referenceMapping['storeAs']) {
72+
throw MappingException::cannotLookupDbRefReference($classMetadata->getReflectionClass()->getShortName(), $association);
73+
}
74+
if (!$isOwningSide) {
75+
if (isset($referenceMapping['repositoryMethod']) || !isset($referenceMapping['mappedBy'])) {
76+
throw MappingException::repositoryMethodLookupNotAllowed($classMetadata->getReflectionClass()->getShortName(), $association);
77+
}
78+
79+
$targetClassMetadata = $this->getClassMetadata($referenceMapping['targetDocument']);
80+
if ($targetClassMetadata instanceof MongoDbOdmClassMetadata && MongoDbOdmClassMetadata::REFERENCE_STORE_AS_ID !== $targetClassMetadata->getFieldMapping($referenceMapping['mappedBy'])['storeAs']) {
81+
throw MappingException::cannotLookupDbRefReference($classMetadata->getReflectionClass()->getShortName(), $association);
82+
}
83+
}
84+
6985
$aggregationBuilder->lookup($classMetadata->getAssociationTargetClass($association))
70-
->localField($referenceMapping['isOwningSide'] ? $localField : '_id')
71-
->foreignField($referenceMapping['isOwningSide'] ? '_id' : $referenceMapping['mappedBy'])
86+
->localField($isOwningSide ? $localField : '_id')
87+
->foreignField($isOwningSide ? '_id' : $referenceMapping['mappedBy'])
7288
->alias($alias);
7389
$aggregationBuilder->unwind("\$$alias");
7490

tests/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtensionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation()
135135
$classMetadataProphecy->hasAssociation('name')->shouldBeCalled()->willReturn(false);
136136
$classMetadataProphecy->getAssociationTargetClass('author')->shouldBeCalled()->willReturn(Dummy::class);
137137
$classMetadataProphecy->hasReference('author')->shouldBeCalled()->willReturn(true);
138-
$classMetadataProphecy->getFieldMapping('author')->shouldBeCalled()->willReturn(['isOwningSide' => true]);
138+
$classMetadataProphecy->getFieldMapping('author')->shouldBeCalled()->willReturn(['isOwningSide' => true, 'storeAs' => ClassMetadata::REFERENCE_STORE_AS_ID]);
139139

140140
$resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadata(null, null, null, null, null, ['order' => ['author.name']]));
141141

tests/Fixtures/TestBundle/Document/FourthLevel.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ class FourthLevel
4242
*/
4343
private $level = 4;
4444

45+
/**
46+
* @ODM\ReferenceMany(targetDocument=ThirdLevel::class, cascade={"persist"}, mappedBy="badFourthLevel", storeAs="id")
47+
*/
48+
public $badThirdLevel;
49+
4550
/**
4651
* @return int
4752
*/

0 commit comments

Comments
 (0)