Giter VIP home page Giter VIP logo

graphql-doctrine's Introduction

GraphQL Doctrine

Build Status Code Quality Code Coverage Total Downloads Latest Stable Version License Join the chat at https://gitter.im/Ecodev/graphql-doctrine

A library to declare GraphQL types from Doctrine entities, PHP type hinting, and attributes, and to be used with webonyx/graphql-php.

It reads most information from type hints, complete some things from existing Doctrine attributes and allow further customizations with specialized attributes. It will then create ObjectType and InputObjectType instances with fields for all getter and setter respectively found on Doctrine entities.

It will not build the entire schema. It is up to the user to use automated types, and other custom types, to define root queries.

Quick start

Install the library via composer:

composer require ecodev/graphql-doctrine

And start using it:

<?php

use GraphQLTests\Doctrine\Blog\Model\Post;
use GraphQLTests\Doctrine\Blog\Types\DateTimeType;
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Doctrine\DefaultFieldResolver;
use GraphQL\Doctrine\Types;
use Laminas\ServiceManager\ServiceManager;

// Define custom types with a PSR-11 container
$customTypes = new ServiceManager([
    'invokables' => [
        DateTimeImmutable::class => DateTimeType::class,
        'PostStatus' => PostStatusType::class,
    ],
    'aliases' => [
        'datetime_immutable' => DateTimeImmutable::class, // Declare alias for Doctrine type to be used for filters
    ],
]);

// Configure the type registry
$types = new Types($entityManager, $customTypes);

// Configure default field resolver to be able to use getters
GraphQL::setDefaultFieldResolver(new DefaultFieldResolver());

// Build your Schema
$schema = new Schema([
    'query' => new ObjectType([
        'name' => 'query',
        'fields' => [
            'posts' => [
                'type' => Type::listOf($types->getOutput(Post::class)), // Use automated ObjectType for output
                'args' => [
                    [
                        'name' => 'filter',
                        'type' => $types->getFilter(Post::class), // Use automated filtering options
                    ],
                    [
                        'name' => 'sorting',
                        'type' => $types->getSorting(Post::class), // Use automated sorting options
                    ],
                ],
                'resolve' => function ($root, $args) use ($types): void {
                    $queryBuilder = $types->createFilteredQueryBuilder(Post::class, $args['filter'] ?? [], $args['sorting'] ?? []);

                    // execute query...
                },
            ],
        ],
    ]),
    'mutation' => new ObjectType([
        'name' => 'mutation',
        'fields' => [
            'createPost' => [
                'type' => Type::nonNull($types->getOutput(Post::class)),
                'args' => [
                    'input' => Type::nonNull($types->getInput(Post::class)), // Use automated InputObjectType for input
                ],
                'resolve' => function ($root, $args): void {
                    // create new post and flush...
                },
            ],
            'updatePost' => [
                'type' => Type::nonNull($types->getOutput(Post::class)),
                'args' => [
                    'id' => Type::nonNull(Type::id()), // Use standard API when needed
                    'input' => $types->getPartialInput(Post::class),  // Use automated InputObjectType for partial input for updates
                ],
                'resolve' => function ($root, $args): void {
                    // update existing post and flush...
                },
            ],
        ],
    ]),
]);

Usage

The public API is limited to the public methods on TypesInterface, Types's constructor, and the attributes.

Here is a quick overview of TypesInterface:

  • $types->get() to get custom types
  • $types->getOutput() to get an ObjectType to be used in queries
  • $types->getFilter() to get an InputObjectType to be used in queries
  • $types->getSorting() to get an InputObjectType to be used in queries
  • $types->getInput() to get an InputObjectType to be used in mutations (typically for creation)
  • $types->getPartialInput() to get an InputObjectType to be used in mutations (typically for update)
  • $types->getId() to get an EntityIDType which may be used to receive an object from database instead of a scalar
  • $types->has() to check whether a type exists
  • $types->createFilteredQueryBuilder() to be used in query resolvers

Information priority

To avoid code duplication as much as possible, information are gathered from several places, where available. And each of those might be overridden. The order of priority, from the least to most important is:

  1. Type hinting
  2. Doc blocks
  3. Attributes

That means it is always possible to override everything with attributes. But existing type hints and dock blocks should cover the majority of cases.

