Skip to content

Commit 9044d7f

Browse files
authored
Merge pull request #2822 from teohhanhui/fix/serializer-readable-writable-link
Fix serialization when using interface as resource
2 parents 8980c87 + c9d9c58 commit 9044d7f

36 files changed

+994
-245
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ indent_size = 4
3838
indent_style = space
3939
indent_size = 4
4040

41-
[*.yml]
41+
[*.{yaml,yml}]
4242
indent_style = space
4343
indent_size = 4
4444
trim_trailing_whitespace = false

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@
4848
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0",
4949
"phpdocumentor/type-resolver": "^0.3 || ^0.4",
5050
"phpspec/prophecy": "^1.8",
51-
"phpstan/phpstan": "^0.11.3",
52-
"phpstan/phpstan-doctrine": "^0.11.2",
51+
"phpstan/extension-installer": "^1.0",
52+
"phpstan/phpstan": "^0.11",
53+
"phpstan/phpstan-doctrine": "^0.11",
5354
"phpstan/phpstan-phpunit": "^0.11",
54-
"phpstan/phpstan-symfony": "^0.11.2",
55+
"phpstan/phpstan-symfony": "^0.11",
5556
"phpunit/phpunit": "^7.5.2",
5657
"psr/log": "^1.0",
5758
"ramsey/uuid": "^3.7",

features/bootstrap/DoctrineContext.php

Lines changed: 110 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@
4444
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Person as PersonDocument;
4545
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\PersonToPet as PersonToPetDocument;
4646
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Pet as PetDocument;
47+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Product as ProductDocument;
4748
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Question as QuestionDocument;
4849
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
4950
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelatedOwnedDummy as RelatedOwnedDummyDocument;
5051
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelatedOwningDummy as RelatedOwningDummyDocument;
5152
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelatedToDummyFriend as RelatedToDummyFriendDocument;
5253
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelationEmbedder as RelationEmbedderDocument;
5354
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\SecuredDummy as SecuredDummyDocument;
55+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Taxon as TaxonDocument;
5456
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument;
5557
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\User as UserDocument;
5658
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Address;
@@ -78,17 +80,20 @@
7880
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceNotApiResourceChild;
7981
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
8082
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
83+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ExternalUser;
8184
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
8285
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
8386
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FooDummy;
8487
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FourthLevel;
8588
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Greeting;
89+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\InternalUser;
8690
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy;
8791
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node;
8892
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Order;
8993
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Person;
9094
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\PersonToPet;
9195
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Pet;
96+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Product;
9297
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
9398
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RamseyUuidDummy;
9499
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
@@ -97,10 +102,13 @@
97102
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
98103
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelationEmbedder;
99104
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
105+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Site;
106+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Taxon;
100107
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
101108
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User;
102109
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy;
103110
use Behat\Behat\Context\Context;
111+
use Behat\Gherkin\Node\PyStringNode;
104112
use Doctrine\Common\Persistence\ManagerRegistry;
105113
use Doctrine\ODM\MongoDB\DocumentManager;
106114
use Doctrine\ORM\EntityManagerInterface;
@@ -1227,6 +1235,108 @@ public function thereAreNbDummyDtoCustom($nb)
12271235
$this->manager->clear();
12281236
}
12291237

