Skip to content

Commit 48fd597

Browse files
mtarldsoyuka
authored andcommitted
test: input without data transformer only with operation
1 parent 0f02a50 commit 48fd597

File tree

16 files changed

+247
-28
lines changed

16 files changed

+247
-28
lines changed

features/jsonld/input_output.feature

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,27 @@ Feature: JSON-LD DTO input and output
354354
Then the response status code should be 400
355355
And the response should be in JSON
356356
And the JSON node "hydra:description" should be equal to "The input data is misformatted."
357+
358+
@!mongodb
359+
Scenario: Reset password through an input DTO without DataTransformer
360+
When I send a "POST" request to "/user-reset-password" with body:
361+
"""
362+
{
363+
"email": "[email protected]"
364+
}
365+
"""
366+
Then the response status code should be 201
367+
And the response should be in JSON
368+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
369+
And the JSON node "email" should be equal to "[email protected]"
370+
371+
@!mongodb
372+
Scenario: Reset password with an invalid payload through an input DTO without DataTransformer
373+
And I send a "POST" request to "/user-reset-password" with body:
374+
"""
375+
{
376+
"email": "this is not an email"
377+
}
378+
"""
379+
Then the response status code should be 422
380+
And the response should be in JSON

src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,8 @@ private function buildResourceOperations(array $attributes, string $resourceClas
137137

138138
// Loop again and set default operations if none where found
139139
foreach ($resources as $index => $resource) {
140-
if (\count($resource->getOperations() ?? [])) {
141-
continue;
142-
}
143-
144140
$operations = [];
145-
foreach ([new Get(), new GetCollection(), new Post(), new Put(), new Patch(), new Delete()] as $i => $operation) {
141+
foreach ($resource->getOperations() ?? [new Get(), new GetCollection(), new Post(), new Put(), new Patch(), new Delete()] as $i => $operation) {
146142
[$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
147143
$operations[$key] = $operation;
148144
}

src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,33 @@ public function create(string $resourceClass): ResourceMetadataCollection
5959

6060
$operations = new Operations();
6161
foreach ($resource->getOperations() ?? new Operations() as $key => $operation) {
62+
/** @var Operation */
6263
$operation = $this->configureUriVariables($operation);
6364

6465
if ($operation->getUriTemplate()) {
6566
$operation = $operation->withExtraProperties($operation->getExtraProperties() + ['user_defined_uri_template' => true]);
67+
if (!$operation->getName()) {
68+
$operation = $operation->withName($key);
69+
}
70+
6671
$operations->add($key, $operation);
6772
continue;
6873
}
6974

7075
if ($routeName = $operation->getRouteName()) {
76+
if (!$operation->getName()) {
77+
$operation = $operation->withName($routeName);
78+
}
79+
7180
$operations->add($routeName, $operation);
7281
continue;
7382
}
7483

7584
$operation = $operation->withUriTemplate($this->generateUriTemplate($operation));
7685
$operationName = $operation->getName() ?: sprintf('_api_%s_%s%s', $operation->getUriTemplate(), strtolower($operation->getMethod() ?? Operation::METHOD_GET), $operation->isCollection() ? '_collection' : '');
86+
if (!$operation->getName()) {
87+
$operation = $operation->withName($operationName);
88+
}
7789

7890
$operations->add($operationName, $operation);
7991
}

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ final class OpenApiFactory implements OpenApiFactoryInterface
6464
private $openApiOptions;
6565
private $paginationOptions;
6666
private $router;
67+
private $routeCollection;
6768

6869
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, SchemaFactoryInterface $jsonSchemaFactory, TypeFactoryInterface $jsonSchemaTypeFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $filterLocator, array $formats = [], Options $openApiOptions = null, PaginationOptions $paginationOptions = null, RouterInterface $router = null)
6970
{
@@ -141,8 +142,13 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
141142

142143
$uriVariables = $operation->getUriVariables();
143144
$resourceClass = $operation->getClass() ?? $resource->getClass();
145+
$routeName = $operation->getRouteName() ?? $operation->getName();
144146

145-
$path = $this->getPath($operation->getRouteName() ? $this->router->getRouteCollection()->get($operation->getRouteName())->getPath() : ($operation->getRoutePrefix() ?? '').$operation->getUriTemplate());
147+
if (!$this->routeCollection && $this->router) {
148+
$this->routeCollection = $this->router->getRouteCollection();
149+
}
150+
151+
$path = $this->getPath($routeName && $this->routeCollection ? $this->routeCollection->get($routeName)->getPath() : ($operation->getRoutePrefix() ?? '').$operation->getUriTemplate());
146152
$method = $operation->getMethod() ?? Operation::METHOD_GET;
147153

148154
if (!\in_array($method, Model\PathItem::$methods, true)) {

src/Serializer/AbstractItemNormalizer.php

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -226,36 +226,45 @@ public function denormalize($data, $class, $format = null, array $context = [])
226226
$normalizedData = is_scalar($data) ? [$data] : $this->prepareForDenormalization($data);
227227
$class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class);
228228
}
229+
229230
$resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
230231
$context['api_denormalize'] = true;
231232
$context['resource_class'] = $resourceClass;
232233

233-
if (null !== ($inputClass = $this->getInputClass($resourceClass, $context)) && null !== ($dataTransformer = $this->getDataTransformer($data, $resourceClass, $context))) {
234-
$dataTransformerContext = $context;
234+
if (null !== $inputClass = $this->getInputClass($resourceClass, $context)) {
235+
if (null !== $dataTransformer = $this->getDataTransformer($data, $resourceClass, $context)) {
236+
$dataTransformerContext = $context;
235237

236-
unset($context['input']);
237-
unset($context['resource_class']);
238+
unset($context['input']);
239+
unset($context['resource_class']);
238240

239-
if (!$this->serializer instanceof DenormalizerInterface) {
240-
throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
241-
}
241+
if (!$this->serializer instanceof DenormalizerInterface) {
242+
throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
243+
}
242244

243-
if ($dataTransformer instanceof DataTransformerInitializerInterface) {
244-
$context[AbstractObjectNormalizer::OBJECT_TO_POPULATE] = $dataTransformer->initialize($inputClass, $context);
245-
$context[AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE] = true;
246-
}
245+
if ($dataTransformer instanceof DataTransformerInitializerInterface) {
246+
$context[AbstractObjectNormalizer::OBJECT_TO_POPULATE] = $dataTransformer->initialize($inputClass, $context);
247+
$context[AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE] = true;
248+
}
247249

248-
try {
249-
$denormalizedInput = $this->serializer->denormalize($data, $inputClass, $format, $context);
250-
} catch (NotNormalizableValueException $e) {
251-
throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
252-
}
250+
try {
251+
$denormalizedInput = $this->serializer->denormalize($data, $inputClass, $format, $context);
252+
} catch (NotNormalizableValueException $e) {
253+
throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
254+
}
255+
256+
if (!\is_object($denormalizedInput)) {
257+
throw new UnexpectedValueException('Expected denormalized input to be an object.');
258+
}
253259

254-
if (!\is_object($denormalizedInput)) {
255-
throw new UnexpectedValueException('Expected denormalized input to be an object.');
260+
return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
256261
}
257262

258-
return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
263+
// Are we in a Request context?
264+
if ($context['operation'] ?? $context['operation_type'] ?? false) {
265+
$resourceClass = $inputClass;
266+
$context['resource_class'] = $inputClass;
267+
}
259268
}
260269

261270
$supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
@@ -290,6 +299,10 @@ public function denormalize($data, $class, $format = null, array $context = [])
290299
$previousObject = null !== $objectToPopulate ? clone $objectToPopulate : null;
291300
$object = parent::denormalize($data, $resourceClass, $format, $context);
292301

302+
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
303+
return $object;
304+
}
305+
293306
// Revert attributes that aren't allowed to be changed after a post-denormalize check
294307
foreach (array_keys($data) as $attribute) {
295308
if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
@@ -414,6 +427,10 @@ protected function extractAttributes($object, $format = null, array $context = [
414427
*/
415428
protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
416429
{
430+
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
431+
return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
432+
}
433+
417434
$options = $this->getFactoryOptions($context);
418435
$propertyNames = $this->propertyNameCollectionFactory->create($context['resource_class'], $options);
419436

@@ -456,6 +473,10 @@ protected function isAllowedAttribute($classOrObject, $attribute, $format = null
456473
*/
457474
protected function canAccessAttribute($object, string $attribute, array $context = []): bool
458475
{
476+
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
477+
return true;
478+
}
479+
459480
$options = $this->getFactoryOptions($context);
460481
/** @var PropertyMetadata|ApiProperty */
461482
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
@@ -857,6 +878,10 @@ protected function transformOutput($object, array $context = [], string $outputC
857878

858879
private function createAttributeValue($attribute, $value, $format = null, array $context = [])
859880
{
881+
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
882+
return $value;
883+
}
884+
860885
/** @var ApiProperty|PropertyMetadata */
861886
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
862887
$type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : ($propertyMetadata->getBuiltinTypes()[0] ?? null);

src/Symfony/Bundle/Resources/config/legacy/doctrine_odm.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<!-- Metadata loader -->
5454
<service id="api_platform.doctrine_mongodb.odm.metadata.property.metadata_factory.legacy"
5555
class="ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Metadata\Property\DoctrineMongoDbOdmPropertyMetadataFactory"
56-
decorates="api_platform.metadata.property.metadata_factory" decoration-priority="40" public="false">
56+
decorates="api_platform.metadata.property.metadata_factory.legacy" decoration-priority="40" public="false">
5757
<argument type="service" id="doctrine_mongodb"/>
5858
<argument type="service" id="api_platform.doctrine_mongodb.odm.metadata.property.metadata_factory.legacy.inner"/>
5959
</service>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\DataPersister;
15+
16+
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\Dto\UserResetPasswordDto;
18+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
19+
20+
final class UserResetPasswordDataPersister implements DataPersisterInterface
21+
{
22+
public function persist($data)
23+
{
24+
if ('[email protected]' === $data->email) {
25+
return $data;
26+
}
27+
28+
throw new NotFoundHttpException();
29+
}
30+
31+
public function remove($data)
32+
{
33+
throw new \LogicException(sprintf('Unexpected "%s()" call.', __METHOD__));
34+
}
35+
36+
public function supports($data): bool
37+
{
38+
return $data instanceof UserResetPasswordDto;
39+
}
40+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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\Dto;
15+
16+
use Symfony\Component\Validator\Constraints as Assert;
17+
18+
final class UserResetPasswordDto
19+
{
20+
/**
21+
* @Assert\Email
22+
*/
23+
public $email;
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\Dto\UserResetPasswordDto;
18+
use Symfony\Component\Validator\Constraints as Assert;
19+
20+
/**
21+
* @ApiResource(
22+
* collectionOperations={
23+
* "post"={
24+
* "method"="POST",
25+
* "path"="/user-reset-password",
26+
* "input"=UserResetPasswordDto::class
27+
* }
28+
* },
29+
* itemOperations={}
30+
* )
31+
*/
32+
final class UserResource
33+
{
34+
/**
35+
* @Assert\NotBlank
36+
*/
37+
public string $username;
38+
}

tests/Fixtures/app/config/config_test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ services:
109109
tags:
110110
- { name: 'api_platform.data_persister' }
111111

112+
app.user_reset_password_data_persister:
113+
class: ApiPlatform\Tests\Fixtures\TestBundle\DataPersister\UserResetPasswordDataPersister
114+
public: false
115+
tags:
116+
- { name: 'api_platform.data_persister' }
117+
112118
app.graphql.query_resolver.dummy_custom_not_retrieved_item:
113119
class: 'ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver\DummyCustomQueryNotRetrievedItemResolver'
114120
public: false

0 commit comments

Comments
 (0)