Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
karser opened this issue Jun 13, 2019 · 9 comments
Closed
Assignees

Comments

@karser
Copy link
Contributor

karser commented Jun 13, 2019

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?

@teohhanhui
Copy link
Contributor

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.

@teohhanhui teohhanhui added the bug label Jun 14, 2019
@teohhanhui teohhanhui self-assigned this Jun 14, 2019
@karser
Copy link
Contributor Author

karser commented Jun 14, 2019 via email

@karser
Copy link
Contributor Author

karser commented Jun 14, 2019

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":[]}]}

@teohhanhui
Copy link
Contributor

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

@teohhanhui teohhanhui added invalid and removed bug labels Jun 17, 2019
@karser
Copy link
Contributor Author

karser commented Jun 17, 2019

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'

@teohhanhui
Copy link
Contributor

teohhanhui commented Jun 18, 2019

@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).

@karser
Copy link
Contributor Author

karser commented Jun 18, 2019

What would ForgotPassword case pseudo-code look like?

@teohhanhui
Copy link
Contributor

teohhanhui commented Jun 18, 2019

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

@teohhanhui
Copy link
Contributor

Fixed by #2910

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants