Skip to content

Add annotation reader cache and fix doctrine typeGuesser #867

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

Merged
merged 37 commits into from
Nov 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9bdd39f
Bump master to 1.0-dev
mcg-web Apr 28, 2021
869bb10
Merge branch '0.14'
mcg-web Apr 29, 2021
0f1a492
Merge branch '0.14'
mcg-web Apr 29, 2021
eeeaf4e
Fix master branch badges
mcg-web Apr 29, 2021
c29a4b8
Merge branch '0.14'
mcg-web Apr 30, 2021
bee2e36
Merge branch '0.14'
mcg-web Apr 30, 2021
40b7eac
Merge branch '0.14'
mcg-web May 4, 2021
f0af747
Merge branch '0.14'
mcg-web May 4, 2021
ba6f6ba
Merge branch '0.14'
mcg-web May 11, 2021
f32d17c
Merge branch '0.14'
mcg-web May 12, 2021
e977543
Fix typo
BenMorel Jun 11, 2021
410264a
Typo fix
BenMorel Jun 15, 2021
a711a8d
Merge pull request #860 from BenMorel/patch-1
mcg-web Jun 17, 2021
c19e226
Merge pull request #862 from BenMorel/patch-2
mcg-web Jun 17, 2021
96208d0
Update AnnotationParser.php
Damienbelingheri Jun 22, 2021
aa2ac26
Fix resolveTypeFromDoctrineType()
Damienbelingheri Jun 22, 2021
d84d0a6
Fix resolveTypeFromDoctrineType
Damienbelingheri Jun 22, 2021
c63a42a
Merge branch 'master' of github.com:Damienbelingheri/GraphQLBundle
Damienbelingheri Jun 22, 2021
2d9fa60
fix return getAnnotationReader
Damienbelingheri Jun 22, 2021
1b38f79
fix single quote
Damienbelingheri Jun 22, 2021
b73885a
change version symfony/cache
Damienbelingheri Jun 22, 2021
cc6995e
remove symfony/cache from composer
Damienbelingheri Jun 23, 2021
7708dca
add check symfony/cache is installed and improve doc
Damienbelingheri Jun 23, 2021
a4ef644
add check symfony/cache is installed and improve doc
Damienbelingheri Jun 23, 2021
a6111bf
Merge branch 'master' of github.com:Damienbelingheri/GraphQLBundle
Damienbelingheri Jun 23, 2021
35aba0c
Update index.md
Damienbelingheri Jun 23, 2021
a196e32
Update composer.json
Damienbelingheri Jun 23, 2021
4933b03
Test for annotation dependencies package
Vincz Jun 24, 2021
a281f75
Fix ci.yml
Vincz Jun 24, 2021
f9e9c61
Make doctrine/orm & doctrine/annotations optionnal
Vincz Jun 24, 2021
3f503a1
Skip validator test if no doctrine/annotations
Vincz Jun 24, 2021
b616d4c
Merge branch '0.14' into master
Vincz Jun 24, 2021
7a06d4a
revert branch 1.0 -> 0.14
Damienbelingheri Sep 22, 2021
02e7e90
revert version 1 -> 0.14
Damienbelingheri Sep 22, 2021
d5c0ba1
Merge branch '0.14' into master
Vincz Nov 11, 2021
9baef2b
Merge branch '0.14' into master
Vincz Nov 25, 2021
2cec73a
Fix ci & composer
Vincz Nov 25, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- php-version: '8.0'
symfony-version: '5.3'
dependencies: 'lowest'
remove-dependencies: '--dev symfony/validator'
remove-dependencies: '--dev symfony/validator doctrine/orm doctrine/annotations'
- php-version: '8.0'
symfony-version: '5.3'
dependencies: 'lowest'
Expand Down Expand Up @@ -77,7 +77,6 @@ jobs:
run: |
composer global require php-coveralls/php-coveralls
php-coveralls --coverage_clover=build/logs/clover.xml -v

coding-standard:
runs-on: ubuntu-20.04
name: Coding Standard
Expand Down Expand Up @@ -130,4 +129,4 @@ jobs:
uses: ramsey/[email protected]

