Skip to content

Commit f9e0b35

Browse files
committed
feat(graphql): Awesome SchemaPrinter with directives, filtering, and advanced formating.
These are few known issues: * Schema description is not yet supported by `graphql-php` (webonyx/graphql-php#1027) * "implements" for interfaces is not yet supported by Lighthouse (just for the reference webonyx/graphql-php#728)
2 parents 7d80539 + e6f49d8 commit f9e0b35

File tree

93 files changed

+11123
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+11123
-6
lines changed

composer.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@
6363
"mockery/mockery": "^1.4",
6464
"nunomaduro/larastan": "^1.0.0",
6565
"orchestra/testbench": "^6.9.0",
66-
"phpstan/phpstan": "^1.0.0",
67-
"phpstan/phpstan-mockery": "^1.0.0",
68-
"phpstan/phpstan-phpunit": "^1.0.0",
69-
"phpstan/phpstan-strict-rules": "^1.0.0",
66+
"phpstan/phpstan": "dev-master",
67+
"phpstan/phpstan-mockery": "dev-master",
68+
"phpstan/phpstan-phpunit": "dev-master",
69+
"phpstan/phpstan-strict-rules": "dev-master",
7070
"phpunit/phpunit": "^9.5.0",
7171
"slevomat/coding-standard": "dev-master",
7272
"squizlabs/php_codesniffer": "dev-master",

packages/core/src/Observer/Dispatcher.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Closure;
66
use SplObjectStorage;
7+
use function array_values;
8+
use function iterator_to_array;
79

810
/**
911
* @template TContext
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;
4+
5+
use GraphQL\Language\AST\ArgumentNode;
6+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\BlockList;
7+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Property;
8+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;
9+
use Traversable;
10+
11+
/**
12+
* @internal
13+
* @extends BlockList<Property<ValueNodeBlock>>
14+
*/
15+
class ArgumentNodeList extends BlockList {
16+
/**
17+
* @param Traversable<ArgumentNode>|array<ArgumentNode> $arguments
18+
*/
19+
public function __construct(
20+
PrinterSettings $settings,
21+
int $level,
22+
int $used,
23+
Traversable|array $arguments,
24+
) {
25+
parent::__construct($settings, $level, $used);
26+
27+
foreach ($arguments as $argument) {
28+
$name = $argument->name->value;
29+
$this[$name] = new Property(
30+
$this->getSettings(),
31+
$name,
32+
new ValueNodeBlock(
33+
$this->getSettings(),
34+
$this->getLevel() + 1,
35+
$this->getUsed(),
36+
$argument->value,
37+
),
38+
);
39+
}
40+
}
41+
42+
protected function getPrefix(): string {
43+
return '(';
44+
}
45+
46+
protected function getSuffix(): string {
47+
return ')';
48+
}
49+
50+
protected function isNormalized(): bool {
51+
return $this->getSettings()->isNormalizeArguments();
52+
}
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;
4+
5+
use GraphQL\Language\AST\DirectiveNode;
6+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Block;
7+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Named;
8+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;
9+
10+
use function mb_strlen;
11+
12+
/**
13+
* @internal
14+
*/
15+
class DirectiveNodeBlock extends Block implements Named {
16+
public function __construct(
17+
PrinterSettings $settings,
18+
int $level,
19+
int $used,
20+
private DirectiveNode $node,
21+
) {
22+
parent::__construct($settings, $level, $used);
23+
}
24+
25+
public function getName(): string {
26+
return "@{$this->getNode()->name->value}";
27+
}
28+
29+
public function getNode(): DirectiveNode {
30+
return $this->node;
31+
}
32+
33+
protected function content(): string {
34+
// Convert
35+
$node = $this->getNode();
36+
$name = $this->getName();
37+
$used = mb_strlen($name);
38+
$args = $this->addUsed(
39+
new ArgumentNodeList(
40+
$this->getSettings(),
41+
$this->getLevel(),
42+
$this->getUsed() + $used,
43+
$node->arguments,
44+
),
45+
);
46+
47+
// Statistics
48+
$this->addUsedDirective($name);
49+
50+
// Return
51+
return "{$name}{$args}";
52+
}
53+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;
4+
5+
use GraphQL\Language\AST\DirectiveNode;
6+
use GraphQL\Language\Parser;
7+
use GraphQL\Language\Printer;
8+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Contracts\Settings;
9+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\DirectiveResolver;
10+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;
11+
use LastDragon_ru\LaraASP\GraphQL\Testing\Package\SchemaPrinter\TestSettings;
12+
use LastDragon_ru\LaraASP\GraphQL\Testing\Package\TestCase;
13+
14+
/**
15+
* @internal
16+
* @coversDefaultClass \LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast\DirectiveNodeBlock
17+
*/
18+
class DirectiveNodeBlockTest extends TestCase {
19+
// <editor-fold desc="Tests">
20+
// =========================================================================
21+
/**
22+
* @covers ::__toString
23+
*
24+
* @dataProvider dataProviderToString
25+
*/
26+
public function testToString(
27+
string $expected,
28+
Settings $settings,
29+
int $level,
30+
int $used,
31+
DirectiveNode $node,
32+
): void {
33+
$settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings);
34+
$actual = (string) (new DirectiveNodeBlock($settings, $level, $used, $node));
35+
$parsed = Parser::directive($actual);
36+
37+
self::assertEquals($expected, $actual);
38+
39+
if (!$settings->isNormalizeArguments()) {
40+
self::assertEquals(
41+
Printer::doPrint($node),
42+
Printer::doPrint($parsed),
43+
);
44+
}
45+
}
46+
47+
/**
48+
* @covers ::__toString
49+
*/
50+
public function testStatistics(): void {
51+
$settings = new TestSettings();
52+
$settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings);
53+
$node = Parser::directive('@test');
54+
$block = new DirectiveNodeBlock($settings, 0, 0, $node);
55+
56+
self::assertNotEmpty((string) $block);
57+
self::assertEquals([], $block->getUsedTypes());
58+
self::assertEquals(['@test' => '@test'], $block->getUsedDirectives());
59+
}
60+
// </editor-fold>
61+
62+
// <editor-fold desc="DataProviders">
63+
// =========================================================================
64+
/**
65+
* @return array<string,array{string, Settings, int, int, DirectiveNode}>
66+
*/
67+
public function dataProviderToString(): array {
68+
$settings = (new TestSettings())
69+
->setNormalizeArguments(false);
70+
71+
return [
72+
'without arguments' => [
73+
'@directive',
74+
$settings,
75+
0,
76+
0,
77+
Parser::directive('@directive'),
78+
],
79+
'without arguments (level)' => [
80+
'@directive',
81+
$settings,
82+
0,
83+
0,
84+
Parser::directive('@directive'),
85+
],
86+
'with arguments (short)' => [
87+
'@directive(a: "a", b: "b")',
88+
$settings,
89+
0,
90+
0,
91+
Parser::directive('@directive(a: "a", b: "b")'),
92+
],
93+
'with arguments (long)' => [
94+
<<<'STRING'
95+
@directive(
96+
b: "b"
97+
a: "a"
98+
)
99+
STRING,
100+
$settings,
101+
0,
102+
120,
103+
Parser::directive('@directive(b: "b", a: "a")'),
104+
],
105+
'with arguments (normalized)' => [
106+
'@directive(a: "a", b: "b")',
107+
$settings->setNormalizeArguments(true),
108+
0,
109+
0,
110+
Parser::directive('@directive(b: "b", a: "a")'),
111+
],
112+
'with arguments (indent)' => [
113+
<<<'STRING'
114+
@directive(
115+
b: "b"
116+
a: "a"
117+
)
118+
STRING,
119+
$settings,
120+
1,
121+
120,
122+
Parser::directive('@directive(b: "b", a: "a")'),
123+
],
124+
];
125+
}
126+
// </editor-fold>
127+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;
4+
5+
use GraphQL\Language\AST\DirectiveNode;
6+
use GraphQL\Language\Parser;
7+
use GraphQL\Type\Definition\Directive;
8+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Block;
9+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\BlockList;
10+
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;
11+
use Traversable;
12+
13+
use function json_encode;
14+
15+
/**
16+
* @internal
17+
* @extends BlockList<DirectiveNodeBlock>
18+
*/
19+
class DirectiveNodeList extends BlockList {
20+
/**
21+
* @param Traversable<DirectiveNode>|array<DirectiveNode> $directives
22+
*/
23+
public function __construct(
24+
PrinterSettings $settings,
25+
int $level,
26+
int $used,
27+
Traversable|array|null $directives,
28+
string|null $deprecationReason = null,
29+
) {
30+
parent::__construct($settings, $level, $used);
31+
32+
$deprecated = Directive::DEPRECATED_NAME;
33+
$directives ??= [];
34+
35+
if ($deprecationReason) {
36+
// todo(graphql): Is there a better way to create directive node?
37+
if ($deprecationReason !== Directive::DEFAULT_DEPRECATION_REASON) {
38+
$reason = json_encode($deprecationReason);
39+
$this[] = $this->block(Parser::directive("@{$deprecated}(reason: {$reason})"));
40+
} else {
41+
$this[] = $this->block(Parser::directive("@{$deprecated}"));
42+
}
43+
}
44+
45+
foreach ($directives as $directive) {
46+
if ($deprecationReason && $directive->name->value === $deprecated) {
47+
continue;
48+
}
49+
50+
$this[] = $this->block($directive);
51+
}
52+
}
53+
54+
protected function isAlwaysMultiline(): bool {
55+
return true;
56+
}
57+
58+
protected function isValidBlock(Block $value): bool {
59+
// Parent?
60+
if (!parent::isValidBlock($value)) {
61+
return false;
62+
}
63+
64+
// Allowed?
65+
$settings = $this->getSettings();
66+
$filter = $settings->getDirectiveFilter();
67+
$valid = $filter === null
68+
|| $filter->isAllowedDirective($settings->getDirective($value->getNode()));
69+
70+
return $valid;
71+
}
72+
73+
private function block(DirectiveNode $directive,): DirectiveNodeBlock {
74+
return new DirectiveNodeBlock(
75+
$this->getSettings(),
76+
$this->getLevel(),
77+
$this->getUsed(),
78+
$directive,
79+
);
80+
}
81+
}

0 commit comments

Comments
 (0)