Exclude sensitive things

All getters, and setters, are included by default in the type. And all properties are included in the filters. But it can be specified otherwise for each method and property.

To exclude a sensitive field from ever being exposed through the API, use #[API\Exclude]:

use GraphQL\Doctrine\Attribute as API;

/**
 * Returns the hashed password
 *
 * @return string
 */
#[API\Exclude]
public function getPassword(): string
{
    return $this->password;
}

And to exclude a property from being exposed as a filter:

use GraphQL\Doctrine\Attribute as API;

#[ORM\Column(name: 'password', type: 'string', length: 255)]
#[API\Exclude]
private string $password = '';

Override output types

Even if a getter returns a PHP scalar type, such as string, it might be preferable to override the type with a custom GraphQL type. This is typically useful for enum or other validation purposes, such as email address. This is done by specifying the GraphQL type FQCN via #[API\Field] attribute:

use GraphQL\Doctrine\Attribute as API;
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;

/**
 * Get status
 *
 * @return string
 */
#[API\Field(type: PostStatusType::class)]
public function getStatus(): string
{
    return $this->status;
}

Type syntax

In most cases, the type must use the ::class notation to specify the PHP class that is either implementing the GraphQL type or the entity itself (see limitations). Use string literals only if you must define it as nullable and/or as an array. Never use the short name of an entity (it is only possible for user-defined custom types).

Supported syntaxes (PHP style or GraphQL style) are:

  • MyType::class
  • '?Application\MyType'
  • 'null|Application\MyType'
  • 'Application\MyType|null'
  • 'Application\MyType[]'
  • '?Application\MyType[]'
  • 'null|Application\MyType[]'
  • 'Application\MyType[]|null'
  • 'Collection<Application\MyType>'

This attribute can be used to override other things, such as name, description and args.

Override arguments

Similarly to #[API\Field], #[API\Argument] allows to override the type of argument if the PHP type hint is not enough:

use GraphQL\Doctrine\Attribute as API;

/**
 * Returns all posts of the specified status
 *
 * @param string $status the status of posts as defined in \GraphQLTests\Doctrine\Blog\Model\Post
 *
 * @return Collection
 */
public function getPosts(
     #[API\Argument(type: '?GraphQLTests\Doctrine\Blog\Types\PostStatusType')]
    ?string $status = Post::STATUS_PUBLIC
): Collection
{
    // ...
}

Once again, it also allows to override other things such as name, description and defaultValue.

Override input types

#[API\Input] is the opposite of #[API\Field] and can be used to override things for input types (setters), typically for validations purpose. This would look like:

use GraphQL\Doctrine\Attribute as API;
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;

/**
 * Set status
 *
 * @param string $status
 */
#[API\Input(type: PostStatusType::class)]
public function setStatus(string $status = self::STATUS_PUBLIC): void
{
    $this->status = $status;
}

This attribute also supports description, and defaultValue.

Override filter types

#[API\FilterGroupCondition] is the equivalent for filters that are generated from properties. So usage would be like:

use GraphQL\Doctrine\Attribute as API;

#[API\FilterGroupCondition(type: '?GraphQLTests\Doctrine\Blog\Types\PostStatusType')]
#[ORM\Column(type: 'string', options: ['default' => self::STATUS_PRIVATE])]
private string $status = self::STATUS_PRIVATE;

An important thing to note is that the value of the type specified will be directly used in DQL. That means that if the value is not a PHP scalar, then it must be convertible to string via __toString(), or you have to do the conversion yourself before passing the filter values to Types::createFilteredQueryBuilder().

Custom types

By default, all PHP scalar types and Doctrine collection are automatically detected and mapped to a GraphQL type. However, if some getter return custom types, such as DateTimeImmutable, or a custom class, then it will have to be configured beforehand.

The configuration is done with a PSR-11 container implementation configured according to your needs. In the following example, we use laminas/laminas-servicemanager, because it offers useful concepts such as: invokables, aliases, factories and abstract factories. But any other PSR-11 container implementation could be used instead.

The keys should be the whatever you use to refer to the type in your model. Typically, that would be either the FQCN of a PHP class "native" type such as DateTimeImmutable, or the FQCN of a PHP class implementing the GraphQL type, or directly the GraphQL type name:

