Hi Folks!
I'm using api-platform for handling custom use cases:
App\UseCase\SampleUseCaseRequest:
collectionOperations:
post:
path: '/sample-path'
messenger: true
output: 'App\UseCase\SampleUseCaseResponse'
This configuration worked well with v2.4.2. After upgrading to 2.4.3 (and 2.4.4 as well) I started getting this issue:
{"@context":"\/contexts\/Error","@type":"hydra:Error","hydra:title":"An error occurred","hydra:description":"No resource class found for object of type \u0022App\\UseCase\\SampleUseCaseResponse\u0022.","trace":[{"namespace":"","short_class":"","class":"","type":"","function":"","file":"vendor\/api-platform\/core\/src\/Api\/ResourceClassResolver.php","line":54,"args":[]},{"namespace":"ApiPlatform\\Core\\Api","short_class":"ResourceClassResolver","class":"ApiPlatform\\Core\\Api\\ResourceClassResolver","type":"-\u003E","function":"getResourceClass","file":"vendor\/api-platform\/core\/src\/Util\/ResourceClassInfoTrait.php","line":52,"args":[["object","App\\UseCase\\SampleUseCaseResponse"]]},{"namespace":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing","short_class":"IriConverter","class":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing\\IriConverter","type":"-\u003E","function":"getResourceClass","file":"vendor\/api-platform\/core\/src\/Bridge\/Symfony\/Routing\/IriConverter.php","line":120,"args":[["object","App\\UseCase\\SampleUseCaseResponse"],["boolean",true]]},{"namespace":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing","short_class":"IriConverter","class":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing\\IriConverter","type":"-\u003E","function":"getIriFromItem","file":"src\/ApiPlatform\/ParamsAwareIriConverter.php","line":62
So IriConverter::getIriFromItem calls ResourceClassResolver::getResourceClass which throws InvalidArgumentException if the output is not an api resource.

As a workaround I decorated IriConverter and added fallback:
class FallbackIriConverter implements IriConverterInterface {
private $decorated;
public function __construct(IriConverterInterface $decorated) {
$this->decorated = $decorated;
}
public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
{
try {
return $this->decorated->getIriFromItem($item, $referenceType);
} catch (InvalidArgumentException $e) {
return '';
}
}
}
I'm wondering is it a bug and how to better approach this issue? Is IRI critically important for the non-resource output?
Can you share the full stack trace please? Serializing with an output DTO is not supposed to hit this code path, but clearly there's a bug somewhere.
Sure, I'll attach the stack trace in a few hours.
On Fri, Jun 14, 2019, 15:38 Teoh Han Hui <[email protected] wrote:
Can you share the full stack trace please? Serializing with an output DTO
is not supposed to hit this code path, but clearly there's a bug somewhere.—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/api-platform/core/issues/2860?email_source=notifications&email_token=AAMY6GPDAYDGXRYHAI7MZ53P2OGKXA5CNFSM4HX7S5WKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXWVK7Y#issuecomment-502093183,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAMY6GMDNBCUXMQJM5LVYB3P2OGKXANCNFSM4HX7S5WA
.
Ok, it is ApiPlatform\Core\Exception\InvalidArgumentException. Here is the full trace:
{"@context":"\/contexts\/Error","@type":"hydra:Error","hydra:title":"An error occurred","hydra:description":"No resource class found for object of type \u0022App\\UseCase\\SampleUseCaseResponse\u0022.","trace":[{"namespace":"","short_class":"","class":"","type":"","function":"","file":"vendor\/api-platform\/core\/src\/Api\/ResourceClassResolver.php","line":54,"args":[]},{"namespace":"ApiPlatform\\Core\\Api","short_class":"ResourceClassResolver","class":"ApiPlatform\\Core\\Api\\ResourceClassResolver","type":"-\u003E","function":"getResourceClass","file":"vendor\/api-platform\/core\/src\/Util\/ResourceClassInfoTrait.php","line":52,"args":[["object","App\\UseCase\\SampleUseCaseResponse"]]},{"namespace":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing","short_class":"IriConverter","class":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing\\IriConverter","type":"-\u003E","function":"getResourceClass","file":"vendor\/api-platform\/core\/src\/Bridge\/Symfony\/Routing\/IriConverter.php","line":120,"args":[["object","App\\UseCase\\SampleUseCaseResponse"],["boolean",true]]},{"namespace":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing","short_class":"IriConverter","class":"ApiPlatform\\Core\\Bridge\\Symfony\\Routing\\IriConverter","type":"-\u003E","function":"getIriFromItem","file":"src\/ApiPlatform\/FallbackIriConverter.php","line":63,"args":[["object","App\\UseCase\\SampleUseCaseResponse"],["integer",1]]},{"namespace":"App\\ApiPlatform","short_class":"FallbackIriConverter","class":"App\\ApiPlatform\\FallbackIriConverter","type":"-\u003E","function":"getIriFromItem","file":"vendor\/api-platform\/core\/src\/EventListener\/WriteListener.php","line":94,"args":[["object","App\\UseCase\\SampleUseCaseResponse"]]},{"namespace":"ApiPlatform\\Core\\EventListener","short_class":"WriteListener","class":"ApiPlatform\\Core\\EventListener\\WriteListener","type":"-\u003E","function":"onKernelView","file":"vendor\/symfony\/event-dispatcher\/EventDispatcher.php","line":212,"args":[["object","Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent"],["string","kernel.view"],["object","Symfony\\Component\\EventDispatcher\\EventDispatcher"]]},{"namespace":"Symfony\\Component\\EventDispatcher","short_class":"EventDispatcher","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","type":"-\u003E","function":"doDispatch","file":"vendor\/symfony\/event-dispatcher\/EventDispatcher.php","line":44,"args":[["array",[["array",["array",[["object","ApiPlatform\\Core\\Validator\\EventListener\\ValidateListener"],["string","onKernelView"]]],["array",[["object","ApiPlatform\\Core\\EventListener\\WriteListener"],["string","onKernelView"]]],["array",[["object","ApiPlatform\\Core\\EventListener\\SerializeListener"],["string","onKernelView"]]],["array",[["object","ApiPlatform\\Core\\EventListener\\RespondListener"],["string","onKernelView"]]]]],["string","kernel.view"],["object","Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent"]]},{"namespace":"Symfony\\Component\\EventDispatcher","short_class":"EventDispatcher","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","type":"-\u003E","function":"dispatch","file":"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":"-\u003E","function":"handleRaw","file":"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":"-\u003E","function":"handle","file":"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":"-\u003E","function":"handle","file":"vendor\/symfony\/http-kernel\/Client.php","line":68,"args":[["object","Symfony\\Component\\HttpFoundation\\Request"],["integer",1],["boolean",true]]},{"namespace":"Symfony\\Component\\HttpKernel","short_class":"Client","class":"Symfony\\Component\\HttpKernel\\Client","type":"-\u003E","function":"doRequest","file":"vendor\/symfony\/framework-bundle\/Client.php","line":131,"args":[["object","Symfony\\Component\\HttpFoundation\\Request"]]},{"namespace":"Symfony\\Bundle\\FrameworkBundle","short_class":"Client","class":"Symfony\\Bundle\\FrameworkBundle\\Client","type":"-\u003E","function":"doRequest","file":"vendor\/symfony\/browser-kit\/Client.php","line":405,"args":[["object","Symfony\\Component\\HttpFoundation\\Request"]]},{"namespace":"Symfony\\Component\\BrowserKit","short_class":"Client","class":"Symfony\\Component\\BrowserKit\\Client","type":"-\u003E","function":"request","file":"tests\/Functional\/ApiClient.php","line":47,"args":[["string","POST"],["string","http:\/\/api.localhost\/sample-path"],["array",[]],["array",[]],["array",{"HTTP_USER_AGENT":["string","Symfony BrowserKit"],"HTTP_HOST":["string","api.localhost"],"HTTP_ACCEPT":["string","application\/ld+json"],"CONTENT_TYPE":["string","application\/ld+json"],"HTTP_REFERER":["string","http:\/\/api.localhost\/sample-path"],"HTTPS":["boolean",false]}]},{"namespace":"App\\Tests\\Functional","short_class":"ApiClient","class":"App\\Tests\\Functional\\ApiClient","type":"-\u003E","function":"callApi","file":"tests\/Functional\/Api\/MyTest.php","line":115,"args":[["string","POST"],["string","\/sample-path"],["array",[]]]},{"namespace":"App\\Tests\\Functional\\Api","short_class":"MyTest","class":"App\\Tests\\Functional\\Api\\MyTest","type":"-\u003E","function":"testMethod","file":"vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","line":1148,"args":[]},{"namespace":"PHPUnit\\Framework","short_class":"TestCase","class":"PHPUnit\\Framework\\TestCase","type":"-\u003E","function":"runTest","file":"vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","line":842,"args":[]},{"namespace":"PHPUnit\\Framework","short_class":"TestCase","class":"PHPUnit\\Framework\\TestCase","type":"-\u003E","function":"runBare","file":"vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","line":675,"args":[]},{"namespace":"PHPUnit\\Framework","short_class":"TestResult","class":"PHPUnit\\Framework\\TestResult","type":"-\u003E","function":"run","file":"vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","line":796,"args":[["object","App\\Tests\\Functional\\Api\\MyTest"]]},{"namespace":"PHPUnit\\Framework","short_class":"TestCase","class":"PHPUnit\\Framework\\TestCase","type":"-\u003E","function":"run","file":"vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","line":750,"args":[["object","PHPUnit\\Framework\\TestResult"]]},{"namespace":"PHPUnit\\Framework","short_class":"TestSuite","class":"PHPUnit\\Framework\\TestSuite","type":"-\u003E","function":"run","file":"vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","line":750,"args":[["object","PHPUnit\\Framework\\TestResult"]]},{"namespace":"PHPUnit\\Framework","short_class":"TestSuite","class":"PHPUnit\\Framework\\TestSuite","type":"-\u003E","function":"run","file":"vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","line":750,"args":[["object","PHPUnit\\Framework\\TestResult"]]},{"namespace":"PHPUnit\\Framework","short_class":"TestSuite","class":"PHPUnit\\Framework\\TestSuite","type":"-\u003E","function":"run","file":"vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","line":622,"args":[["object","PHPUnit\\Framework\\TestResult"]]},{"namespace":"PHPUnit\\TextUI","short_class":"TestRunner","class":"PHPUnit\\TextUI\\TestRunner","type":"-\u003E","function":"doRun","file":"vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","line":206,"args":[["object","PHPUnit\\Framework\\TestSuite"],["array",{"listGroups":["boolean",false],"listSuites":["boolean",false],"listTests":["boolean",false],"listTestsXml":["boolean",false],"loader":["null",null],"useDefaultConfiguration":["boolean",true],"loadedExtensions":["array",[]],"notLoadedExtensions":["array",[]],"testsuite":["string","functional"],"configuration":["object","PHPUnit\\Util\\Configuration"],"printer":["string","PHPUnit\\Util\\Log\\TeamCity"],"testSuffixes":["array",[["string","Test.php"],["string",".phpt"]]],"debug":["boolean",false],"filter":["boolean",false],"listeners":["array",[["object","Symfony\\Bridge\\PhpUnit\\Legacy\\SymfonyTestsListenerForV7"],["object","App\\Tests\\Functional\\DBSchemaPHPUnitListener"]]],"backupGlobals":["boolean",false],"bootstrap":["string","vendor\/autoload.php"],"colors":["string","auto"],"testdoxGroups":["array",[]],"testdoxExcludeGroups":["array",[]],"addUncoveredFilesFromWhitelist":["boolean",true],"backupStaticAttributes":["null",null],"beStrictAboutChangesToGlobalState":["null",null],"beStrictAboutResourceUsageDuringSmallTests":["boolean",false],"cacheResult":["boolean",false],"cacheTokens":["boolean",false],"columns":["integer",80],"convertDeprecationsToExceptions":["boolean",true],"convertErrorsToExceptions":["boolean",true],"convertNoticesToExceptions":["boolean",true],"convertWarningsToExceptions":["boolean",true],"crap4jThreshold":["integer",30],"disallowTestOutput":["boolean",false],"disallowTodoAnnotatedTests":["boolean",false],"defaultTimeLimit":["integer",0],"enforceTimeLimit":["boolean",false],"excludeGroups":["array",[]],"failOnRisky":["boolean",false],"failOnWarning":["boolean",false],"executionOrderDefects":["integer",0],"groups":["array",[]],"processIsolation":["boolean",false],"processUncoveredFilesFromWhitelist":["boolean",false],"randomOrderSeed":["integer",1560524700],"registerMockObjectsFromTestArgumentsRecursively":["boolean",false],"repeat":["boolean",false],"reportHighLowerBound":["integer",90],"reportLowUpperBound":["integer",50],"reportUselessTests":["boolean",true],"reverseList":["boolean",false],"executionOrder":["integer",0],"resolveDependencies":["boolean",false],"stopOnError":["boolean",false],"stopOnFailure":["boolean",false],"stopOnIncomplete":["boolean",false],"stopOnRisky":["boolean",false],"stopOnSkipped":["boolean",false],"stopOnWarning":["boolean",false],"stopOnDefect":["boolean",false],"strictCoverage":["boolean",false],"timeoutForLargeTests":["integer",60],"timeoutForMediumTests":["integer",10],"timeoutForSmallTests":["integer",1],"verbose":["boolean",false]}],["boolean",true]]},{"namespace":"PHPUnit\\TextUI","short_class":"Command","class":"PHPUnit\\TextUI\\Command","type":"-\u003E","function":"run","file":"vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","line":162,"args":[["array",[["string","vendor\/phpunit\/phpunit\/phpunit"],["string","--testsuite"],["string","functional"],["string","--configuration"],["string","phpunit.xml.dist"],["string","--teamcity"]]],["boolean",true]]},{"namespace":"PHPUnit\\TextUI","short_class":"Command","class":"PHPUnit\\TextUI\\Command","type":"::","function":"main","file":"vendor\/phpunit\/phpunit\/phpunit","line":61,"args":[]}]}
@karser You should not return SampleUseCaseResponse from your controller (or GetResponseForControllerResultEvent::setControllerResult).
What you should do is to implement a DataTransformer that transforms your resource class to the output class.
I don't think this issue is invalid, I believe that there is a misunderstanding of the use case. Let's take a look at the forgot password case. Here is pseudo-code:
ForgotPasswordRequest {
public $email;
}
ForgotPasswordResponse {
public $emailSent;
}
ForgotPasswordUseCase implements MessageHandlerInterface {
public function __invoke(ForgotPasswordRequest $request): ForgotPasswordResponse;
}
Originally I was using the @lyrixx approach. Then after the output and messenger support were added to api-platform, the following worked quite well (until v2.4.3):
App\UseCase\ForgotPasswordRequest:
collectionOperations:
post:
path: '/users/forgot-password-request'
messenger: true
output: 'App\UseCase\ForgotPasswordResponse'
@karser Yes, the way you use it is wrong:
public function __invoke(ForgotPasswordRequest $request): ForgotPasswordResponse;
You're expected to return ForgotPasswordRequest. It's then the job of the DataTransformer to transform the resource class to the output class (this happens during serialization).
What would ForgotPassword case pseudo-code look like?
In your case, I'd say ForgotPasswordRequest should be the input class. The resource class could be the User, ~or something like SendResetPasswordTokenCommand for CQRS~ (I'm not qualified enough to advise on CQRS lol).
Fixed by #2910