Skip to content

Commit

Permalink
fix(state): improve DX around the generic ObjectProvider (#5051)
Browse files Browse the repository at this point in the history
  • Loading branch information
chalasr authored Oct 6, 2022
1 parent 87eae48 commit 00db2cd
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .commitlintrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"openapi",
"serializer",
"jsonschema",
"validation"
"validation",
"state"
]
],
"scope-empty": [
Expand Down
8 changes: 6 additions & 2 deletions src/State/CreateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\State;

use ApiPlatform\Exception\RuntimeException;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link;
Expand Down Expand Up @@ -58,8 +59,11 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
}

$relation = $this->decorated->provide(new Get(uriVariables: $relationUriVariables, class: $relationClass), $uriVariables);
$refl = new \ReflectionClass($operation->getClass());
$resource = $refl->newInstanceWithoutConstructor();
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);

return $resource;
Expand Down
11 changes: 7 additions & 4 deletions src/State/ObjectProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@

namespace ApiPlatform\State;

use ApiPlatform\Exception\RuntimeException;
use ApiPlatform\Metadata\Operation;

/**
* An ItemProvider that just create a new object.
* An ItemProvider that just creates a new object.
*
* @author Antoine Bluchet <[email protected]>
*
Expand All @@ -26,8 +27,10 @@ final class ObjectProvider implements ProviderInterface
{
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
{
$refl = new \ReflectionClass($operation->getClass());

return $refl->newInstanceWithoutConstructor();
try {
return 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Get;
use ApiPlatform\Metadata\Post;

#[Post]
#[ApiResource(
uriTemplate: '/companies/{companyId}/employees/{id}',
uriVariables: [
'companyId' => ['from_class' => Company::class, 'to_property' => 'company'],
'id' => ['from_class' => DummyResourceWithComplexConstructor::class],
]
)]
#[Get]
class DummyResourceWithComplexConstructor
{
private \DateTimeInterface $someInternalTimestamp;
private ?Company $company;

public function __construct(private int $id, private string $name)
{
$this->someInternalTimestamp = new \DateTimeImmutable();
}

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

public function getCompany(): Company
{
return $this->company;
}

public function setCompany(Company $company): void
{
$this->company = $company;
}

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

public function setName(string $name): void
{
$this->name = $name;
}

public function getSomeInternalTimestamp(): \DateTimeInterface
{
return $this->someInternalTimestamp;
}

public function setSomeInternalTimestamp(\DateTimeInterface $timestamp): void
{
$this->someInternalTimestamp = $timestamp;
}
}
19 changes: 19 additions & 0 deletions tests/State/CreateProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@

namespace ApiPlatform\Tests\State;

use ApiPlatform\Exception\RuntimeException;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\CreateProvider;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Company;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyResourceWithComplexConstructor;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Employee;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
Expand All @@ -41,6 +43,23 @@ public function testProvide(): void
$createProvider->provide($operation, ['company' => 1]);
}

public function testProvideFailsProperlyOnComplexConstructor(): void
{
$link = new Link(identifiers: ['id'], fromClass: Company::class, parameterName: 'company');
$decorated = $this->prophesize(ProviderInterface::class);
$decorated->provide(
new Get(uriVariables: ['id' => $link], class: Company::class),
['company' => 1]
)->shouldBeCalled()->willReturn(new Company());
$operation = new Post(class: DummyResourceWithComplexConstructor::class, uriTemplate: '/company/{company}/employees', uriVariables: ['company' => $link]);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('An error occurred while trying to create an instance of the "ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyResourceWithComplexConstructor" resource. Consider writing your own "ApiPlatform\State\ProviderInterface" implementation and setting it as `provider` on your operation instead.');

$createProvider = new CreateProvider($decorated->reveal());
$createProvider->provide($operation, ['company' => 1]);
}

public function testSkipWhenController(): void
{
$decorated = $this->prophesize(ProviderInterface::class);
Expand Down
44 changes: 44 additions & 0 deletions tests/State/ObjectProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\State;

use ApiPlatform\Exception\RuntimeException;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ObjectProvider;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyResourceWithComplexConstructor;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;

class ObjectProviderTest extends TestCase
{
use ProphecyTrait;

public function testProvide(): void
{
$operation = new Post(class: \stdClass::class);
$objectProvider = new ObjectProvider();
$this->assertInstanceOf(\stdClass::class, $objectProvider->provide($operation));
}

public function testProvideFailsProperlyOnComplexConstructor(): void
{
$operation = new Post(class: DummyResourceWithComplexConstructor::class);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('An error occurred while trying to create an instance of the "ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyResourceWithComplexConstructor" resource. Consider writing your own "ApiPlatform\State\ProviderInterface" implementation and setting it as `provider` on your operation instead.');

$objectProvider = new ObjectProvider();
$objectProvider->provide($operation);
}
}

0 comments on commit 00db2cd

Please sign in to comment.