$customTypes = new ServiceManager([
    'invokables' => [
        DateTimeImmutable::class => DateTimeType::class,
        'PostStatus' => PostStatusType::class,
    ],
]);

$types = new Types($entityManager, $customTypes);

// Build schema...

That way it is not necessary to annotate every single getter returning one of the configured type. It will be mapped automatically.

Entities as input arguments

If a getter takes an entity as parameter, then a specialized InputType will be created automatically to accept an ID. The entity will then be automatically fetched from the database and forwarded to the getter. So this will work out of the box:

public function isAllowedEditing(User $user): bool
{
    return $this->getUser() === $user;
}

You may also get an input type for an entity by using Types::getId() to write things like:

[
    // ...
    'args' => [
        'id' => $types->getId(Post::class),
    ],
    'resolve' => function ($root, array $args) {
        $post = $args['id']->getEntity();

        // ...
    },
]

Partial inputs

In addition to normal input types, it is possible to get a partial input type via getPartialInput(). This is especially useful for mutations that update existing entities, when we do not want to have to re-submit all fields. By using a partial input, the API client is able to submit only the fields that need to be updated and nothing more.

This potentially reduces network traffic, because the client does not need to fetch all fields just to be able re-submit them when he wants to modify only one field.

And it also enables to easily design mass editing mutations where the client would submit only a few fields to be updated for many entities at once. This could look like:

<?php

$mutations = [
    'updatePosts' => [
        'type' => Type::nonNull(Type::listOf(Type::nonNull($types->get(Post::class)))),
        'args' => [
            'ids' => Type::nonNull(Type::listOf(Type::nonNull(Type::id()))),
            'input' => $types->getPartialInput(Post::class),  // Use automated InputObjectType for partial input for updates
        ],
        'resolve' => function ($root, $args) {
            // update existing posts and flush...
        }
    ],
];

Default values

Default values are automatically detected from arguments for getters, as seen in getPosts() example above.

For setters, the default value will be looked up on the mapped property, if there is one matching the setter name. But if the setter itself has an argument with a default value, it will take precedence.

So the following will make an input type with an optional field name with a default value john, an optional field foo with a default value defaultFoo and a mandatory field bar without any default value:

#[ORM\Column(type: 'string']
private $name = 'jane';

public function setName(string $name = 'john'): void
{
    $this->name = $name;
}

public function setFoo(string $foo = 'defaultFoo'): void
{
    // do something
}

public function setBar(string $bar): void
{
    // do something
}

Filtering and sorting

It is possible to expose generic filtering for entity fields and their types to let users easily create and apply generic filters. This expose basic SQL-like syntax that should cover most simple cases.

Filters are structured in an ordered list of groups. Each group contains an unordered set of joins and conditions on fields. For simple cases a single group of a few conditions would probably be enough. But the ordered list of group allow more advanced filtering with OR logic between a set of conditions.

In the case of the Post class, it would generate that GraphQL schema for filtering, and for sorting it would be that simpler schema.

For concrete examples of possibilities and variables syntax, refer to the test cases.

For security and complexity reasons, it is not meant to solve advanced use cases. For those it is possible to write custom filters and sorting.

Custom filters

A custom filer must extend AbstractOperator. This will allow to define custom arguments for the API, and then a method to build the DQL condition corresponding to the argument.

This would also allow to filter on joined relations by carefully adding joins when necessary.

Then a custom filter might be used like so:

use Doctrine\ORM\Mapping as ORM;
use GraphQL\Doctrine\Attribute as API;
use GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType;

/**
 * A blog post with title and body
 */
#[ORM\Entity]
#[API\Filter(field: 'custom', operator: SearchOperatorType::class, type: 'string')]
final class Post extends AbstractModel

Custom sorting

A custom sorting option must implement SortingInterface. The constructor has no arguments and the __invoke() must define how to apply the sorting.

Similarly to custom filters, it may be possible to carefully add joins if necessary.

Then a custom sorting might be used like so:

use Doctrine\ORM\Mapping as ORM;
use GraphQL\Doctrine\Attribute as API;
use GraphQLTests\Doctrine\Blog\Sorting\UserName;

/**
 * A blog post with title and body
 */
