Passed
Push — master ( 864347...46e361 )
by Bruno
09:55
created

Parser::getScalars()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
1
<?php declare(strict_types=1);
2
3
namespace Modelarium;
4
5
use Formularium\Formularium;
6
use GraphQL\Language\AST\NodeKind;
7
use GraphQL\Language\Visitor;
8
use GraphQL\Type\Definition\ObjectType;
9
use GraphQL\Type\Definition\Type;
10
use GraphQL\Type\Schema;
11
use GraphQL\Utils\SchemaExtender;
12
use Modelarium\Exception\ScalarNotFoundException;
13
use Modelarium\Types\ScalarType;
14
15
class Parser
16
{
17
    /**
18
     * @var \GraphQL\Language\AST\DocumentNode
19
     */
20
    protected $ast;
21
22
    /**
23
     * @var \GraphQL\Type\Schema
24 22
     */
25
    protected $schema;
26
27
    /**
28
     * @var string[]
29
     */
30
    protected $scalars = [];
31
32
    protected function __construct()
33
    {
34
        $this->scalars = [
35
            'String' => 'Modelarium\\Types\\Datatype_string',
36
            'Int' => 'Modelarium\\Types\\Datatype_integer',
37
            'Float' => 'Modelarium\\Types\\Datatype_float',
38
            'Boolean' => 'Modelarium\\Types\\Datatype_bool',
39
        ];
40 22
    }
41
42
    /** @phpstan-ignore-next-line */
43 23
    public static function extendDatatypes(array $typeConfig, $typeDefinitionNode): array
0 ignored issues
show
Unused Code introduced by
The parameter $typeDefinitionNode is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

43
    public static function extendDatatypes(array $typeConfig, /** @scrutinizer ignore-unused */ $typeDefinitionNode): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
44
    {
45
        /* TODO: extended datatypes
46 23
        if ($typeConfig['name'] === 'Email') {
47
            $typeConfig = array_merge($typeConfig, [
48
                'serialize' => function ($value) {
49
                    // ...
50
                },
51
                'parseValue' => function ($value) {
52
                    // ...
53
                },
54
                'parseLiteral' => function ($ast) {
55
                    // ...
56
                }
57
            ]);
58
        } */
59
        return $typeConfig;
60
    }
61
62
    /**
63
     * Returns a Parser from a string
64
     *
65
     * @param string $data the string
66
     * @return Parser
67
     */
68
    public static function fromString(string $data): self
69 17
    {
70
        $p = new self();
71 17
        $p->ast = \GraphQL\Language\Parser::parse($data);
72 17
        $p->processAst();
73
        $schemaBuilder = new \GraphQL\Utils\BuildSchema(
74
            $p->ast,
75
            [__CLASS__, 'extendDatatypes']
76
        );
77
        
78
        $p->schema = $schemaBuilder->buildSchema();
79
        $p->processSchema();
80
        return $p;
81
    }
82 22
83
    /**
84 22
     *
85 22
     * @param string[] $sources
86 22
     * @return self
87 22
     */
88 22
    public static function fromStrings(array $sources): self
89
    {
90 22
        $p = new self();
91 22
        $schema = new Schema([
92
            'query' => new ObjectType(['name' => 'Query']),
93
            'mutation' => new ObjectType(['name' => 'Mutation']),
94
        ]);
95
96
        foreach ($sources as &$s) {
97
            $s = \Safe\preg_replace('/^type Mutation/m', 'extend type Mutation', $s);
98
            $s = \Safe\preg_replace('/^type Query/m', 'extend type Query', $s);
99 1
        }
100
        $extensionSource = implode("\n\n", $sources);
101 1
        $p->ast = \GraphQL\Language\Parser::parse($extensionSource);
102 1
103 1
        // TODO: extendDatatypes
104 1
        $p->schema = SchemaExtender::extend(
105
            $schema,
106
            $p->ast
107 1
        );
108 1
        // $schemaBuilder = new \GraphQL\Utils\BuildSchema(
109 1
        //     $p->ast,
110
        //     [__CLASS__, 'extendDatatypes']
111 1
        // );
112 1
113
        // $p->schema = $schemaBuilder->buildSchema();
114
        $p->processAst();
115 1
        return $p;
116 1
    }
117 1
118
    /**
119
     *
120
     * @param array $files
121
     * @return self
122
     * @throws \Safe\Exceptions\FilesystemException
123
     */
124
    public static function fromFiles(array $files): self
125 1
    {
126
        $sources = [
127
            Formularium::validatorGraphqlDirectives()
128
        ];
129 21
        foreach ($files as $f) {
130
            $data = \Safe\file_get_contents($f);
131 21
            $sources = array_merge($sources, static::processImports($data, dirname($f)));
132
            $sources[] = $data;
133
        }
134 4
        return static::fromStrings($sources);
135
    }
136 4
137
    /**
138
     * Returns a Parser from a file path
139
     *
140
     * @param string $path The file path
141
     * @return Parser
142
     * @throws \Safe\Exceptions\FilesystemException If file is not found or parsing fails.
143
     */
144
    public static function fromFile(string $path): self
145
    {
146
        $data = \Safe\file_get_contents($path);
147
        $imports = static::processImports($data, dirname($path));
148
        // TODO: recurse imports
149
        return self::fromString(implode("\n", $imports) . $data);
150
    }
151
152
    protected function processSchema(): void
153
    {
154
        $originalTypeLoader = $this->schema->getConfig()->typeLoader;
155
156
        $this->schema->getConfig()->typeLoader = function ($typeName) use ($originalTypeLoader) {
157
            $type = $originalTypeLoader($typeName);
158
            if ($type instanceof \GraphQL\Type\Definition\CustomScalarType) {
159
                $scalarName = $type->name;
160
                $className = $this->scalars[$scalarName];
161
                return new $className($type->config);
162
            }
163
            return $type;
164
        };
165
    }
166
167
    protected function processAst(): void
168
    {
169
        $this->ast = Visitor::visit($this->ast, [
170
            // load the scalar type classes
171
            NodeKind::SCALAR_TYPE_DEFINITION => function ($node) {
172
                $scalarName = $node->name->value;
173
174
                // load classes
175
                $className = null;
176
                foreach ($node->directives as $directive) {
177
                    switch ($directive->name->value) {
178
                    case 'scalar':
179
                        foreach ($directive->arguments as $arg) {
180
                            /**
181
                             * @var \GraphQL\Language\AST\ArgumentNode $arg
182
                             */
183
        
184
                            $value = $arg->value->value;
0 ignored issues
show
Bug introduced by
Accessing value on the interface GraphQL\Language\AST\ValueNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
185
        
186
                            switch ($arg->name->value) {
187
                            case 'class':
188
                                $className = $value;
189
                            break;
190
                            }
191
                        }
192
                    break;
193
                    }
194
                }
195
196
                // Require special handler class for custom scalars:
197
                if (!class_exists($className, true)) {
198
                    throw new \Modelarium\Exception\Exception(
199
                        "Custom scalar must have corresponding handler class $className"
200
                    );
201
                }
202
203
                $this->scalars[$scalarName] = $className;
204
205
                // return
206
                //   null: no action
207
                //   Visitor::skipNode(): skip visiting this node
208
                //   Visitor::stop(): stop visiting altogether
209
                //   Visitor::removeNode(): delete this node
210
                //   any value: replace this node with the returned value
211
                return null;
212
            }
213
        ]);
214
    }
215
216
    /**
217
     * @param string $data
218
     * @return string[]
219
     */
220
    protected static function processImports(string $data, string $basedir): array
221
    {
222
        $matches = [];
223
        $imports = \Safe\preg_match_all('/^#import\s+\"([^"]+)\"$/m', $data, $matches, PREG_SET_ORDER, 0);
224
        if (!$imports) {
225
            return [];
226
        }
227
        return array_map(
228
            function ($i) use ($basedir) {
229
                if ($i[1] === 'formularium.graphql') {
230
                    return \Safe\file_get_contents(__DIR__ . '/Types/Graphql/scalars.graphql');
231
                }
232
                return \Safe\file_get_contents($basedir . '/' . $i[1]);
233
            },
234
            $matches
235
        );
236
    }
237
238
    public function getSchema(): Schema
239
    {
240
        return $this->schema;
241
    }
242
243
    public function getType(string $name) : ?Type
244
    {
245
        return $this->schema->getType($name);
246
    }
247
248
    public function getScalars(): array
249
    {
250
        return $this->scalars;
251
    }
252
253
    /**
254
     * Factory.
255
     *
256
     * @param string $datatype
257
     * @return ScalarType
258
     */
259
    public function getScalarType(string $datatype): ?ScalarType
260
    {
261
        $className = $this->scalars[$datatype] ?? null;
262
        if (!$className) {
263
            return null;
264
        }
265
        if (!class_exists($className)) {
266
            throw new ScalarNotFoundException("Class not found for $datatype ($className)");
267
        }
268
        return new $className();
269
    }
270
}
271