Passed
Push — master ( a0d881...ad9c9b )
by Bruno
09:28
created

Parser::setImport()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 2
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
    /**
33
     * @var string[]
34
     */
35
    protected $imports = [];
36
37
    public function __construct()
38
    {
39
        $this->scalars = [
40 22
            'String' => 'Modelarium\\Types\\Datatype_string',
41
            'Int' => 'Modelarium\\Types\\Datatype_integer',
42
            'Float' => 'Modelarium\\Types\\Datatype_float',
43 23
            'Boolean' => 'Modelarium\\Types\\Datatype_bool',
44
        ];
45
46 23
        $this->imports = [
47
            'formularium.graphql' => \Safe\file_get_contents(__DIR__ . '/Types/Graphql/scalars.graphql'),
48
        ];
49
    }
50
51
    /** @phpstan-ignore-next-line */
52
    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

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