#[ORM\Entity]
#[API\Sorting([UserName::class])]
final class Post extends AbstractModel

Limitations

Namespaces

The use statement is not supported. So types in attributes or doc blocks must be the FQCN, or the name of a user-defined custom types (but never the short name of an entity).

Composite identifiers

Entities with composite identifiers are not supported for automatic creation of input types. Possible workarounds are to change input argument to be something else than an entity, write custom input types and use them via attributes, or adapt the database schema.

Logical operators in filtering

Logical operators support only two levels, and second level cannot mix logic operators. In SQL that would mean only one level of parentheses. So you can generate SQL that would look like:

-- mixed top level
WHERE cond1 AND cond2 OR cond3 AND ...

-- mixed top level and non-mixed sublevels
WHERE cond1 OR (cond2 OR cond3 OR ...) AND (cond4 AND cond5 AND ...) OR ...

But you cannot generate SQL that would like that:

-- mixed sublevels does NOT work
WHERE cond1 AND (cond2 OR cond3 AND cond4) AND ...

-- more than two levels will NOT work
WHERE cond1 OR (cond2 AND (cond3 OR cond4)) OR ...

Those cases would probably end up being too complex to handle on the client-side. And we recommend instead to implement them as a custom filter on the server side, in order to hide complexity from the client and benefit from Doctrine's QueryBuilder full flexibility.

Sorting on join

Out of the box, it is not possible to sort by a field from a joined relation. This should be done via a custom sorting to ensure that joins are done properly.

Prior work

Doctrine GraphQL Mapper has been an inspiration to write this package. While the goals are similar, the way it works is different. In Doctrine GraphQL Mapper, attributes are spread between properties and methods (and classes for filtering), but we work only on methods. Setup seems slightly more complex, but might be more flexible. We built on conventions and widespread use of PHP type hinting to have an easier out-of-the-box experience.

graphql-doctrine's People

Contributors

dependabot-preview[bot] avatar glensc avatar james-damstra avatar neeckeloo avatar popovserhii avatar powerkiki avatar themasch avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-doctrine's Issues

Problem when using with "doctrine/doctrine-bundle"

First of all thanks for all the effort made to develop this bundle!

My problem is that when i try to use this bundle together with doctrine/doctrine-bundle it doesn't work because of error:

graphql-doctrine requires Doctrine to be configured with a `Doctrine\\Persistence\\Mapping\\Driver\\AnnotationDriver`.