1238+
/**
1239+
* @Given there is an order with same customer and recipient
1240+
*/
1241+
public function thereIsAnOrderWithSameCustomerAndRecipient()
1242+
{
1243+
$customer = $this->isOrm() ? new Customer() : new CustomerDocument();
1244+
$customer->name = 'customer_name';
1245+
1246+
$address1 = $this->isOrm() ? new Address() : new AddressDocument();
1247+
$address1->name = 'foo';
1248+
$address2 = $this->isOrm() ? new Address() : new AddressDocument();
1249+
$address2->name = 'bar';
1250+
1251+
$order = $this->isOrm() ? new Order() : new OrderDocument();
1252+
$order->recipient = $customer;
1253+
$order->customer = $customer;
1254+
1255+
$customer->addresses->add($address1);
1256+
$customer->addresses->add($address2);
1257+
1258+
$this->manager->persist($address1);
1259+
$this->manager->persist($address2);
1260+
$this->manager->persist($customer);
1261+
$this->manager->persist($order);
1262+
1263+
$this->manager->flush();
1264+
$this->manager->clear();
1265+
}
1266+
1267+
/**
1268+
* @Given there are :nb sites with internal owner
1269+
*/
1270+
public function thereAreSitesWithInternalOwner(int $nb)
1271+
{
1272+
for ($i = 1; $i <= $nb; ++$i) {
1273+
$internalUser = new InternalUser();
1274+
$internalUser->setFirstname('Internal');
1275+
$internalUser->setLastname('User');
1276+
$internalUser->setEmail('[email protected]');
1277+
$internalUser->setInternalId('INT');
1278+
$site = new Site();
1279+
$site->setTitle('title');
1280+
$site->setDescription('description');
1281+
$site->setOwner($internalUser);
1282+
$this->manager->persist($site);
1283+
}
1284+
$this->manager->flush();
1285+
}
1286+
1287+
/**
1288+
* @Given there are :nb sites with external owner
1289+
*/
1290+
public function thereAreSitesWithExternalOwner(int $nb)
1291+
{
1292+
for ($i = 1; $i <= $nb; ++$i) {
1293+
$externalUser = new ExternalUser();
1294+
$externalUser->setFirstname('External');
1295+
$externalUser->setLastname('User');
1296+
$externalUser->setEmail('[email protected]');
1297+
$externalUser->setExternalId('EXT');
1298+
$site = new Site();
1299+
$site->setTitle('title');
1300+
$site->setDescription('description');
1301+
$site->setOwner($externalUser);
1302+
$this->manager->persist($site);
1303+
}
1304+
$this->manager->flush();
1305+
}
1306+
1307+
/**
1308+
* @Given there is the following taxon:
1309+
*/
1310+
public function thereIsTheFollowingTaxon(PyStringNode $dataNode): void
1311+
{
1312+
$data = json_decode((string) $dataNode, true);
1313+
1314+
$taxon = $this->isOrm() ? new Taxon() : new TaxonDocument();
1315+
$taxon->setCode($data['code']);
1316+
$this->manager->persist($taxon);
1317+
1318+
$this->manager->flush();
1319+
}
1320+
1321+
/**
1322+
* @Given there is the following product:
1323+
*/
1324+
public function thereIsTheFollowingProduct(PyStringNode $dataNode): void
1325+
{
1326+
$data = json_decode((string) $dataNode, true);
1327+
1328+
$product = $this->isOrm() ? new Product() : new ProductDocument();
1329+
$product->setCode($data['code']);
1330+
if (isset($data['mainTaxon'])) {
1331+
$mainTaxonId = (int) str_replace('/taxons/', '', $data['mainTaxon']);
1332+
$mainTaxon = $this->manager->getRepository($this->isOrm() ? Taxon::class : TaxonDocument::class)->find($mainTaxonId);
1333+
$product->setMainTaxon($mainTaxon);
1334+
}
1335+
$this->manager->persist($product);
1336+
1337+
$this->manager->flush();
1338+
}
1339+
12301340
private function isOrm(): bool
12311341
{
12321342
return null !== $this->schemaTool;
@@ -1532,73 +1642,4 @@ private function buildThirdLevel()
15321642
{
15331643
return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument();
15341644
}
1535-
1536-
/**
1537-
* @Given there is a order with same customer and receiver
1538-
*/
1539-
public function testEagerLoadingNotDuplicateRelation()
1540-
{
1541-
$customer = $this->isOrm() ? new Customer() : new CustomerDocument();
1542-
$customer->name = 'customer_name';
1543-
1544-
$address1 = $this->isOrm() ? new Address() : new AddressDocument();
1545-
$address1->name = 'foo';
1546-
$address2 = $this->isOrm() ? new Address() : new AddressDocument();
1547-
$address2->name = 'bar';
1548-
1549-
$order = $this->isOrm() ? new Order() : new OrderDocument();
1550-
$order->recipient = $customer;
1551-
$order->customer = $customer;
1552-
1553-
$customer->addresses->add($address1);
1554-
$customer->addresses->add($address2);
1555-
1556-
$this->manager->persist($address1);
1557-
$this->manager->persist($address2);
1558-
$this->manager->persist($customer);
1559-
$this->manager->persist($order);
1560-
1561-
$this->manager->flush();
1562-
$this->manager->clear();
1563-
}
1564-
1565-
/**
1566-
* @Given there are :nb sites with internal owner
1567-
*/
1568-
public function thereAreSitesWithInternalOwner(int $nb)
1569-
{
1570-
for ($i = 1; $i <= $nb; ++$i) {
1571-
$internalUser = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\InternalUser();
1572-
$internalUser->setFirstname('Internal');
1573-
$internalUser->setLastname('User');
1574-
$internalUser->setEmail('[email protected]');
1575-
$internalUser->setInternalId('INT');
1576-
$site = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Site();
1577-
$site->setTitle('title');
1578-
$site->setDescription('description');
1579-
$site->setOwner($internalUser);
1580-
$this->manager->persist($site);
1581-
}
1582-
$this->manager->flush();
1583-
}
1584-
1585-
/**
1586-
* @Given there are :nb sites with external owner
1587-
*/
1588-
public function thereAreSitesWithExternalOwner(int $nb)
1589-
{
1590-
for ($i = 1; $i <= $nb; ++$i) {
1591-
$externalUser = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ExternalUser();
1592-
$externalUser->setFirstname('External');
1593-
$externalUser->setLastname('User');
1594-
$externalUser->setEmail('[email protected]');
1595-
$externalUser->setExternalId('EXT');
1596-
$site = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Site();
1597-
$site->setTitle('title');
1598-
$site->setDescription('description');
1599-
$site->setOwner($externalUser);
1600-
$this->manager->persist($site);
1601-
}
1602-
$this->manager->flush();
1603-
}
16041645
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Feature: JSON-LD using interface as resource
2+
In order to use interface as resource
3+
As a developer
4+
I should be able to serialize objects of an interface as API resource.
5+
6+
Background:
7+
Given I add "Accept" header equal to "application/ld+json"
8+
And I add "Content-Type" header equal to "application/ld+json"
9+
10+
@createSchema
11+
Scenario: Retrieve a taxon
12+
Given there is the following taxon:
13+
"""
14+
{
15+
"code": "WONDERFUL_TAXON"
16+
}
17+
"""
18+
When I send a "GET" request to "/taxons/1"
19+
Then the response status code should be 200
20+
And the response should be in JSON
21+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
22+
And the JSON should be equal to:
23+
"""
24+
{
25+
"@context": "/contexts/Taxon",
26+
"@id": "/taxons/1",
27+
"@type": "Taxon",
28+
"code": "WONDERFUL_TAXON"
29+
}
30+
"""
31+
32+
Scenario: Retrieve a product with a main taxon
33+
Given there is the following product:
34+
"""
35+
{
36+
"code": "GREAT_PRODUCT",
37+
"mainTaxon": "/taxons/1"
38+
}
39+
"""
40+
When I send a "GET" request to "/products/1"
41+
Then the response status code should be 200
42+
And the response should be in JSON
43+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
44+
And the JSON should be equal to:
45+
"""
46+
{
47+
"@context": "/contexts/Product",
48+
"@id": "/products/1",
49+
"@type": "Product",
50+
"code": "GREAT_PRODUCT",
51+
"mainTaxon": {
52+
"@id": "/taxons/1",
53+
"@type": "Taxon",
54+
"code": "WONDERFUL_TAXON"
55+
}
56+
}
57+
"""

features/main/relation.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ Feature: Relations support
559559
"""
560560

561561
Scenario: Eager load relations should not be duplicated
562-
Given there is a order with same customer and receiver
562+
Given there is an order with same customer and recipient
563563
When I add "Content-Type" header equal to "application/ld+json"
564564
And I send a "GET" request to "/orders"
565565
Then the response status code should be 200

phpstan.neon.dist

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
includes:
22
- vendor/jangregor/phpstan-prophecy/src/extension.neon
3-
- vendor/phpstan/phpstan-doctrine/extension.neon
4-
- vendor/phpstan/phpstan-phpunit/extension.neon
5-
- vendor/phpstan/phpstan-symfony/extension.neon
63

74
parameters:
85
level: 6

src/Api/ResourceClassResolver.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,32 +50,33 @@ public function getResourceClass($value, string $resourceClass = null, bool $str
5050
throw new InvalidArgumentException('Resource type could not be determined. Resource class must be specified.');
5151
}
5252

53-
if (null !== $resourceClass && !$this->isResourceClass($resourceClass)) {
54-
throw new InvalidArgumentException(sprintf('Specified class "%s" is not a resource class.', $resourceClass));
53+
if (null !== $actualClass && !$this->isResourceClass($actualClass)) {
54+
throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s".', $actualClass));
5555
}
5656

57-
if (null === $actualClass) {
58-
return $resourceClass;
57+
if (null !== $resourceClass && !$this->isResourceClass($resourceClass)) {
58+
throw new InvalidArgumentException(sprintf('Specified class "%s" is not a resource class.', $resourceClass));
5959
}
6060

61-
if ($strict && !is_a($actualClass, $resourceClass, true)) {
61+
if ($strict && null !== $actualClass && !is_a($actualClass, $resourceClass, true)) {
6262
throw new InvalidArgumentException(sprintf('Object of type "%s" does not match "%s" resource class.', $actualClass, $resourceClass));
6363
}
6464

65+
$targetClass = $actualClass ?? $resourceClass;
6566
$mostSpecificResourceClass = null;
6667

6768
foreach ($this->resourceNameCollectionFactory->create() as $resourceClassName) {
68-
if (!is_a($actualClass, $resourceClassName, true)) {
69+
if (!is_a($targetClass, $resourceClassName, true)) {
6970
continue;
7071
}
7172

72-
if (null === $mostSpecificResourceClass || is_subclass_of($resourceClassName, $mostSpecificResourceClass, true)) {
73+
if (null === $mostSpecificResourceClass || is_subclass_of($resourceClassName, $mostSpecificResourceClass)) {
7374
$mostSpecificResourceClass = $resourceClassName;
7475
}
7576
}
7677

7778
if (null === $mostSpecificResourceClass) {
78-
throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s".', $actualClass));
79+
throw new \LogicException('Unexpected execution flow.');
7980
}
8081

8182
return $mostSpecificResourceClass;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
7171
<argument type="service" id="serializer.mapping.class_metadata_factory" />
7272
<argument type="service" id="api_platform.metadata.property.metadata_factory.serializer.inner" />
73+
<argument type="service" id="api_platform.resource_class_resolver" />
7374
</service>
7475

7576
<service id="api_platform.metadata.property.metadata_factory.cached" class="ApiPlatform\Core\Metadata\Property\Factory\CachedPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="-10" public="false">

src/Hydra/Serializer/DocumentationNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ private function getProperty(PropertyMetadata $propertyMetadata, string $propert
459459
{
460460
$propertyData = [
461461
'@id' => $propertyMetadata->getIri() ?? "#$shortName/$propertyName",
462-
'@type' => $propertyMetadata->isReadableLink() ? 'rdf:Property' : 'hydra:Link',
462+
'@type' => false === $propertyMetadata->isReadableLink() ? 'hydra:Link' : 'rdf:Property',
463463
'rdfs:label' => $propertyName,
464464
'domain' => $prefixedShortName,
465465
];

0 commit comments

Comments
 (0)