| Q | A
| ---------------- | -----
| Bug report? | no
| Feature request? |no
| BC Break report? | no
| RFC? | no
| Version/Branch | ^0.12
Documentation: @Field
This example can not work:
<?php
/**
* @GQL\Type()
*/
class Hero {
/**
* @GQL\Field(
* name="friends",
* type="[Hero]",
* args={@GQL\Arg(name="limit", type="Int")}
* )
*/
public function getFriends(int $limit) {
return array_slice($this->friends, 0, $limit);
}
}
What does $this point to?
Also the doc states: If @Field is on a method [...] and no resolve attribute is defined [...] it defaults to @=value.methodName(...args)
Because value is then the value from parent type, the method on the Hero type is never invoked.
I am not even sure if it is just doc-wise confusing and works, just differently, or an issue/bug.
This forces me to literally define every call to an @Field annotation method via resolve.
My intent is not to mix the annotation into my domain, maybe the annotation are currently implemented in mind doing exactly that (annotating domain)? This would explain much :)
Hey @akomm :-)
The point of annotations, is to be able to have a bridge between manipulated PHP objects and GQL types.
This means that in this context, $this will be an instance of the Hero class.
Usually, you'll use Doctrine to manage your objects. Your repositories methods will return collections of entities. For each PHP class, you'll have an associated GQL type. So, when the resolver will try to resolve the values of each field, it will be given your class instance, and it will know what to do.
In this example, we have a GQL type Hero with a field friends. When the GraphQL resolver will be call, it will receive a PHP object of class Hero and it will know that to resolve the field friends of the GQL type Hero, it will have to resolve @=value.methodName(..args) or in this case $hero->getFriends($limit).
Hope it'll help :-)
Thanks!
This is what I also assumed/understood out of the documentation. However:
The code in the getFriends method on Hero will never be invoked, because the @Field annotation has no resolve attribute. Therefore value.methodName(...args) is invoked. So for getFrieds to be invoked, the value must be the Hero class. However Hero is not an entity with friends, but a GQL type/service. At least I would expect it.
Example:
# Query.php
use Overblog\GraphQLBundle\Annotation as GQL;
/**
* @GQL\Type()
*/
final class Query
{
/**
* @GQL\Field(type="DateTime!")
*/
public function time()
{
return new \DateTimeImmutable();
}
}
# DateTime.php
use Overblog\GraphQLBundle\Annotation as GQL;
/**
* @GQL\Type()
*/
final class DateTime
{
/**
* @GQL\Field(
* type="String!",
* args={@GQL\Arg(name="format", type="String!")}
* )
*/
public function format(string $format)
{
return sprintf("invoked with format: %s", $format);
}
/**
* @GQL\Field(
* type="String!",
* args={@GQL\Arg(name="format", type="String!")}
* )
*/
public function formatX(string $format)
{
return sprintf("invoked with format: %s", $format);
}
}
Query 1:
query q {
time {
format(format: "d.m.Y H:i:s")
}
}
Result 1:
{
"data": {
"time": {
"format": "18.07.2019 15:07:27"
}
}
}
Query 2:
query q {
time {
formatX(format: "d.m.Y H:i:s")
}
}
Result 2:
errors [...] Call to undefined method DateTimeImmutable::formatX()
While the outcome makes sense to me, in my opinion it does not make sense for it to work like that. Basically Hero is a service and its method used as resolver callback. The annotation adds to the typing configuration. Now Hero can obviously not be the value. With yaml I could pass the value via resolve(..., [value]) in expression. I could also define the same in resolve with annotation, but that kills the purpose of having a real PHP file with method API and then duplicate invocation on said method via resolve attribute. I would also have to reference the Hero service/resolver somehow, via alias (which does not exist at this point), or FQCN while being already in the resolver class.
Hum... I think you mix things up.
You tell your GraphQL schema that time will return a GraphQL type with name DateTime (it's on a GraphQL point of view).
But que method returns a PHP object of class DateTimeImmutable. It's gonna be the value of your time field, so it'll try to get the properties from this object and failed.
Correct. And since DateTimeInterface has format method and is the value, its invoked. I understand it from the start :). The formatX was added exactly to show that it not only invoked format due to the method on value, but even if the method does not exist on value its not invoked on the resolver.
However, how can you return a value then, if you do not define its type? I have to define the DateTime type. Then I have to define fields. For that I have to define properties on the annotated DateTime class. But it does not make any sense, as I do not return this class. And also when I add format as property & field of type String, I can not use it with args (format). So then I have to define it as method. But then I define it as method, the method is never invoked, for the reason above, its just dead code. And if I want to add a virtual field on the type, which does not directly exist on the php DateTime, it will also not be invoked. I will have to define resolve.
The same on the Hero class. The hero class is not the entity (and is therefore not the value), so its getFriends does not get invoked.
The idea is that the class with the annotations, will be the class holding the value.
So, in your example, you should either return an instance of your DateTime class itself:
/**
* @GQL\Type()
*/
final class Query
{
/**
* @GQL\Field(type="DateTime!")
*/
public function time()
{
return new DateTime();
}
}
.... or ....
You could define the DateTime resolvers knowing that you will get instances of DateTimeImmutable in the value.
/**
* @GQL\Type()
*/
final class DateTime
{
/**
* @GQL\Field(
* type="String!",
* args={@GQL\Arg(name="format", type="String!")},
* resolver="@=value.format(args['format'])"
* )
*/
public $format;
/**
* @GQL\Field(
* type="String!",
* args={@GQL\Arg(name="format", type="String!")}
* resolver="@=value.format(...)
* )
*/
public $formatX;
}
The annotations allow you to define everything at once. Your Hero entity, will have a corresponding Hero GraphQL type.
The parser keep a map of PHP Classes <=> GraphQL types, so it knows if a given PHP class has a corresponding GraphQL type and if a given GraphQL type has a corresponding PHP class.
So, yes, Hero will probably be an entity. The purpose of the annotations is to ease the GraphQL <-> PHP process.
So, yes,
Herowill probably be an entity. The purpose of the annotations is to ease the GraphQL <-> PHP process.
This is what I mean before, if the intent is to have the typing embedded into domain (entities/model, etc.). I actually do not want to do it. I want my API be flexible projection into my Model, but it must not be 1:1 map. In my opinion one of great advantages with graphql.
I also initially assumed types are like the root types services, but they are not. That is why I excluded the option to return the type class.
So I see, @Provider can be used for such fields, that require dependencies as it does get stuff injected from container.
This however ends up at the same situations as before, just at a different place:
@Provider I have to still explicitly make resolve logic.# QueryType.php
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Overblog\GraphQLBundle\Annotation as GQL;
/**
* @GQL\Type(name="Query")
*/
final class QueryType
{
/** @var EntityManagerInterface */
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* @GQL\Field(type="[User!]!")
*/
public function users(): array
{
return $this->entityManager->getRepository(User::class)->findAll();
}
}
# UserType.php
use Overblog\GraphQLBundle\Annotation as GQL;
/**
* @GQL\Type(name="User")
*/
final class UserType
{
/**
* @GQL\Field(
* type="String!",
* resolve="@=value.id().toString()"
* )
*/
public $id;
/**
* @GQL\Field(type="String!")
*/
public $username;
/**
* @GQL\Field(type="String!")
*/
public $name;
}
# UserFriendsProvider.php
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Overblog\GraphQLBundle\Annotation as GQL;
/**
* @GQL\Provider()
*/
final class UserFriendsProvider
{
/** @var EntityManagerInterface */
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* @GQL\Query(targetType="User", type="[User!]")
*/
public function friends()
{
return $this->entityManager->getRepository(User::class)->findAll();
}
}
The issue is now, how you get the value via resolve into friends(). Even if I write complex resolve expression to fetch the FQCN service, etc. It still fails on auto-guessing before it even gets to resolve as soon as I make the signature friend(User $user).
What I want to understand is simply, whether the annotation feature is focused on being used inside domain and therefore my attempts to do it differently are the core issue, or not. I know such things might be opinionated and I respect it and people get along with stuff different ways, etc.. :) I simply need to understand whats the case.
I think the main confusion was that it was not clear, whether Hero class is supposed to be a pure resolver service or an entity. This should be now clear. And I guess if you do not use other mappings in combination you have no choice but to either create types on with annotation that map over model in resolve, or annotate model directly.
I will make a PR to clarify this in docs (I mean Hero = Entity, not resolver service).
I like the annotation for resolving, similar to graphql-files + mapping, but mapping has some limitation and is to verbose, this is why I checked annotation. I will see if I can combine it with other annotation types and it work or not, maybe it is still somehow possible.
Thanks for your answers @Vincz .
I'm using the annotations and it's all I need. For example, an entity with annotations, a repository as provider and you are good to go. You don't need anything else. The parser will even use the Doctrine annotations (if it find some) in order auto-guess types. At the moment, there is things you can only do with annotations (but there is nothing you can't do with them).
I really hope you understood correctly. If not, I will try to take time this weekend to make a really small and simple demo repository.
@Vincz I know what you mean. My pivoting point is, I am not certain if I want to have annotations all over the place that makes up my schema. It would go over different namespaces, etc. I like to have things clear so its simple to understand. While in some places it might get exactly that with this method, crossing boundaries feels weird to me. maybe I am wrong...
Also one thing so far I could not find out, is how to pass value to @Provider method. Even if I specify resolve like I would with yaml, I have to call the resolver via FQCN as there is no alias concept (which is actually good, don`t want to mess with strings), so it must be FQCN in expression, which sucks. However even if I do it, and inject the arg, it still fails error that the type in signature can not be guessed. Probably due to the reason its checked before resolve (attribute) expression is processed?
About the annotation all over the place, you can always dump your schema to have a clearer picture. We could also add the ability to dump a schema with the file location in comment for each type.
In the case of @Provider, provider classes must be services available with FQCN. So, the default resolver will do something like $container->get('<FQCN of the service>')->method(...).
The annotations parser will try to ease the process and take shortcuts about your definitions, but at the end of the day, you can redefine everything as if it was YAML.
If you want to define the args used, just use the @Arg annotation.
If you want to pass values to your resolver method, just use the resolve attribute on your @Field annotation to define how the function must be called.
For me, I like it this way, because I have the GraphQL declaration and the way the resolver is supposed to be called in one place. If I want to change a argument or whatever, I see all at once: The GraphQL definition and the way the resolver is called.
Let's say I add a @Field to Hero via @Provider, which the Hero entity itself does not have. T requires instance of Hero as reference (e. G. lookup friends, which are not mapped on Hero entity directly). I will have to still define resolve attribute right? Now lets say the @Provider is a repository, but I do not want to have a constraint between method on repository and field name on Hero, I can also rename it via @Field(name=...).
# HeroRepository.php class HeroRepository ...
/** @Query(targetType="Hero", name="friends", resolve="@=serv("App\\Some\\Namespace\\HeroRepository").getFriendsOfHero(value)") */
public function getFriendsOfHero(Hero $hero) { /* ... */ }
Feels very redundant having to do it, or did I miss something?
fixed to @Query and added targetType