After some digging I found that doctrine/doctrine-bundle implemented decorator for metadriver implementation (doctrine/DoctrineBundle@a1268d4#diff-0c1c41fb2c90d9d15f9c9bf6b9e4543221b239e048c1dde8b8c71bb58d13cec5) which causes \GraphQL\Doctrine\Factory\AbstractFactory::getAnnotationReader to fail.

At the moment I solved it with following hackaround:

$actualMetadataDriver = $this->entityManager->getConfiguration()->getMetadataDriverImpl()->getDriver();
$this->entityManager->getConfiguration()->setMetadataDriverImpl($actualMetadataDriver);
$types = new Types($this->entityManager, $this->getCustomTypes());

Maybe there are other (more proper) ways to make bundle work with newest doctrine/doctrine-bundle?

Also one more side question: maybe you could give any advise on how to test code where \GraphQL\Doctrine\Types class is used? It's final, doesn't have any interface and expects Doctrine\ORM\EntityManager and not its interface so I'm not sure how to mock it.

Thanks in advance!

Support webonyx/graphql-php 14

Hello,

Thanks for this great library.
Do you think is it possible to support webonyx/graphql-php 14 ?

composer require ecodev/graphql-doctrine throw an error with non compatible version.

Best regards.

phpstan

The new(er) tool phpstan/phpstan could be used on this repository to improve the code.

I don't feel I have the breath of knowledge to correct the 33 errors it finds with --level=7 so I'm just putting in this suggestion.

Detached Entity

Currently implementing a delete mutation like so:

'deleteUser' => [
                'type' => Type::nonNull($this->types->getOutput(User::class)),
                'args' => [
                    'id' => Type::nonNull($this->types->getId(User::class)) // Use standard API when needed
                ],
                'resolve' => function ($root, $args) {
                    $user = $args['id']->getEntity();
                    if (!$user) {
                        throw new \Exception('no user found');
                    }
                    $em = $this->doctrine->getEntityManager();
                    $em->destroy($user);
                    $em->flush();
                    return $user->toArray();
                },
            ]

When I run this, I get:
Detached entity User@000000000c6488ea0000000036bbfb16 cannot be removed
I can fix this by changing the code to:

    $em = $this->doctrine->getEntityManager();
    $attach = $em->merge($user);
    $em->destroy($attach);
    $em->flush();

However, merge() is deprecated and going to be removed soon. Is there any reason why I'm getting a detached entity inside the resolve method when using $user = $args['id']->getEntity();?

$types->getInput() doesn't work as expected

The documentation says we can use $types->getInput() in next way

$schema = new Schema([
    'mutation' => new ObjectType([
        'name' => 'mutation',
        'fields' => [
            'createPost' => [
                'type' => Type::nonNull($types->getOutput(Post::class)),
                'args' => [
                    'input' => Type::nonNull($types->getInput(Post::class)), // Use automated InputObjectType for input
                ],
                'resolve' => function ($root, $args): void {
                    // create new post and flush...
                },
            ],
        ],
    ]),
]);

I implemented schema for my own models in a similar way

[
    'createRankMonitor' => [
        'type' => Type::nonNull($types->getOutput(RankMonitor::class)),
        'args' => [
            'input' => Type::nonNull($types->getInput(RankMonitor::class)),
        ],
        'resolve' => function ($root, $args, $context) {

        },
    ],
]

But when tied to generate schema I got error Type for parameter $user for method Stagem\Product\Model\RankMonitor::setUser() must be an instance of GraphQL\Type\Definition\InputType, but was GraphQL\Type\Definition\ObjectType. Use @API\Input annotation to specify a custom InputType.

My Stagem\Product\Model\RankMonitor

namespace Stagem\Product\Model;

use Doctrine\ORM\Mapping as ORM;
use GraphQL\Doctrine\Annotation as API;
use Popov\ZfcUser\Model\User;

/**
 * @ORM\Entity()
 * @ORM\Table(name="product_rank_monitor")
 */
class RankMonitor
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer", options={"unsigned":true})
     */
    private $id;

    // ...

    /**
     * @var User
     * @ORM\ManyToOne(targetEntity="Popov\ZfcUser\Model\User")
     * @ORM\JoinColumn(name="userId", referencedColumnName="id", nullable=false)
     */
    private $user;

    /**
     * @return User
     */
    public function getUser(): User
    {
        return $this->user;
    }

    /**
     * @param User $user
     * @return RankMonitor
     */
    public function setUser(User $user): RankMonitor
    {
        $this->user = $user;

        return $this;
    }
}

Slim Framework Example

Can we have a Slim framework example/skeleton for this? That would be extremely helpful.

Groups with OR logic and Joins

Consider this query and variables :

query TransactionLines($filter: TransactionLineFilter, $sorting: [TransactionLineSorting!], $pagination: PaginationInput) {
  transactionLines(filter: $filter, sorting: $sorting, pagination: $pagination) {
    items {
      id
      name
      credit {
        id
        owner {
          id
          name
        }
      }
      debit {
        id
        owner {
          id
          name
        }
      }
    }
    pageSize
    pageIndex
    length
    totalBalance
  }
}

Variables :

{
    "filter": {
        "groups": [
            {
                "joins": {"debit": {"conditions": [{"owner": {"in": {"values": ["1002"]}}}]}}
            },
            {
                "groupLogic": "OR",
                "joins": {"credit": {"conditions": [{"owner": {"in": {"values": ["1002"]}}}]}}
            }
        ]
    }
}

What I expect is to filter TransactionLines for which debit or credit account belong to given user (1002)

Using the first group alone returns transactions that decrease account.
Using the second group alone returns transactions the increase account.

But using them together with OR logic returns no result like the joins ignore the OR operator on groups

Cannot return null for non-nullable field

Hi,

I'm new with GraphQL and maybe doing something wrong.

I have this query:

getTapeLanguages(tapeId:112830){
    languageId
    name
    createdAt
  }

Query type is something like this

'type' => Type::listOf($types->getOutput(Language::class))

And resolver return this

