Skip to content

Commit 8bdf829

Browse files
committed
Merge pull request #552 from soyuka/bug-reproduction-identifiers
Fix identifiers behavior bugs
2 parents aea75ab + 96fd131 commit 8bdf829

File tree

9 files changed

+350
-16
lines changed

9 files changed

+350
-16
lines changed

features/relation.feature

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ Feature: Relations support
2323
}
2424
"""
2525

26+
Scenario: Create a dummy friend
27+
When I send a "POST" request to "/dummy_friends" with body:
28+
"""
29+
{"name": "Zoidberg"}
30+
"""
31+
Then the response status code should be 201
32+
And the response should be in JSON
33+
And the header "Content-Type" should be equal to "application/ld+json"
34+
And the JSON should be equal to:
35+
"""
36+
{
37+
"@context": "/contexts/DummyFriend",
38+
"@id": "/dummy_friends/1",
39+
"@type": "DummyFriend",
40+
"name": "Zoidberg"
41+
}
42+
"""
43+
2644
Scenario: Create a related dummy
2745
When I send a "POST" request to "/related_dummies" with body:
2846
"""
@@ -42,12 +60,61 @@ Feature: Relations support
4260
"name": null,
4361
"dummyDate": null,
4462
"thirdLevel": "/third_levels/1",
63+
"relatedToDummyFriend": null,
4564
"dummyBoolean": null,
4665
"symfony": "symfony",
4766
"age": null
4867
}
4968
"""
5069

70+
Scenario: Create a friend relationship
71+
When I send a "POST" request to "/related_to_dummy_friends" with body:
72+
"""
73+
{
74+
"name": "Friends relation",
75+
"dummyFriend": "/dummy_friends/1",
76+
"relatedDummy": "/related_dummies/1"
77+
}
78+
"""
79+
Then the response status code should be 201
80+
And the response should be in JSON
81+
And the header "Content-Type" should be equal to "application/ld+json"
82+
And the JSON should be equal to:
83+
"""
84+
{
85+
"@context": "/contexts/RelatedToDummyFriend",
86+
"@id": "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1",
87+
"@type": "RelatedToDummyFriend",
88+
"name": "Friends relation",
89+
"dummyFriend": {
90+
"@id": "/dummy_friends/1",
91+
"@type": "DummyFriend",
92+
"name": "Zoidberg"
93+
}
94+
}
95+
"""
96+
97+
Scenario: Get the relationship
98+
When I send a "GET" request to "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1"
99+
And the response status code should be 200
100+
And the response should be in JSON
101+
And the header "Content-Type" should be equal to "application/ld+json"
102+
And the JSON should be equal to:
103+
"""
104+
{
105+
"@context": "/contexts/RelatedToDummyFriend",
106+
"@id": "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1",
107+
"@type": "RelatedToDummyFriend",
108+
"name": "Friends relation",
109+
"dummyFriend": {
110+
"@id": "/dummy_friends/1",
111+
"@type": "DummyFriend",
112+
"name": "Zoidberg"
113+
}
114+
}
115+
"""
116+
117+
51118
Scenario: Create a dummy with relations
52119
When I send a "POST" request to "/dummies" with body:
53120
"""

src/Bridge/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public function create(string $resourceClass, string $property, array $options =
5656
foreach ($identifiers as $identifier) {
5757
if ($identifier === $property) {
5858
$propertyMetadata = $propertyMetadata->withIdentifier(true);
59-
$propertyMetadata = $propertyMetadata->withReadable(false);
6059
$propertyMetadata = $propertyMetadata->withWritable($doctrineClassMetadata->isIdentifierNatural());
6160

6261
break;

src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464

6565
<!-- Metadata loader -->
6666

67-
<service id="api_platform.doctrine.orm.metadata.property.metadata_factory" class="ApiPlatform\Core\Bridge\Doctrine\Orm\Metadata\Property\DoctrineOrmPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="10" public="false">
67+
<service id="api_platform.doctrine.orm.metadata.property.metadata_factory" class="ApiPlatform\Core\Bridge\Doctrine\Orm\Metadata\Property\DoctrineOrmPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="40" public="false">
6868
<argument type="service" id="doctrine" />
6969
<argument type="service" id="api_platform.doctrine.orm.metadata.property.metadata_factory.inner" />
7070
</service>

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,25 +77,75 @@ public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface
7777
$resourceClass = $this->getObjectClass($item);
7878
$routeName = $this->getRouteName($resourceClass, false);
7979

80+
$identifiers = $this->generateIdentifiersUrl($this->getIdentifiersFromItem($item));
81+
82+
return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType);
83+
}
84+
85+
/**
86+
* Generate the identifier url.
87+
*
88+
* @param array $identifiers
89+
*
90+
* @return array
91+
*/
92+
public function generateIdentifiersUrl(array $identifiers) : array
93+
{
94+
if (1 === count($identifiers)) {
95+
return [rawurlencode(array_values($identifiers)[0])];
96+
}
97+
98+
foreach ($identifiers as $name => $value) {
99+
$identifiers[$name] = sprintf('%s=%s', $name, $value);
100+
}
101+
102+
return $identifiers;
103+
}
104+
105+
/**
106+
* Find identifiers from an Item (Object).
107+
*
108+
* @param object $item
109+
*
110+
* @throws RuntimeException
111+
*
112+
* @return array
113+
*/
114+
private function getIdentifiersFromItem($item) : array
115+
{
116+
$identifiers = [];
117+
$resourceClass = $this->getObjectClass($item);
118+
80119
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
81120
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
82121

83-
if ($propertyMetadata->isIdentifier()) {
84-
$identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
122+
if (!$propertyMetadata->isIdentifier()) {
123+
continue;
85124
}
86-
}
87125

88-
if (1 === count($identifiers)) {
89-
$identifiers = array_map(function ($identifierValue) {
90-
return rawurlencode($identifierValue);
91-
}, $identifiers);
92-
} else {
93-
$identifiers = array_map(function ($identifierName, $identifierValue) {
94-
return sprintf('%s=%s', $identifierName, rawurlencode($identifierValue));
95-
}, array_keys($identifiers), $identifiers);
126+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
127+
128+
if (!is_object($identifiers[$propertyName])) {
129+
continue;
130+
}
131+
132+
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
133+
$relatedItem = $identifiers[$propertyName];
134+
135+
foreach ($this->propertyNameCollectionFactory->create($relatedResourceClass) as $relatedPropertyName) {
136+
$propertyMetadata = $this->propertyMetadataFactory->create($relatedResourceClass, $relatedPropertyName);
137+
138+
if ($propertyMetadata->isIdentifier()) {
139+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($relatedItem, $relatedPropertyName);
140+
}
141+
}
142+
143+
if (empty($identifiers[$propertyName])) {
144+
throw new \RuntimeException(sprintf('%s identifiers can not be found', $resourceClass));
145+
}
96146
}
97147

98-
return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType);
148+
return $identifiers;
99149
}
100150

101151
/**

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
193193
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options);
194194

195195
if (
196-
(isset($context['jsonld_normalize']) && !$propertyMetadata->isIdentifier() && $propertyMetadata->isReadable()) ||
196+
(isset($context['jsonld_normalize']) && $propertyMetadata->isReadable()) ||
197197
(isset($context['jsonld_denormalize']) && $propertyMetadata->isWritable())
198198
) {
199199
$allowedAttributes[] = $propertyName;

src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ private function transformReadWrite(PropertyMetadata $propertyMetadata, string $
6868
{
6969
$groups = $this->getPropertySerializerGroups($resourceClass, $property);
7070

71-
if (false !== $propertyMetadata->isReadable()) {
71+
if ($propertyMetadata->isIdentifier()) {
72+
$propertyMetadata = $propertyMetadata->withReadable(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $groups)));
73+
} elseif (false !== $propertyMetadata->isReadable()) {
7274
$propertyMetadata = $propertyMetadata->withReadable(null === $normalizationGroups || !empty(array_intersect($normalizationGroups, $groups)));
7375
}
7476
if (false !== $propertyMetadata->isWritable()) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
13+
14+
use ApiPlatform\Core\Annotation\ApiProperty;
15+
use ApiPlatform\Core\Annotation\ApiResource;
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Symfony\Component\Serializer\Annotation\Groups;
18+
use Symfony\Component\Validator\Constraints as Assert;
19+
20+
/**
21+
* DummyFriend.
22+
*
23+
* @author Kévin Dunglas <[email protected]>
24+
*
25+
* @ApiResource()
26+
* @ORM\Entity
27+
*/
28+
class DummyFriend
29+
{
30+
/**
31+
* @var int The id.
32+
*
33+
* @ORM\Column(type="integer")
34+
* @ORM\Id
35+
* @ORM\GeneratedValue(strategy="AUTO")
36+
*/
37+
private $id;
38+
39+
/**
40+
* @var string The dummy name.
41+
*
42+
* @ORM\Column
43+
* @Assert\NotBlank
44+
* @ApiProperty(iri="http://schema.org/name")
45+
* @Groups({"fakemanytomany"})
46+
*/
47+
private $name;
48+
49+
/**
50+
* Get id.
51+
*
52+
* @return id.
53+
*/
54+
public function getId()
55+
{
56+
return $this->id;
57+
}
58+
59+
/**
60+
* Set id.
61+
*
62+
* @param id the value to set.
63+
*/
64+
public function setId($id)
65+
{
66+
$this->id = $id;
67+
}
68+
69+
/**
70+
* Get name.
71+
*
72+
* @return name.
73+
*/
74+
public function getName()
75+
{
76+
return $this->name;
77+
}
78+
79+
/**
80+
* Set name.
81+
*
82+
* @param name the value to set.
83+
*/
84+
public function setName($name)
85+
{
86+
$this->name = $name;
87+
}
88+
}

tests/Fixtures/TestBundle/Entity/RelatedDummy.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class RelatedDummy extends ParentDummy
6060
*/
6161
public $thirdLevel;
6262

63+
/**
64+
* @ORM\OneToMany(targetEntity="RelatedToDummyFriend", cascade={"persist"}, fetch="EAGER", mappedBy="relatedDummy")
65+
* @Groups({"fakemanytomany"})
66+
*/
67+
public $relatedToDummyFriend;
68+
6369
/**
6470
* @var bool A dummy bool.
6571
*
@@ -117,4 +123,24 @@ public function setDummyBoolean($dummyBoolean)
117123
{
118124
$this->dummyBoolean = $dummyBoolean;
119125
}
126+
127+
/**
128+
* Get relatedToDummyFriend.
129+
*
130+
* @return relatedToDummyFriend.
131+
*/
132+
public function getRelatedToDummyFriend()
133+
{
134+
return $this->relatedToDummyFriend;
135+
}
136+
137+
/**
138+
* Set relatedToDummyFriend.
139+
*
140+
* @param relatedToDummyFriend the value to set.
141+
*/
142+
public function setRelatedToDummyFriend(RelatedToDummyFriend $relatedToDummyFriend)
143+
{
144+
$this->relatedToDummyFriend = $relatedToDummyFriend;
145+
}
120146
}

0 commit comments

Comments
 (0)