Core: [2.4.3] No resource class found for object of type when using DTO as output

Created on 13 Jun 2019  Â·  9Comments  Â·  Source: api-platform/core

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.
image

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?

Has a PR bug

All 9 comments

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CvekCoding picture CvekCoding  Â·  3Comments

mahmoodbazdar picture mahmoodbazdar  Â·  3Comments

ghost picture ghost  Â·  3Comments

dunglas picture dunglas  Â·  3Comments

stipic picture stipic  Â·  3Comments