$tape->getLanguages()->toArray()

While getLanguages()->count() is equal to 1 and getLanguages()->current()->getLanguageId() return 12 (expected value) I get this error:

Cannot return null for non-nullable field Language.languageId.

What I'm doing wrong? Resolver could return a Language Collection as array and is it supported?

Regards

Support collection in setter

I've started using this library recently and love it so far, I have a small support request though. :-)

Currently, I have a mutation that allows a user to create a Company.

'createCompany' => [
    'type' => Type::nonNull($types->get(Company::class)),
    'args' => [
        'input' => Type::nonNull($types->getInput(Company::class)),
    ],
    'resolve' => function ($root, $args) use ($resolver) {
        return $resolver->createObject(Company::class, $args['input']);
    },
],

The Company model holds a Collection (Doctrine) of objects of the Entity Contact. Retrieving the Collection through GraphQL is not a problem:

/**
  * @return \Doctrine\Common\Collections\Collection
  * @\GraphQL\Doctrine\Annotation\Field(type="?Contact[]")
  */
public function getContacts() : Collection
{
    return $this->contacts;
}

I have annotated Company::getContacts() using the Field annotation as per the documentation (this works, but am I doing this the right way?).

Setting the collection however, does not work. This code:

/**
  * @param \Doctrine\Common\Collections\Collection $contacts
  * @\GraphQL\Doctrine\Annotation\Input(type="Contact[]")
  */
public function setContacts(Collection $contacts)
{
    $this->contacts = $contacts;
}

Results in this error:

Type for parameter `$contacts` for method `Company::setContacts()` must be an instance of `GraphQL\\Type\\Definition\\InputType`, but was `GraphQL\\Type\\Definition\\ObjectType`. Use `@API\\Input` annotation to specify a custom InputType.

What am I doing wrong?

Thanks in advance!

PS: I'm using ecodev/graphql-doctrine:2.0.1 and webonyx/graphql-php:v0.11.6.

Custom Trait in Model throws Exception

All Models in my projects look like this

use Popov\ZfcCore\Model\DomainAwareTrait;

class Marketplace
{
   use DomainAwareTrait;

      /**
     * @var int
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer", options={"unsigned":true})
     */
    private $id;
    // ...
}

and DomainAwareTrait looks like this

namespace Popov\ZfcCore\Model;

use GraphQL\Doctrine\Annotation as API;

trait DomainAwareTrait
{
    /**
     * @API\Exclude
     *
     * @param array $data
     * @return $this
     */
    public function exchangeArray(array $data): self
    {
        foreach ($this->getArrayCopy() as $property => $value) {
            $this->{$property} = (isset($data[$property])) ? $data[$property] : $value;
        }

        return $this;
    }

    /**
     * @API\Exclude
     *
     * @return array
     */
    public function getArrayCopy(): array
    {
        return get_object_vars($this);
    }

    /**
     * @API\Exclude
     *
     * @return array
     */
    public function toArray(): array
    {
        return $this->getObjectAsArray();
    }
   // ...
}

Parts of these methods is required by framework (ZF) and other for custom purpouse.

But when I try implement simple Schema for my model I got Exception: graphql-doctrine requires Popov\ZfcCore\Model\DomainAwareTrait entity to be configured with a Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver.

This is my schema example:

$schema = new Schema([
	'query' => new ObjectType([
		'name' => 'query',
		'fields' => [
			'marketplaces' => [
				'type' => Type::listOf($types->getOutput(Marketplace::class)), // Use automated ObjectType for output
				'args' => [
					[
						'name' => 'filter',
						'type' => $types->getFilter(Marketplace::class), // Use automated filtering options
					],
					[
						'name' => 'sorting',
						'type' => $types->getSorting(Marketplace::class), // Use automated sorting options
					],
				],
				'resolve' => function ($root, $args) use ($types) {
					$queryBuilder = $types->createFilteredQueryBuilder(Marketplace::class, $args['filter'] ?? [], $args['sorting'] ?? []);
					$result = $queryBuilder->getQuery()->getArrayResult();
					return $result;
				},
			],
		],
		'resolveField' => function($val, $args, $context, ResolveInfo $info) {
			return $this->{$info->fieldName}($val, $args, $context, $info);
		}
	])
]);

