Input: Cached type in registry is not the type returned by type mapper
NormySan opened this issue · comments
Hey,
I'm having a weird issue with input types where I get the exception in the title "Cached type in the registry is not the type returned by type mapper".
For some reason, the type stored in the cache is not the same as the one from the type mapper even if the input class they both reference is the same.
The mutation that I try to run only fails when I use variables to run the mutation, If I run the mutation without variables and provide the values directly it all works but this is not possible to do in the frontend since everything is automatically generated and requires the type to be provided.
Failing mutation
mutation CreateFacility($input: FacilityInput!) {
createFacility(input: $input) {
id
}
}
Working mutation
mutation CreateFacility {
createFacility(input: {
name: "Facility"
subdomain: "facility"
}) {
id
}
}
Input type
#[Input]
class FacilityInput
{
#[Field]
public string $name;
#[Field]
public string $subdomain;
}
Controller/Resolver
class FacilityResolver
{
public function __construct(
private readonly CommandBus $commandBus,
) {
}
#[Mutation]
#[Logged]
public function createFacility(
#[InjectUser] Employee $employee,
FacilityInput $input
): Facility {
/** @var Facility $facility */
$facility = $this->commandBus->dispatch(new CreateFacilityCommand(
(string) $employee->organizationId,
$input->name,
$input->subdomain,
));
return $facility;
}
}
The cached type
object(TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType)[782]
public 'name' => string 'FacilityInput' (length=13)
public 'description' => null
public 'astNode' => null
public 'config' =>
array (size=3)
'name' => string 'FacilityInput' (length=13)
'fields' =>
object(Closure)[775]
virtual 'closure' 'TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType::TheCodingMachine\GraphQLite\Types\{closure}'
public 'static' =>
array (size=5)
...
'interfaces' =>
object(Closure)[842]
virtual 'closure' 'TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType::TheCodingMachine\GraphQLite\Types\{closure}'
public 'static' =>
array (size=3)
...
public 'extensionASTNodes' =>
array (size=0)
empty
private 'fields' (GraphQL\Type\Definition\TypeWithFields) => null
public 'resolveFieldFn' => null
private 'interfaces' (GraphQL\Type\Definition\ObjectType) => null
private 'interfaceMap' (GraphQL\Type\Definition\ObjectType) => null
private ?string 'status' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => string 'frozen' (length=6)
private array 'fieldsCallables' (TheCodingMachine\GraphQLite\Types\MutableObjectType) =>
array (size=0)
empty
private ?array 'fields' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => null
private ?string 'className' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => string 'App\API\Dashboard\Input\FacilityInput' (length=37)
The type from the type mapper
object(TheCodingMachine\GraphQLite\Types\InputType)[1057]
public 'name' => string 'FacilityInput' (length=13)
public 'description' => null
public 'astNode' => null
public 'config' =>
array (size=3)
'name' => string 'FacilityInput' (length=13)
'description' => null
'fields' =>
object(Closure)[1063]
virtual 'closure' '$this->TheCodingMachine\GraphQLite\Types\{closure}'
public 'static' =>
array (size=4)
...
public 'this' =>
&object(TheCodingMachine\GraphQLite\Types\InputType)[1057]
public 'extensionASTNodes' => null
private 'fields' (GraphQL\Type\Definition\InputObjectType) => null
protected string 'status' => string 'pending' (length=7)
protected array 'fieldsCallables' =>
array (size=0)
empty
protected ?array 'finalFields' => null
private array 'constructorInputFields' =>
array (size=0)
empty
private array 'inputFields' =>
array (size=0)
empty
private string 'className' => string 'App\API\Dashboard\Input\FacilityInput' (length=37)
private ?TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface 'inputTypeValidator' => null
@NormySan what cache adapter are you using? I'm not familiar with this error. It looks like you're doing things correctly.
@oojacoboo I'm using a cache pool from Symfony. Only inputs causing this problem so far, all other types seems to be working properly.
From the cached type:
private ?string 'className' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => string 'App\API\Dashboard\Input\FacilityInput' (length=37)
From the type mapper:
private string 'className' => string 'App\API\Dashboard\Input\FacilityInput' (length=37)
What's the stack-trace look like? How is the comparison being made?
@oojacoboo This is the stack trace:
https://gist.github.com/NormySan/d505993a1817843f7c23aa2f5a39acc0
The exception is thrown here in the recursive type mapper:
@NormySan do you use this same input type in multiple mutations? Can you confirm that the cached type and the type from the type mapper are effectively the same object? I think the issue here is that it's using !==
instead of !=
. Can you test with !=
and see if that resolves the issue for you?
@oojacoboo Yeah, I'm using the input type in multiple mutations, one is for updating and one is for creating. I tried changing the comparison to just using !=
and I'm getting the same exception.
@NormySan if you're certain in your testing that it was changed and executed (not cached), and that comparison is failing, can you identify what's different about the two objects and what's causing it to fail that comparison?
@oojacoboo I think it's failing because they are entirely different objects, I'm going to set up a basic reproduction of my codebase, maybe that will help with identifying the underlying issue.
Here is a simple reproduction of the exception in a simple Symfony application.
https://github.com/NormySan/graphqlite-input-error
It does not use the Symfony bundle but instead uses a custom implementation. The GraphQL request is handled by the App\GraphQL\RequestHandler
class, this class is created through the App\GraphQL\RequestHandlerFactory
.
So far I've narrowed it down to the following:
- An attempt is made to load the
FacilityInput
type. - All type mappers are called where it ends up in the
RecursiveTypeMapper
in themapNameToType
method. - This method then calls the
mapNameToType
method on the type mapper. - This results in a
TypeAnnotatedObjectType
being returned and registered in the type registry. - Later on a call is made to the
RecursiveTypeMapper
to themapClassToInputType
method to get the input type. - This results in an
InputType
object being created instead by the type mapper which is not the same type of object that was cached earlier on. - Because the objects are not the same it crashes with the exception "Cached type in the registry is not the type returned by type mapper".
I'm not sure what the expected type in the cache should be but I'm guessing that it should be the InputType
object and not the TypeAnnotatedObjectType
object?
@NormySan so, I think the issue here is that you're using the same Input type for multiple operations. I'm not going to assume this is an incorrect design decision on your part, although it very well could be. It's mostly recommended to have a unique Input type per operation. I know this is how our API is designed and likely the reason we haven't run into this error - same for most others.
That said, looking at the code, it seems clear that it's doing a direct comparison of a cached version of the Input type and the one currently being processed by the type mapper.
I don't have enough insight or feedback on the overall use of this. But, it seems like a fresh look at this caching logic needs to be had.
- What is cached and how is that used?
- How much value are we getting out of the cache?
- If the cache can't be used, do we need to throw an Exception, or can the non-cached version be returned?
- What possible performance implications might be had from the design decision?
I don't have any answers to these questions, but it seems this is the path forward. Then we can assess a refactor of the design and PR.
Tried using two different input types and I'm still getting the same error so must be something else. I did try to set up a very minimal test based on the no-framework instructions and having the same name for the input type worked well in this case so it must be something else in Symfony or my setup causing the problem.
If I can locate the underlying cause and if it's in this package I will try to make a PR.
Similar issue in EnumTypeMapper.php. Need to add $enumClass = ltrim($enumClass, '\\');
in mapByClassName()
. Because of the slash does not find the class in the cache.
@NormySan did you try ommiting the Input suffix on the PHP Class?
Its automatically added and i do remember having issues if i added it by accident.
@Lappihuan That solved it, I would really like my inputs to be named like that though since it creates a lot of ambiguity if the input is named the same as the entities. Going to have a look at what part of the library that might be causing that issue.
I've been trying to find a cause as to why some inputs are created as TypeAnnotatedObjectType
while others are created as InputType
but have yet to find a clear reason for this. The code jumps so much back and forth and the mappers are calling each other back and forth so it's very hard to get a feel for what the code is doing.
I've also been trying to find out why the name of the input might be causing issues but I can't find any reason for this either. The naming strategy looks to be working fine, this was the only place I could find where a name is being decided.
@NormySan I agree it can be confusing. Have a look at this doc page for some insight into the type mapper
https://graphqlite.thecodingmachine.io/docs/internals
Also, regarding the Input name, have you tried using the following?
#[Input(name: 'FacilityInput')]
class FacilityInput
Yeah, I read the docs and it helped a bit to get started on where to look. Tried assigning a name to the input also but didn't work. I've come to the conclusion that none of the input types in my API are working.
we use input on entities, but that might now work for you
We just have our inputs separate from our models, entirely, and within our GraphQL namespace along with our operations.
I do the same, everything related to the GraphQL API:s is separate from the rest of the code to create a clear boundary between API and "Domain".
I think I might have found something related to why the types are not the same:
- The
RecursiveTypeMapper
makes a call to themapNameToType
method here:
https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/RecursiveTypeMapper.php#L487
- The result from the call is a
TypeAnnotatedObjectType
object that is created by theTypeGenerator
when themapAnnotatedObject
method is called. This method is called from theAbstractTypeMapper
in themapNameToType
method. It looks like the input is considered a regular type by this code so it never continues to the input part later on.
https://github.com/thecodingmachine/graphqlite/blob/master/src/TypeGenerator.php#L43
https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/AbstractTypeMapper.php#L323
-
The
TypeAnnotatedObjectType
has now been created and returned to theRecursiveTypeMapper
. Since the type has not been cached in the type registry the type is then cached by calling theregisterType
method on theTypeRegistry
class. -
Later in the code the
mapClassToInputType
method on theRecursiveTypeMapper
is called. Here it attempts to find the type in theclassToInputTypeCache
but it's not there so a call is made to themapClassToInputType
method on the type mapper. This call results in anInputType
object being created.
https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/RecursiveTypeMapper.php#L393
- The
InputType
type is this time created by themapClassToInputType
method in theAbstractTypeMapper
class.
https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/AbstractTypeMapper.php#L296
- The
RecursiveTypeMapper
continues and checks if the type exists in the type registry which it does but the object that has been registered here is theTypeAnnotatedObjectType
that was done previously when themapNameToType
method was called in the same class. But this time around anInputType
is created instead so when a comparison is made on these objects they are not the same and the runtime exception is thrown.
My question here then is why do the mapNameToType
and mapClassToInputType
not generate the same end result. My expectations would be that they both return an InputType
and that calling either of the methods result in an InputType
being cached in the type registry.
A change to the mapNameToType
method in the AbstractTypeMapper
class where it attempts to map a factory or input first solves the issue for me. No tests are failing with this change, maybe it would be good to add some regression tests to make sure this never happens again.
Edit: Looks like there is a failing test, will have a look at that.
@NormySan instead of re-ordering the operations. I think we should focus on the checks that determine which type is being mapped where.
Are you using the Type
attribute on your Input type class? Or just the Input
attribute?
@oojacoboo I'm using the Input
attribute.
Here is another example repo that uses no framework that also has the same problem with the inputs as the one I linked to previously.
@NormySan iirc there is semthing odd in nested input types i recently encountered which required me to "manualy" set inputType propery on Field/Input/Type attribute.
I did not have time to investigate the issue in depth but i suspect there is inconsitent name conversion for PHP->GQL.
@Lappihuan Might be, I don't really have any more time to investigate the issue either, unfortunately.
Encountered this as well. And the issue was also mentioned in #248.
I did some digging and the issue was introduced as part of #458 since now the AnnotationReader::getTypeAnnotation
method returns classes annotated with #[Input]
as if they were #[Type]
classes. Then, when mapping the type, it will end up as an output type, because that conversions happens before #[Factory]
and #[Input]
. I'm not sure what the change intended to solve exactly, but maybe you can shed some light on this @oojacoboo. Thanks @NormySan for the excellent and well-described minimal test repo. It made it very easy to track down.
If we revert this change from
$type = $this->getClassAnnotation($refClass, Type::class)
?? $this->getClassAnnotation($refClass, Input::class);
back to
$type = $this->getClassAnnotation($refClass, Type::class);
everything works as expected.
Change: 83357f0#diff-b4cb271cb57cae8e3c9c55d3590bab4ea0781066f699d94ac970f89f8b7e06b4R269
#532 has been merged. Please confirm that this issue is now resolved.