Skip to content

Commit

Permalink
fix(metadata): fix POST on subresource
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Aug 8, 2023
1 parent 83dbfbf commit 53bb206
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 12 deletions.
24 changes: 19 additions & 5 deletions features/main/sub_resource.feature
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ Feature: Sub-resource support

@!mongodb
@createSchema
Scenario: The generated crud should allow us to interact with the SubresourceEmployee
Scenario Outline: The generated crud should allow us to interact with the subresources
Given I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/subresource_organizations" with body:
"""
Expand All @@ -574,22 +574,36 @@ Feature: Sub-resource support
"""
Then the response status code should be 201
Given I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/subresource_organizations/1/subresource_employees" with body:
And I send a "POST" request to "<invalid_uri>" with body:
"""
{
"name": "soyuka"
}
"""
Then the response status code should be 404
Given I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "<collection_uri>" with body:
"""
{
"name": "soyuka"
}
"""
Then the response status code should be 201
And I send a "GET" request to "/subresource_organizations/1/subresource_employees/1"
And I send a "GET" request to "<item_uri>"
Then the response status code should be 200
And I send a "GET" request to "/subresource_organizations/1/subresource_employees"
And I send a "GET" request to "<collection_uri>"
Then the response status code should be 200
Given I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/subresource_organizations/1/subresource_employees/1" with body:
And I send a "PUT" request to "<item_uri>" with body:
"""
{
"name": "ok"
}
"""
Then the response status code should be 200
Given I send a "DELETE" request to "<item_uri>"
Then the response status code should be 204
Examples:
| invalid_uri | collection_uri | item_uri |
| /subresource_organizations/invalid/subresource_employees | /subresource_organizations/1/subresource_employees | /subresource_organizations/1/subresource_employees/1 |
| /subresource_organizations/invalid/subresource_factories | /subresource_organizations/1/subresource_factories | /subresource_organizations/1/subresource_factories/1 |
13 changes: 7 additions & 6 deletions src/Metadata/Resource/Factory/OperationDefaultsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,7 @@ private function getDefaultHttpOperations($resource): iterable
return new Operations($operations);
}

$post = new Post();
if ($resource->getUriTemplate() && !$resource->getProvider()) {
$post = $post->withProvider(CreateProvider::class);
}

return [new Get(), new GetCollection(), $post, new Put(), new Patch(), new Delete()];
return [new Get(), new GetCollection(), new Post(), new Put(), new Patch(), new Delete()];
}

private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource
Expand Down Expand Up @@ -207,6 +202,12 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper
$operation = $operation->withName($operation->getRouteName());
}

// Handle subresources on Post operations (e.g.: POST /books/{bookId}/reviews)
if ($operation instanceof Post && $operation->getUriTemplate() && !$operation->getProvider()) {
$operation = $operation->withProvider(CreateProvider::class);
}

// @phpstan-ignore-next-line
$operationName = $operation->getName() ?? $this->getDefaultOperationName($operation, $resource->getClass());

return [
Expand Down
8 changes: 7 additions & 1 deletion src/State/CreateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

Expand Down Expand Up @@ -59,12 +60,17 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
}

$relation = $this->decorated->provide(new Get(uriVariables: $relationUriVariables, class: $relationClass), $uriVariables);
if (!$relation) {
throw new NotFoundHttpException('Not Found');

Check warning on line 64 in src/State/CreateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/CreateProvider.php#L64

Added line #L64 was not covered by tests
}

try {
$resource = new ($operation->getClass());
} catch (\Throwable $e) {
throw new RuntimeException(sprintf('An error occurred while trying to create an instance of the "%s" resource. Consider writing your own "%s" implementation and setting it as `provider` on your operation instead.', $operation->getClass(), ProviderInterface::class), 0, $e);
}
$this->propertyAccessor->setValue($resource, $key, $relation);
$property = $operationUriVariables[$key]->getToProperty() ?? $key;
$this->propertyAccessor->setValue($resource, $property, $relation);

return $resource;
}
Expand Down
107 changes: 107 additions & 0 deletions tests/Fixtures/TestBundle/Entity/SubresourceFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\ORM\Mapping as ORM;

#[ApiResource(
types: ['https://schema.org/LocalBusiness'],
operations: [
new GetCollection(
uriTemplate: '/subresource_organizations/{subresourceOrganizationId}/subresource_factories',
uriVariables: [
'subresourceOrganizationId' => new Link(toProperty: 'subresourceOrganization', fromClass: SubresourceOrganization::class),
]
),
new Post(
uriTemplate: '/subresource_organizations/{subresourceOrganizationId}/subresource_factories',
uriVariables: [
'subresourceOrganizationId' => new Link(toProperty: 'subresourceOrganization', fromClass: SubresourceOrganization::class),
]
),
new Get(
uriTemplate: '/subresource_organizations/{subresourceOrganizationId}/subresource_factories/{id}',
uriVariables: [
'subresourceOrganizationId' => new Link(toProperty: 'subresourceOrganization', fromClass: SubresourceOrganization::class),
'id' => new Link(fromClass: SubresourceFactory::class),
]
),
new Put(
uriTemplate: '/subresource_organizations/{subresourceOrganizationId}/subresource_factories/{id}',
extraProperties: ['standard_put' => false],
uriVariables: [
'subresourceOrganizationId' => new Link(toProperty: 'subresourceOrganization', fromClass: SubresourceOrganization::class),
'id' => new Link(fromClass: SubresourceFactory::class),
]
),
new Delete(
uriTemplate: '/subresource_organizations/{subresourceOrganizationId}/subresource_factories/{id}',
uriVariables: [
'subresourceOrganizationId' => new Link(toProperty: 'subresourceOrganization', fromClass: SubresourceOrganization::class),
'id' => new Link(fromClass: SubresourceFactory::class),
]
),
]
)]
#[ORM\Entity]
class SubresourceFactory
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;

#[ORM\ManyToOne(inversedBy: 'factories')]
#[ORM\JoinColumn(nullable: false)]
private ?SubresourceOrganization $subresourceOrganization = null;

public function getId(): ?int
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(?string $name): self
{
$this->name = $name;

return $this;
}

public function getSubresourceOrganization(): ?SubresourceOrganization
{
return $this->subresourceOrganization;
}

public function setSubresourceOrganization(?SubresourceOrganization $subresourceOrganization): self
{
$this->subresourceOrganization = $subresourceOrganization;

return $this;
}
}
32 changes: 32 additions & 0 deletions tests/Fixtures/TestBundle/Entity/SubresourceOrganization.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ class SubresourceOrganization
#[ORM\OneToMany(mappedBy: 'subresourceOrganization', targetEntity: SubresourceEmployee::class, orphanRemoval: true)]
private Collection $employees;

#[ORM\OneToMany(mappedBy: 'subresourceOrganization', targetEntity: SubresourceFactory::class, orphanRemoval: true)]
private Collection $factories;

public function __construct()
{
$this->employees = new ArrayCollection();
$this->factories = new ArrayCollection();
}

public function getId(): ?int
Expand Down Expand Up @@ -82,4 +86,32 @@ public function removeSubresourceEmployee(SubresourceEmployee $employee): self

return $this;
}

/**
* @return Collection<int, SubresourceFactory>
*/
public function getSubresourceFactories(): Collection
{
return $this->factories;
}

public function addSubresourceFactory(SubresourceFactory $factory): self
{
if (!$this->factories->contains($factory)) {
$this->factories->add($factory);
$factory->setSubresourceOrganization($this);
}

return $this;
}

public function removeSubresourceFactory(SubresourceFactory $factory): self
{
// set the owning side to null (unless already changed)
if ($this->factories->removeElement($factory) && $factory->getSubresourceOrganization() === $this) {
$factory->setSubresourceOrganization(null);
}

return $this;
}
}

0 comments on commit 53bb206

Please sign in to comment.