The exception is throwing only when 'filter' and 'sorting' are enabled.

Entity without relations must not have an empty FilterGroupJoin

I'm trying to implement a filter to model but instead of I'm getting an error (in GraphiQL) Error: MarketplaceFilterGroupJoin fields must be an object with field names as keys or a function which returns such an object.

I'm doing something wrong? There are some requirements for default filtering?

My $queryType:

$queryType = new ObjectType([
	'name' => 'query',
	'fields' => [
		'marketplaces' => [
			'type' => Type::listOf($types->getOutput(Marketplace::class)),
			'args' => [
				[
					'name' => 'filter',
					'type' => $types->getFilter(Marketplace::class),
				],
				[
					'name' => 'sorting',
					'type' => $types->getSorting(Marketplace::class),
				],
			],
			'resolve' => function ($root, $args) use ($types) {
				$queryBuilder = $types->createFilteredQueryBuilder(Marketplace::class, $args['filter'] ?? [], $args['sorting'] ?? []);
				$result = $queryBuilder->getQuery()->getArrayResult();
				return $result;
			},
		],
	],
]);

Realization is getting from Quick star

Documentation generation error

I've encountered a problem with automatic documentation generation when using generic filters. Filtering in queries is working fine but I get a following error when trying to fetch docs:

Error: TypeFilterGroupJoin fields must be an object with field names as keys or a function which returns such an object.
    at invariant (chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:14605:11)
    at GraphQLInputObjectType._defineFieldMap (chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:14084:56)
    at GraphQLInputObjectType.getFields (chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:14075:49)
    at typeMapReducer (chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:16618:26)
    at chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:16621:20
    at Array.forEach (<anonymous>)
    at typeMapReducer (chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:16619:28)
    at chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:16621:20
    at Array.forEach (<anonymous>)
    at typeMapReducer (chrome-extension://fkkiamalmpiidkljmicmjfbieiclmeij/dist/chromeiql.js:16619:28)

I'm using ChromeiQL (same thing happens in GraphiQL Feen). When I comment out args providing generic filters documentation generates without an error. Since this is happening when using two different clients is seems it may be a backend problem.

Here's my code:

$types = new Types($this->objectManager, $this->container);

        $schema = new Schema([
            'query' => new ObjectType([
                'name' => 'productList',
                'description' => 'Fetches product list',
                'fields' => [
                    'products' => [
                        'type' => Type::listOf($types->getOutput(Product::class)), // Use automated ObjectType for output
                        'args' => [
                            [
                                'name' => 'filter',
                                'type' => $types->getFilter(Product::class), // Use automated filtering options
                            ],
                            [
                                'name' => 'sorting',
                                'type' => $types->getSorting(Product::class), // Use automated sorting options
                            ],
                        ],
                        'resolve' => function ($root, $args) use ($types) {
                            $queryBuilder = $types->createFilteredQueryBuilder(Product::class, $args['filter'] ?? [], $args['sorting'] ?? []);

                            return $queryBuilder->getQuery()->execute();
                        },
                    ]
                ]
            ])
        ]);
        $server = new StandardServer([
            'schema' => $schema,
            'queryBatching' => true,
            'debug' => true,
            'fieldResolver' => new DefaultFieldResolver()
        ]);

Btw, I'm using Zend Expressive and the entity mapping is very simple (Product with couple of fields and ManyToOne association to Type entity).

Getting entity fields with magic methods

Hi,

I'm implementing graphql with doctrine and I found a "corner case" where the library is not able to obtain the value of the entity. I'm implementing my entities with private fields and a __get magic method, something like this:

class Entity {
    private string $name = "Name";
    
    public function __get(string $name)
    {
        if (!property_exists($this, $name)) {
            throw new InvalidArgumentException();
        }

        return $this->$name;
    }
}

But the DefaultFieldResolver has this checking that makes null the value:

if (isset($source->{$fieldName})) {

In this case isset returns false, but $source->{$fieldName} returns a valid value. empty function won't work neither, the only thing that could work is doing isset with the value returned, but I don't know if this is a proper solution.

Have you ever found a similar case or is there a better solution for this case?

Thanks

EDIT: I've solved this in my fork modifying the line I was talking about with this one:

if (isset($source->{$fieldName}) || method_exists($source, '__get')) {

I'm not sure if this a recommendable solution, haven't found a lot of documentation on how to do somehting like that, but I wanted to leave it here. Thanks again

More sorting orders

Hi, amazing library ๐ŸŽ‰

I've a feature request for listing sortings : DESC but with null values first.

 ORDER BY myCol IS NULL DESC, myCol DESC

Support Embeddable classes

Doctrine 2 has a great feature named as Embeddable classes.
I try to use it in the pair with graphql-doctrine and got error No type registered with key Position. Either correct the usage, or register it in your custom types container when instantiating GraphQL\Doctrine\Types, where Position is a simple object

namespace Stagem\Product\Model\Monitor;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Embeddable
 */
class Position
{
    /**
     * @ORM\Column(type="integer")
     */
    private $line = 1;

    /**
     * @ORM\Column(type="integer")
     */
    private $column = 1;
    // ...
}

and RankMonitor model

namespace Stagem\Product\Model;

use Doctrine\ORM\Mapping as ORM;
use GraphQL\Doctrine\Annotation as API;
use Stagem\Product\Model\Monitor\Position;

/**
 * @ORM\Entity()
 * @ORM\Table(name="product_rank_monitor")
 */
class RankMonitor
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer", options={"unsigned":true})
     */
    private $id;

    /**
     * @var Position
     * @ORM\Embedded(class="Stagem\Product\Model\Monitor\Position", columnPrefix="position_")
     */
    private $position;
    // ...
}

Do you plan to add native support for Embeddable classes or maybe can explain how to implement this feature?

Some usage issues

First of all thank you so much for this amazing library! because of this now so easy to switch to GraphQL from REST API with in few minutes.

I have few newbie niggles with the library,

  1. I feel there is no proper documentation on how to write sorting and filter queries. I tried to figure it out from the examples and I managed to understand sorting but not the filters yet.

I could sort DESC by age by

query {
	Employee (sorting: { field: age, order: DESC }) {
      name
      age
      address
      city
      cTime
    }
}

Not sure how i can also filter by employees of age between 18 and 55? I have tried several weird things such as

query {
	Employee (sorting: { field: age, order: DESC }, filter: { conditions: { age : { between: { 18 55 } }  } }) {
      name
      age
      address
      city
      cTime
    }
}
  1. Is it possible to do doctrine joins with some table with GraphQL query with this library? can you please provide an example
  2. I saw it is possible to add custom sorting and filtering from the blog example but is it possible to get this field in the response as well?

Thanks in advance!

Event driven programming

I think this would benefit from event tie-ins. For instance I would like to build a configurable hydrator rather than use annotations. But there are many event models out there. I prefer Zend Framework. This repository seems to try to be rather framework agnostic.

Is the project open to building in events?

I generate my entities from orm:generate-entities and my schema is all in XML thanks to Skipper and I never use annotations. But I still think this library has promise for my methods.

Support for non-empty constructor of custom types

Right now it seems impossible to define a custom object type, which includes another custom object type, because it requires passing TypeRegistry as a parameter to the type constructor or having its methods statically available. I.e.

 class MyCustomType extends ObjectType
 {
     public function __construct(TypeRegistry $types)
     {
         parent::__construct([
                 'description' => 'Something ',
                 'fields' => [
                     'listOfSomething' => [
                         'type' => Type::listOf($types->get('OtherCustomObjectType')
                     ]
                 ]
             ]
         );
     }

Implying that "OtherCustomObjectType" is also used elsewhere in API so can't be declared inline.

Since all the class management is done internally, it's not possible to bypass implemented object creation routine. I suggest either making Types::registerInstance a public method, or allowing passing a class instance as type definition instead of just name to Types::get

Properties starting with is/has IE: isActive instead of getActive

Is there a reason all the getters must be named: getProperty().

We're wondering if there is a reason for this, since we think it's more logical to name a property active instead of IsActive.

This could be solved in the DefaultFieldResolver::getGetter() if desired.

Hello, could you give me a demo?

Hello, I was pushed by the fact that there is very little documentation on the Internet on this subject, and I had difficulties with studying, could you throw a demo example of a simple implementation on symfony 4 or at least start writing wiki ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.