- name: "Run static-analysis"
run: composer static-analysis
run: composer static-analysis
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Talks and slides to help you start

* GraphQL in Symfony *by Bernd Alter* - [Twitter](https://twitter.com/bazoo0815)
- [Talk about GraphQL and its implementation with Symfony (26.04.2017)](https://www.slideshare.net/berndalter7/graphql-in-symfony) `English`
* GraphQL is right in front of us, let's to it! *by Renato Mendes Figueiredo* - [Twitter](https://twitter.com/renatomefi), [GitHub](https://github.com/renatomefi)
* GraphQL is right in front of us, let's do it! *by Renato Mendes Figueiredo* - [Twitter](https://twitter.com/renatomefi), [GitHub](https://github.com/renatomefi)
- [Slides at http://talks.mefi.in/graphql-scotphp17](http://talks.mefi.in/graphql-scotphp17/) `English`
- [Video at SymfonyCamp UA 2017](https://www.youtube.com/watch?v=jyoYlnCPNgk) `English`
- [Video at DPC 2017](https://www.youtube.com/watch?v=E7MjoCOGSSY) `English`
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "symfony-bundle",
"license": "MIT",
"description": "This bundle provides tools to build a GraphQL server in your Symfony App.",
"keywords": ["GraphQL", "Relay"],
"keywords": ["GraphQL","Relay"],
"authors": [
{
"name": "Overblog",
Expand Down Expand Up @@ -83,7 +83,7 @@
"static-analysis": [
"phpstan analyse --ansi --memory-limit=1G"
],
"bench": [
"bench": [
"test -f phpbench.phar || wget https://github.com/phpbench/phpbench/releases/download/1.0.0-alpha7/phpbench.phar -O phpbench.phar",
"@php phpbench.phar run -l dots --ansi -vvv --report='generator: \"table\", cols: [\"benchmark\", \"subject\", \"params\", \"best\", \"mean\", \"mode\", \"worst\", \"diff\"], break: [\"benchmark\"], sort: {mean: \"asc\"}'"
],
Expand Down
6 changes: 4 additions & 2 deletions docs/annotations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

In order to use annotations or attributes, you need to configure the mapping:

To use annotations, use the `annotation` mapping type.
To use annotations, You must install `symfony/cache` and `doctrine/annotation` and use the `annotation` mapping type.


```yaml
# config/packages/graphql.yaml
overblog_graphql:
Expand All @@ -13,7 +15,6 @@ overblog_graphql:
dir: "%kernel.project_dir%/src/GraphQL"
suffix: ~
```

To use attributes, use the `attribute` mapping type.

```yaml
Expand Down Expand Up @@ -215,6 +216,7 @@ In this example, the type `String!` will be auto-guessed from the type hint of t
### @Field type auto-guessing from Doctrine ORM Annotations

Based on other Doctrine annotations on your fields, the corresponding GraphQL type can sometimes be guessed automatically.
In order to activate this guesser, you must install `doctrine/orm` package.

The type can be auto-guessed from the following annotations:

Expand Down
2 changes: 1 addition & 1 deletion docs/error-handling/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Custom error handling / formatting
-----------------------------------

This can also be done by using events.
* First totally disabled default errors handler:
* First totally disable default errors handler:
```yaml
overblog_graphql:
errors_handler: false
Expand Down
9 changes: 9 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ overblog_graphql_multiple_endpoint:
prefix: /graphql
```


Optionnal features depencies
------------

- To use the Validator features, you must also install `symfony/validator` and `doctrine/annotations`
- To use the annotations, you must also install `doctrine/annotations`
- To use the annotations doctrine type guesser, you must also install `doctrine/orm`


Composer autoloader configuration (optional)
------------

Expand Down
37 changes: 26 additions & 11 deletions src/Config/Parser/AnnotationParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\PsrCachedReader;
use Doctrine\Common\Annotations\Reader;
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\MetadataParser;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use Reflector;
use RuntimeException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

class AnnotationParser extends MetadataParser
{
public const METADATA_FORMAT = '@%s';

protected static ?AnnotationReader $annotationReader = null;
protected static Reader $annotationReader;

protected static function getMetadatas(Reflector $reflector): array
{
Expand All @@ -32,18 +36,29 @@ protected static function getMetadatas(Reflector $reflector): array
return [];
}

protected static function getAnnotationReader(): AnnotationReader
public static function getAnnotationReader(): Reader
{
if (null === self::$annotationReader) {
if (!class_exists(AnnotationReader::class) ||
!class_exists(AnnotationRegistry::class)) {
// @codeCoverageIgnoreStart
throw new RuntimeException('In order to use graphql annotation, you need to require doctrine annotations');
// @codeCoverageIgnoreEnd
if (!isset(self::$annotationReader)) {
if (!class_exists(AnnotationReader::class)) {
throw new ServiceNotFoundException("In order to use annotations, you need to install 'doctrine/annotations' first. See: 'https://www.doctrine-project.org/projects/annotations.html'");
}
if (!class_exists(ApcuAdapter::class)) {
throw new ServiceNotFoundException("In order to use annotations, you need to install 'symfony/cache' first. See: 'https://symfony.com/doc/current/components/cache.html'");
}

if (class_exists(AnnotationRegistry::class)) {
AnnotationRegistry::registerLoader('class_exists');
}
$cacheKey = md5(__DIR__);
// @codeCoverageIgnoreStart
if (extension_loaded('apcu') && apcu_enabled()) {
$annotationCache = new ApcuAdapter($cacheKey);
} else {
$annotationCache = new PhpFilesAdapter($cacheKey);
}
// @codeCoverageIgnoreEnd

AnnotationRegistry::registerLoader('class_exists');
self::$annotationReader = new AnnotationReader();
self::$annotationReader = new PsrCachedReader(new AnnotationReader(), $annotationCache, true);
}

return self::$annotationReader;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@

namespace Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\ORM\Mapping\Annotation as MappingAnnotation;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use Overblog\GraphQLBundle\Config\Parser\AnnotationParser;
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\ClassesTypesMap;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use Reflector;
use RuntimeException;

class DoctrineTypeGuesser extends TypeGuesser
{
protected ?AnnotationReader $annotationReader = null;
protected array $doctrineMapping = [];

public function __construct(ClassesTypesMap $map, array $doctrineMapping = [])
Expand All @@ -46,14 +43,19 @@ public function supports(Reflector $reflector): bool
*/
public function guessType(ReflectionClass $reflectionClass, Reflector $reflector, array $filterGraphQLTypes = []): ?string
{
if (!class_exists(Column::class)) {
throw new TypeGuessingException(sprintf('You must install doctrine/orm package to use this type guesser.'));
}

if (!$reflector instanceof ReflectionProperty) {
throw new TypeGuessingException('Doctrine type guesser only apply to properties.');
}

/** @var Column|null $columnAnnotation */
$columnAnnotation = $this->getAnnotation($reflector, Column::class);

if (null !== $columnAnnotation) {
$type = $this->resolveTypeFromDoctrineType($columnAnnotation->type ?? 'string');
$type = $this->resolveTypeFromDoctrineType($columnAnnotation->type ?: 'string');
$nullable = $columnAnnotation->nullable;
if ($type) {
return $nullable ? $type : sprintf('%s!', $type);
Expand Down Expand Up @@ -100,7 +102,7 @@ public function guessType(ReflectionClass $reflectionClass, Reflector $reflector

private function getAnnotation(Reflector $reflector, string $annotationClass): ?MappingAnnotation
{
$reader = $this->getAnnotationReader();
$reader = AnnotationParser::getAnnotationReader();
$annotations = [];
switch (true) {
case $reflector instanceof ReflectionClass: $annotations = $reader->getClassAnnotations($reflector); break;
Expand All @@ -117,21 +119,6 @@ private function getAnnotation(Reflector $reflector, string $annotationClass): ?
return null;
}

private function getAnnotationReader(): AnnotationReader
{
if (null === $this->annotationReader) {
if (!class_exists(AnnotationReader::class) ||
!class_exists(AnnotationRegistry::class)) {
throw new RuntimeException('In order to use graphql annotation/attributes, you need to require doctrine annotations');
}

AnnotationRegistry::registerLoader('class_exists');
$this->annotationReader = new AnnotationReader();
}

return $this->annotationReader;
}

/**
* Resolve a FQN from classname and namespace.
*
Expand Down
28 changes: 28 additions & 0 deletions tests/Config/Parser/AnnotationParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,22 @@

use Overblog\GraphQLBundle\Config\Parser\AnnotationParser;
use SplFileInfo;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

class AnnotationParserTest extends MetadataParserTest
{
public function setUp(): void
{
if ('testNoDoctrineAnnotations' !== $this->getName()) {
if (!self::isDoctrineAnnotationInstalled()) {
$this->markTestSkipped('doctrine/annotations are not installed. Skipping annotation parser tests.');
}
parent::setUp();
}
}

public function parser(string $method, ...$args)
{
return AnnotationParser::$method(...$args);
Expand All @@ -19,6 +32,21 @@ public function formatMetadata(string $metadata): string
return sprintf('@%s', $metadata);
}

public function testNoDoctrineAnnotations(): void
{
if (self::isDoctrineAnnotationInstalled()) {
$this->markTestSkipped('doctrine/annotations are installed');
}

try {
$containerBuilder = $this->getMockBuilder(ContainerBuilder::class)->disableOriginalConstructor()->getMock();
AnnotationParser::parse(new SplFileInfo(__DIR__.'/fixtures/annotations/Type/Animal.php'), $containerBuilder);
} catch (InvalidArgumentException $e) {
$this->assertInstanceOf(ServiceNotFoundException::class, $e->getPrevious());
$this->assertMatchesRegularExpression('/doctrine\/annotations/', $e->getPrevious()->getMessage());
}
}

public function testLegacyNestedAnnotations(): void
{
$this->config = self::cleanConfig($this->parser('parse', new SplFileInfo(__DIR__.'/fixtures/annotations/Deprecated/DeprecatedNestedAnnotations.php'), $this->containerBuilder, ['doctrine' => ['types_mapping' => []]]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Overblog\GraphQLBundle\Tests\Config\Parser;

use Doctrine\ORM\Mapping\Column;
use Exception;
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\ClassesTypesMap;
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DoctrineTypeGuesser;
Expand All @@ -15,24 +16,47 @@ class DoctrineTypeGuesserTest extends TestCase
// @phpstan-ignore-next-line
protected $property;

public static function isDoctrineInstalled(): bool
{
return class_exists(Column::class);
}

public function testGuessError(): void
{
if (!self::isDoctrineInstalled()) {
$this->markTestSkipped('Doctrine ORM is not installed');
}

$refClass = new ReflectionClass(__CLASS__);
$docBlockGuesser = new DoctrineTypeGuesser(new ClassesTypesMap());
$doctrineGuesser = new DoctrineTypeGuesser(new ClassesTypesMap());

try {
// @phpstan-ignore-next-line
$docBlockGuesser->guessType($refClass, $refClass);
$doctrineGuesser->guessType($refClass, $refClass);
} catch (Exception $e) {
$this->assertInstanceOf(TypeGuessingException::class, $e);
$this->assertStringContainsString('Doctrine type guesser only apply to properties.', $e->getMessage());
}

try {
$docBlockGuesser->guessType($refClass, $refClass->getProperty('property'));
$doctrineGuesser->guessType($refClass, $refClass->getProperty('property'));
} catch (Exception $e) {
$this->assertInstanceOf(TypeGuessingException::class, $e);
$this->assertStringContainsString('No Doctrine ORM annotation found.', $e->getMessage());
}
}

public function testDoctrineOrmNotInstalled(): void
{
if (self::isDoctrineInstalled()) {
$this->markTestSkipped('Doctrine ORM is installed');
}

$this->expectException(TypeGuessingException::class);
$this->expectExceptionMessageMatches('/^You must install doctrine\/orm/');

$refClass = new ReflectionClass(__CLASS__);
$doctrineGuesser = new DoctrineTypeGuesser(new ClassesTypesMap());
$doctrineGuesser->guessType($refClass, $refClass->getProperty('property'));
}
}
Loading