api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.4k stars 859 forks source link

Impossible to use messenger Input class with empty DTO (no attributes) #2773

Closed Nightbr closed 3 years ago

Nightbr commented 5 years ago

Hey folks,

I'm running into an issue where I want to use API entrypoint with messenger="input" and my input class (Command) is a DTO and it has no attributes.

A simple example of a User entity who needs to resend its validation email:

Entity/User.php

...
 *         "postResendValidationMail"={
 *             "method"="POST",
 *             "path"="/users/resend_validation_mail",
 *             "output"=false,
 *             "status"=202,
 *             "input"=ResendValidationMailCommand::class,
 *             "messenger"="input",
 *             "access_control"="is_granted('IS_AUTHENTICATED_FULLY')"
 *         },
...

And the input class (DTO) here we called them Command: ResendValidationMailCommand.php

<?php

declare(strict_types=1);

namespace App\Domain\User\Command;

/**
 * Class ResendValidationMailCommand.
 */
class ResendValidationMailCommand implements UserCommandInterface
{
}

This will always throw an error because the body is empty when we call the POST entrypoint:

{
  "@context": "\/api\/contexts\/Error",
  "@type": "hydra:Error",
  "hydra:title": "An error occurred",
  "hydra:description": "Cannot validate values of type \"NULL\" automatically. Please provide a constraint.",
  "trace": [
    {
      "namespace": "",
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "\/var\/www\/vendor\/symfony\/validator\/Validator\/RecursiveContextualValidator.php",
      "line": 166,
      "args": []
    },
    {
      "namespace": "Symfony\\Component\\Validator\\Validator",
      "short_class": "RecursiveContextualValidator",
      "class": "Symfony\\Component\\Validator\\Validator\\RecursiveContextualValidator",
      "type": "->",
      "function": "validate",
      "file": "\/var\/www\/vendor\/symfony\/validator\/Validator\/RecursiveValidator.php",
      "line": 100,
      "args": [
        [
          "null",
          null
        ],
        [
          "null",
          null
        ],
        [
          "array",
          [
            [
              "string",
              "Default"
            ]
          ]
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Validator\\Validator",
      "short_class": "RecursiveValidator",
      "class": "Symfony\\Component\\Validator\\Validator\\RecursiveValidator",
      "type": "->",
      "function": "validate",
      "file": "\/var\/www\/vendor\/symfony\/validator\/Validator\/TraceableValidator.php",
      "line": 66,
      "args": [
        [
          "null",
          null
        ],
        [
          "null",
          null
        ],
        [
          "null",
          null
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Validator\\Validator",
      "short_class": "TraceableValidator",
      "class": "Symfony\\Component\\Validator\\Validator\\TraceableValidator",
      "type": "->",
      "function": "validate",
      "file": "\/var\/www\/vendor\/liip\/functional-test-bundle\/src\/Validator\/DataCollectingValidator.php",
      "line": 66,
      "args": [
        [
          "null",
          null
        ],
        [
          "null",
          null
        ],
        [
          "null",
          null
        ]
      ]
    },
    {
      "namespace": "Liip\\FunctionalTestBundle\\Validator",
      "short_class": "DataCollectingValidator",
      "class": "Liip\\FunctionalTestBundle\\Validator\\DataCollectingValidator",
      "type": "->",
      "function": "validate",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Bridge\/Symfony\/Validator\/Validator.php",
      "line": 61,
      "args": [
        [
          "null",
          null
        ],
        [
          "null",
          null
        ],
        [
          "null",
          null
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Bridge\\Symfony\\Validator",
      "short_class": "Validator",
      "class": "ApiPlatform\\Core\\Bridge\\Symfony\\Validator\\Validator",
      "type": "->",
      "function": "validate",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Validator\/EventListener\/ValidateListener.php",
      "line": 62,
      "args": [
        [
          "null",
          null
        ],
        [
          "array",
          {
            "groups": [
              "null",
              null
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Validator\\EventListener",
      "short_class": "ValidateListener",
      "class": "ApiPlatform\\Core\\Validator\\EventListener\\ValidateListener",
      "type": "->",
      "function": "onKernelView",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/Debug\/WrappedListener.php",
      "line": 115,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent"
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "WrappedListener",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener",
      "type": "->",
      "function": "__invoke",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/EventDispatcher.php",
      "line": 212,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent"
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "doDispatch",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/EventDispatcher.php",
      "line": 44,
      "args": [
        [
          "array",
          [
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ]
          ]
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/Debug\/TraceableEventDispatcher.php",
      "line": 145,
      "args": [
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "TraceableEventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 155,
      "args": [
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handleRaw",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 67,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/Kernel.php",
      "line": 198,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ],
        [
          "boolean",
          true
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "Kernel",
      "class": "Symfony\\Component\\HttpKernel\\Kernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/public\/index.php",
      "line": 25,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ]
      ]
    }
  ]
}

Analysis

It seems the Symfony Validator is always called even if there is no attribute to be validated. It think it could be great to handle this case in https://github.com/api-platform/core/blob/master/src/Bridge/Symfony/Validator/EventListener/ValidateListener.php#L85 When the $validationGroup & $data are null/empty, don't call validate.

I think it's more in https://github.com/api-platform/core/blob/master/src/Validator/EventListener/ValidateListener.php#L60 we need to handle empty DTO input.

Further investigation

I tried to put "defaults"={"_api_receive"=false}, on the operation but it can't handle $data null for POST:

{
  "@context": "\/api\/contexts\/Error",
  "@type": "hydra:Error",
  "hydra:title": "An error occurred",
  "hydra:description": "Controller \"ApiPlatform\\Core\\Action\\PlaceholderAction\" requires that you provide a value for the \"$data\" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.",
  "trace": [
    {
      "namespace": "",
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/Controller\/ArgumentResolver.php",
      "line": 78,
      "args": []
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel\\Controller",
      "short_class": "ArgumentResolver",
      "class": "Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver",
      "type": "->",
      "function": "getArguments",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/Controller\/TraceableArgumentResolver.php",
      "line": 38,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "object",
          "ApiPlatform\\Core\\Action\\PlaceholderAction"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel\\Controller",
      "short_class": "TraceableArgumentResolver",
      "class": "Symfony\\Component\\HttpKernel\\Controller\\TraceableArgumentResolver",
      "type": "->",
      "function": "getArguments",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 142,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "object",
          "ApiPlatform\\Core\\Action\\PlaceholderAction"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handleRaw",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 67,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/Kernel.php",
      "line": 198,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ],
        [
          "boolean",
          true
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "Kernel",
      "class": "Symfony\\Component\\HttpKernel\\Kernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/public\/index.php",
      "line": 25,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ]
      ]
    }
  ]
}

Workaround

The only workaround we found is to add a dummy attribute in the DTO but it's not ideal:

<?php

declare(strict_types=1);

namespace App\Domain\User\Command;

/**
 * Class ResendValidationMailCommand.
 */
class ResendValidationMailCommand implements UserCommandInterface
{
    /**
     * @var string
     */
    public $dummy = '';
}

ideas or other workaround?

Any idea on this? Or workaround because we have several entrypoint we want to manage with CQRS pattern which haven't data to transport (just the info that Command is called).

Thanks in advance ;)

soyuka commented 5 years ago

https://github.com/api-platform/core/pull/2757 might help you fix this I'm also okay for a fix in the validation but it may be wrong to not validate on these simple criteria :|.

Nightbr commented 5 years ago

Hey, thanks for the quick answer!

Sure if we can control the listener it will handle our use case perfectly and be less intrusive in the Validation process ;)

I think we can say: No validation if No attribute & No data. But I'm not sure if there is some drawback to do this.

teohhanhui commented 5 years ago

If your input DTO has no attributes, you must send an empty JSON object, not an empty body.

Nightbr commented 5 years ago

I tried with an empty DTO and send an empty JSON object {} but still an error: ResendValidationMailCommand.php (UserCommandInterface is just an empty interface)

<?php

declare(strict_types=1);

namespace App\Domain\User\Command;

/**
 * Class ResendValidationMailCommand.
 */
class ResendValidationMailCommand implements UserCommandInterface
{
}

Response:

{
  "@context": "\/api\/contexts\/Error",
  "@type": "hydra:Error",
  "hydra:title": "An error `occurred",`
  "hydra:description": "There is no PropertyInfo extractor supporting the class \"App\\Domain\\User\\Command\\ResendValidationMailCommand\".",
  "trace": [
    {
      "namespace": "",```
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Bridge\/Symfony\/PropertyInfo\/Metadata\/Property\/PropertyInfoPropertyNameCollectionFactory.php",
      "line": 46,
      "args": []
    },
    {
      "namespace": "ApiPlatform\\Core\\Bridge\\Symfony\\PropertyInfo\\Metadata\\Property",
      "short_class": "PropertyInfoPropertyNameCollectionFactory",
      "class": "ApiPlatform\\Core\\Bridge\\Symfony\\PropertyInfo\\Metadata\\Property\\PropertyInfoPropertyNameCollectionFactory",
      "type": "->",
      "function": "create",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Metadata\/Property\/Factory\/InheritedPropertyNameCollectionFactory.php",
      "line": 44,
      "args": [
        [
          "string",
          "App\\Domain\\User\\Command\\ResendValidationMailCommand"
        ],
        [
          "array",
          {
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Metadata\\Property\\Factory",
      "short_class": "InheritedPropertyNameCollectionFactory",
      "class": "ApiPlatform\\Core\\Metadata\\Property\\Factory\\InheritedPropertyNameCollectionFactory",
      "type": "->",
      "function": "create",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Metadata\/Property\/Factory\/ExtractorPropertyNameCollectionFactory.php",
      "line": 50,
      "args": [
        [
          "string",
          "App\\Domain\\User\\Command\\ResendValidationMailCommand"
        ],
        [
          "array",
          {
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Metadata\\Property\\Factory",
      "short_class": "ExtractorPropertyNameCollectionFactory",
      "class": "ApiPlatform\\Core\\Metadata\\Property\\Factory\\ExtractorPropertyNameCollectionFactory",
      "type": "->",
      "function": "create",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Metadata\/Property\/Factory\/ExtractorPropertyNameCollectionFactory.php",
      "line": 50,
      "args": [
        [
          "string",
          "App\\Domain\\User\\Command\\ResendValidationMailCommand"
        ],
        [
          "array",
          {
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Metadata\\Property\\Factory",
      "short_class": "ExtractorPropertyNameCollectionFactory",
      "class": "ApiPlatform\\Core\\Metadata\\Property\\Factory\\ExtractorPropertyNameCollectionFactory",
      "type": "->",
      "function": "create",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Metadata\/Property\/Factory\/CachedPropertyNameCollectionFactory.php",
      "line": 47,
      "args": [
        [
          "string",
          "App\\Domain\\User\\Command\\ResendValidationMailCommand"
        ],
        [
          "array",
          {
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Metadata\\Property\\Factory",
      "short_class": "CachedPropertyNameCollectionFactory",
      "class": "ApiPlatform\\Core\\Metadata\\Property\\Factory\\CachedPropertyNameCollectionFactory",
      "type": "->",
      "function": "ApiPlatform\\Core\\Metadata\\Property\\Factory\\{closure}",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Cache\/CachedTrait.php",
      "line": 44,
      "args": []
    },
    {
      "namespace": "ApiPlatform\\Core\\Metadata\\Property\\Factory",
      "short_class": "CachedPropertyNameCollectionFactory",
      "class": "ApiPlatform\\Core\\Metadata\\Property\\Factory\\CachedPropertyNameCollectionFactory",
      "type": "->",
      "function": "getCached",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Metadata\/Property\/Factory\/CachedPropertyNameCollectionFactory.php",
      "line": 48,
      "args": [
        [
          "string",
          "property_name_collection_24a6f81e1e9443f5e1f3be6986ae3e8d"
        ],
        [
          "object",
          "Closure"
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Metadata\\Property\\Factory",
      "short_class": "CachedPropertyNameCollectionFactory",
      "class": "ApiPlatform\\Core\\Metadata\\Property\\Factory\\CachedPropertyNameCollectionFactory",
      "type": "->",
      "function": "create",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Serializer\/AbstractItemNormalizer.php",
      "line": 291,
      "args": [
        [
          "string",
          "App\\Domain\\User\\Command\\ResendValidationMailCommand"
        ],
        [
          "array",
          {
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Serializer",
      "short_class": "AbstractItemNormalizer",
      "class": "ApiPlatform\\Core\\Serializer\\AbstractItemNormalizer",
      "type": "->",
      "function": "getAllowedAttributes",
      "file": "\/var\/www\/vendor\/symfony\/serializer\/Normalizer\/AbstractObjectNormalizer.php",
      "line": 278,
      "args": [
        [
          "string",
          "App\\Domain\\User\\Command\\ResendValidationMailCommand"
        ],
        [
          "array",
          {
            "resource_class": [
              "string",
              "App\\Domain\\User\\Command\\ResendValidationMailCommand"
            ],
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ],
            "api_allow_update": [
              "boolean",
              false
            ],
            "input": [
              "array",
              {
                "class": [
                  "string",
                  "App\\Domain\\User\\Command\\ResendValidationMailCommand"
                ],
                "name": [
                  "string",
                  "ResendValidationMailCommand"
                ]
              }
            ],
            "output": [
              "array",
              {
                "class": [
                  "null",
                  null
                ]
              }
            ],
            "request_uri": [
              "string",
              "\/api\/users\/resend_validation_mail"
            ],
            "uri": [
              "string",
              "http:\/\/api.renters.test\/api\/users\/resend_validation_mail"
            ],
            "api_denormalize": [
              "boolean",
              true
            ],
            "cache_key": [
              "string",
              "be175295861aa5aa1e028b51fff958fe"
            ]
          }
        ],
        [
          "boolean",
          true
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Serializer\\Normalizer",
      "short_class": "AbstractObjectNormalizer",
      "class": "Symfony\\Component\\Serializer\\Normalizer\\AbstractObjectNormalizer",
      "type": "->",
      "function": "denormalize",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Serializer\/AbstractItemNormalizer.php",
      "line": 179,
      "args": [
        [
          "array",
          []
        ],
        [
          "string",
          "App\\Domain\\User\\Command\\ResendValidationMailCommand"
        ],
        [
          "string",
          "json"
        ],
        [
          "array",
          {
            "resource_class": [
              "string",
              "App\\Domain\\User\\Command\\ResendValidationMailCommand"
            ],
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ],
            "api_allow_update": [
              "boolean",
              false
            ],
            "input": [
              "array",
              {
                "class": [
                  "string",
                  "App\\Domain\\User\\Command\\ResendValidationMailCommand"
                ],
                "name": [
                  "string",
                  "ResendValidationMailCommand"
                ]
              }
            ],
            "output": [
              "array",
              {
                "class": [
                  "null",
                  null
                ]
              }
            ],
            "request_uri": [
              "string",
              "\/api\/users\/resend_validation_mail"
            ],
            "uri": [
              "string",
              "http:\/\/api.renters.test\/api\/users\/resend_validation_mail"
            ],
            "api_denormalize": [
              "boolean",
              true
            ],
            "cache_key": [
              "string",
              "be175295861aa5aa1e028b51fff958fe"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Serializer",
      "short_class": "AbstractItemNormalizer",
      "class": "ApiPlatform\\Core\\Serializer\\AbstractItemNormalizer",
      "type": "->",
      "function": "denormalize",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/Serializer\/ItemNormalizer.php",
      "line": 71,
      "args": [
        [
          "array",
          []
        ],
        [
          "string",
          "App\\Infrastructure\\Doctrine\\Projection\\User"
        ],
        [
          "string",
          "json"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ],
            "api_allow_update": [
              "boolean",
              false
            ],
            "resource_class": [
              "string",
              "App\\Infrastructure\\Doctrine\\Projection\\User"
            ],
            "input": [
              "array",
              {
                "class": [
                  "string",
                  "App\\Domain\\User\\Command\\ResendValidationMailCommand"
                ],
                "name": [
                  "string",
                  "ResendValidationMailCommand"
                ]
              }
            ],
            "output": [
              "array",
              {
                "class": [
                  "null",
                  null
                ]
              }
            ],
            "request_uri": [
              "string",
              "\/api\/users\/resend_validation_mail"
            ],
            "uri": [
              "string",
              "http:\/\/api.renters.test\/api\/users\/resend_validation_mail"
            ],
            "api_denormalize": [
              "boolean",
              true
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Serializer",
      "short_class": "ItemNormalizer",
      "class": "ApiPlatform\\Core\\Serializer\\ItemNormalizer",
      "type": "->",
      "function": "denormalize",
      "file": "\/var\/www\/vendor\/symfony\/serializer\/Serializer.php",
      "line": 191,
      "args": [
        [
          "array",
          []
        ],
        [
          "string",
          "App\\Infrastructure\\Doctrine\\Projection\\User"
        ],
        [
          "string",
          "json"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ],
            "api_allow_update": [
              "boolean",
              false
            ],
            "resource_class": [
              "string",
              "App\\Infrastructure\\Doctrine\\Projection\\User"
            ],
            "input": [
              "array",
              {
                "class": [
                  "string",
                  "App\\Domain\\User\\Command\\ResendValidationMailCommand"
                ],
                "name": [
                  "string",
                  "ResendValidationMailCommand"
                ]
              }
            ],
            "output": [
              "array",
              {
                "class": [
                  "null",
                  null
                ]
              }
            ],
            "request_uri": [
              "string",
              "\/api\/users\/resend_validation_mail"
            ],
            "uri": [
              "string",
              "http:\/\/api.renters.test\/api\/users\/resend_validation_mail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Serializer",
      "short_class": "Serializer",
      "class": "Symfony\\Component\\Serializer\\Serializer",
      "type": "->",
      "function": "denormalize",
      "file": "\/var\/www\/vendor\/symfony\/serializer\/Serializer.php",
      "line": 142,
      "args": [
        [
          "array",
          []
        ],
        [
          "string",
          "App\\Infrastructure\\Doctrine\\Projection\\User"
        ],
        [
          "string",
          "json"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ],
            "api_allow_update": [
              "boolean",
              false
            ],
            "resource_class": [
              "string",
              "App\\Infrastructure\\Doctrine\\Projection\\User"
            ],
            "input": [
              "array",
              {
                "class": [
                  "string",
                  "App\\Domain\\User\\Command\\ResendValidationMailCommand"
                ],
                "name": [
                  "string",
                  "ResendValidationMailCommand"
                ]
              }
            ],
            "output": [
              "array",
              {
                "class": [
                  "null",
                  null
                ]
              }
            ],
            "request_uri": [
              "string",
              "\/api\/users\/resend_validation_mail"
            ],
            "uri": [
              "string",
              "http:\/\/api.renters.test\/api\/users\/resend_validation_mail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Serializer",
      "short_class": "Serializer",
      "class": "Symfony\\Component\\Serializer\\Serializer",
      "type": "->",
      "function": "deserialize",
      "file": "\/var\/www\/vendor\/api-platform\/core\/src\/EventListener\/DeserializeListener.php",
      "line": 100,
      "args": [
        [
          "array",
          []
        ],
        [
          "string",
          "App\\Infrastructure\\Doctrine\\Projection\\User"
        ],
        [
          "string",
          "json"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "postResendValidationMail"
            ],
            "api_allow_update": [
              "boolean",
              false
            ],
            "resource_class": [
              "string",
              "App\\Infrastructure\\Doctrine\\Projection\\User"
            ],
            "input": [
              "array",
              {
                "class": [
                  "string",
                  "App\\Domain\\User\\Command\\ResendValidationMailCommand"
                ],
                "name": [
                  "string",
                  "ResendValidationMailCommand"
                ]
              }
            ],
            "output": [
              "array",
              {
                "class": [
                  "null",
                  null
                ]
              }
            ],
            "request_uri": [
              "string",
              "\/api\/users\/resend_validation_mail"
            ],
            "uri": [
              "string",
              "http:\/\/api.renters.test\/api\/users\/resend_validation_mail"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\EventListener",
      "short_class": "DeserializeListener",
      "class": "ApiPlatform\\Core\\EventListener\\DeserializeListener",
      "type": "->",
      "function": "onKernelRequest",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/Debug\/WrappedListener.php",
      "line": 115,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent"
        ],
        [
          "string",
          "kernel.request"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "WrappedListener",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener",
      "type": "->",
      "function": "__invoke",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/EventDispatcher.php",
      "line": 212,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent"
        ],
        [
          "string",
          "kernel.request"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "doDispatch",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/EventDispatcher.php",
      "line": 44,
      "args": [
        [
          "array",
          [
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ]
          ]
        ],
        [
          "string",
          "kernel.request"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "\/var\/www\/vendor\/symfony\/event-dispatcher\/Debug\/TraceableEventDispatcher.php",
      "line": 145,
      "args": [
        [
          "string",
          "kernel.request"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "TraceableEventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 126,
      "args": [
        [
          "string",
          "kernel.request"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handleRaw",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 67,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/vendor\/symfony\/http-kernel\/Kernel.php",
      "line": 198,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ],
        [
          "boolean",
          true
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "Kernel",
      "class": "Symfony\\Component\\HttpKernel\\Kernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/public\/index.php",
      "line": 25,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ]
      ]
    }
  ]
}
soyuka commented 5 years ago

I think this issue exists indeed because your class is empty.

Nightbr commented 5 years ago

Ok, so I think it could be a nice feature to be able to handle Empty DTO (Command) for some use case such as ResendUserPassword, POST workflow change state, ...

I think we could discuss on this implementation. Is it link to the possibility to toggle listeners or is it built in the different part of API platform (add condition on validator, propertyInfo, ...)?

soyuka commented 5 years ago

IMO if you really want this behavior you should implement your own listener.

Nightbr commented 5 years ago

it's not a feature we want to handle in API Platform itself? I can make a PR if it's the subject here.

soyuka commented 5 years ago

I think that we should fix this in the https://github.com/api-platform/core/blob/2f941fc3c0dc9dd257da3b34a6bb3f5c21c4230c/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php#L46 instead. We should return new PropertyNameCollection([]) instead but I'm afraid it'd break some stuff :p.

teohhanhui commented 5 years ago

No, it should be fixed in PropertyInfo. null should not be returned from getProperties when the class simply has no properties.

teohhanhui commented 5 years ago

This is wrong: https://github.com/symfony/symfony/blob/v4.2.8/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php#L98

But perhaps it's done that way because PropertyInfo does not have a way to merge results from multiple extractors.

soyuka commented 5 years ago

:+1: indeed I also thought about this, we should try to fix this upstream then :/

zalesak commented 5 years ago

For me this workaround works:

  1. set "defaults"={"_api_receive"=false},
  2. set controller for operation which allow null value for $data, this disangeage PlaceholderAction
class NotificationTokensDelete
{
    public function __invoke($data = null)
    {
        return $data;
    }
}

It would by nice if PlaceholderAction allows null as value for $data - this problem raise everytime when you disable read listener and has empty body. For example, I have delete action for notification token for logged user, so there is no need to send id for entity. Read, validation listeners are disabled and everything I need is custom data persister - there is also POST endpoint for set tokens.

soyuka commented 3 years ago

Greetings! We appreciate your concern but weren't able to reproduce this issue or it is more of a question. As described in the API Platform contributing guide, we use GitHub issues for bugs and feature requests only.

For support question ("How To", usage advice, or troubleshooting your own code), you have several options:

Feel free reach one of the support channels above. In the meantime we're closing this issue.