Skip to content

Commit e7bc2ab

Browse files
authored
fix(jsonschema): indirect resource input schema (api-platform#6001)
Fixes api-platform#5998 A resource embedded in another class can be writable without having a write operation (POST, PUT, PATCH).
1 parent 49e4adc commit e7bc2ab

File tree

5 files changed

+225
-1
lines changed

5 files changed

+225
-1
lines changed

src/JsonSchema/SchemaFactory.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ public function buildSchema(string $className, string $format = 'json', string $
8383
$method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
8484
}
8585

86-
if (Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
86+
// In case of FORCE_SUBSCHEMA an object can be writable through another class eventhough it has no POST operation
87+
if (!($serializerContext[self::FORCE_SUBSCHEMA] ?? false) && Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
8788
return $schema;
8889
}
8990

@@ -217,6 +218,10 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
217218
}
218219

219220
$subSchema = $this->buildSchema($className, $format, $parentType, null, $subSchema, $serializerContext + [self::FORCE_SUBSCHEMA => true], false);
221+
if (!isset($subSchema['$ref'])) {
222+
continue;
223+
}
224+
220225
if ($isCollection) {
221226
$propertySchema['items']['$ref'] = $subSchema['$ref'];
222227
unset($propertySchema['items']['type']);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Post;
18+
use Doctrine\Common\Collections\ArrayCollection;
19+
use Doctrine\Common\Collections\Collection;
20+
use Doctrine\ORM\Mapping as ORM;
21+
22+
#[ORM\Entity]
23+
#[ApiResource]
24+
#[Post(
25+
denormalizationContext: ['groups' => ['product:write']],
26+
input: SaveProduct::class,
27+
)]
28+
class Issue5998Product
29+
{
30+
#[ORM\Id]
31+
#[ORM\GeneratedValue]
32+
#[ORM\Column]
33+
private ?int $id = null;
34+
35+
/**
36+
* @var Collection<int, ProductCode>
37+
*/
38+
#[ORM\OneToMany(mappedBy: 'product', targetEntity: ProductCode::class, cascade: ['persist'], orphanRemoval: true)]
39+
private Collection $codes;
40+
41+
public function __construct()
42+
{
43+
$this->codes = new ArrayCollection();
44+
}
45+
46+
public function getId(): ?int
47+
{
48+
return $this->id;
49+
}
50+
51+
/**
52+
* @return Collection<int, ProductCode>
53+
*/
54+
public function getCodes(): Collection
55+
{
56+
return $this->codes;
57+
}
58+
59+
public function addCode(ProductCode $code): void
60+
{
61+
if (!$this->codes->contains($code)) {
62+
$this->codes->add($code);
63+
$code->setProduct($this);
64+
}
65+
}
66+
67+
public function removeCode(ProductCode $code): void
68+
{
69+
if ($this->codes->removeElement($code)) {
70+
// set the owning side to null (unless already changed)
71+
if ($code->getProduct() === $this) {
72+
$code->setProduct(null);
73+
}
74+
}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use Doctrine\ORM\Mapping as ORM;
19+
use Symfony\Component\Serializer\Annotation\Groups;
20+
21+
#[ApiResource]
22+
#[Get]
23+
#[ORM\Entity]
24+
class ProductCode
25+
{
26+
#[ORM\Id]
27+
#[ORM\GeneratedValue]
28+
#[ORM\Column]
29+
private ?int $id = null;
30+
31+
#[ORM\Column(length: 180)]
32+
#[Groups(['product:write'])]
33+
private ?string $type = null;
34+
35+
#[ORM\Column(length: 180)]
36+
#[Groups(['product:write'])]
37+
private ?string $value = null;
38+
39+
#[ORM\ManyToOne(inversedBy: 'codes')]
40+
#[ORM\JoinColumn(nullable: false)]
41+
private ?Issue5998Product $product = null;
42+
43+
public function getId(): ?int
44+
{
45+
return $this->id;
46+
}
47+
48+
public function getType(): ?string
49+
{
50+
return $this->type;
51+
}
52+
53+
public function setType(string $type): void
54+
{
55+
$this->type = $type;
56+
}
57+
58+
public function getValue(): ?string
59+
{
60+
return $this->value;
61+
}
62+
63+
public function setValue(?string $value): void
64+
{
65+
$this->value = $value;
66+
}
67+
68+
public function getProduct(): ?Issue5998Product
69+
{
70+
return $this->product;
71+
}
72+
73+
public function setProduct(?Issue5998Product $product): void
74+
{
75+
$this->product = $product;
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998;
15+
16+
use Doctrine\Common\Collections\ArrayCollection;
17+
use Doctrine\Common\Collections\Collection;
18+
use Symfony\Component\Serializer\Annotation\Groups;
19+
20+
class SaveProduct
21+
{
22+
/**
23+
* @var Collection<int, ProductCode>
24+
*/
25+
#[Groups(['product:write'])]
26+
private Collection $codes;
27+
28+
public function __construct()
29+
{
30+
$this->codes = new ArrayCollection();
31+
}
32+
33+
/**
34+
* @return Collection<int, ProductCode>
35+
*/
36+
public function getCodes(): Collection
37+
{
38+
return $this->codes;
39+
}
40+
41+
public function addCode(ProductCode $code): void
42+
{
43+
if (!$this->codes->contains($code)) {
44+
$this->codes->add($code);
45+
}
46+
}
47+
48+
public function removeCode(ProductCode $code): void
49+
{
50+
if ($this->codes->contains($code)) {
51+
$this->codes->removeElement($code);
52+
}
53+
}
54+
}

tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php

+12
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,16 @@ public function testArraySchemaWithTypeFactory(): void
138138

139139
$this->assertEquals($json['definitions']['Foo.jsonld']['properties']['expiration'], ['type' => 'string', 'format' => 'date']);
140140
}
141+
142+
/**
143+
* Test issue #5998.
144+
*/
145+
public function testWritableNonResourceRef(): void
146+
{
147+
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998\SaveProduct', '--type' => 'input']);
148+
$result = $this->tester->getDisplay();
149+
$json = json_decode($result, associative: true);
150+
151+
$this->assertEquals($json['definitions']['SaveProduct.jsonld']['properties']['codes']['items']['$ref'], '#/definitions/ProductCode.jsonld');
152+
}
141153
}

0 commit comments

Comments
 (0)