GraphQLTypeGenerator::generateTypeMapper()   B
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 41
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 8.8571
c 0
b 0
f 0
nc 4
cc 3
eloc 19
nop 1
1
<?php
2
namespace TheCodingMachine\Tdbm\GraphQL;
3
4
use Mouf\Composer\ClassNameMapper;
5
use TheCodingMachine\TDBM\Configuration;
6
use TheCodingMachine\TDBM\ConfigurationInterface;
7
use TheCodingMachine\TDBM\Utils\AbstractBeanPropertyDescriptor;
8
use TheCodingMachine\TDBM\Utils\BeanDescriptorInterface;
9
use TheCodingMachine\TDBM\Utils\DirectForeignKeyMethodDescriptor;
10
use TheCodingMachine\TDBM\Utils\GeneratorListenerInterface;
11
use Symfony\Component\Filesystem\Filesystem;
12
use TheCodingMachine\TDBM\Utils\MethodDescriptorInterface;
13
use TheCodingMachine\TDBM\Utils\ObjectBeanPropertyDescriptor;
14
use TheCodingMachine\TDBM\Utils\ScalarBeanPropertyDescriptor;
15
use Youshido\GraphQL\Type\Scalar\BooleanType;
16
use Youshido\GraphQL\Type\Scalar\DateTimeType;
17
use Youshido\GraphQL\Type\Scalar\FloatType;
18
use Youshido\GraphQL\Type\Scalar\IdType;
19
use Youshido\GraphQL\Type\Scalar\IntType;
20
use Youshido\GraphQL\Type\Scalar\StringType;
21
22
class GraphQLTypeGenerator implements GeneratorListenerInterface
23
{
24
    /**
25
     * @var string
26
     */
27
    private $namespace;
28
    /**
29
     * @var string
30
     */
31
    private $generatedNamespace;
32
    /**
33
     * @var null|NamingStrategyInterface
34
     */
35
    private $namingStrategy;
36
    /**
37
     * @var ClassNameMapper
38
     */
39
    private $classNameMapper;
40
41
    /**
42
     * @param string $namespace The namespace the type classes will be written in.
43
     * @param string|null $generatedNamespace The namespace the generated type classes will be written in (defaults to $namespace + '\Generated')
44
     * @param NamingStrategyInterface|null $namingStrategy
45
     * @param ClassNameMapper|null $classNameMapper
46
     */
47
    public function __construct(string $namespace, ?string $generatedNamespace = null, ?NamingStrategyInterface $namingStrategy = null, ?ClassNameMapper $classNameMapper = null)
48
    {
49
        $this->namespace = trim($namespace, '\\');
50
        if ($generatedNamespace !== null) {
51
            $this->generatedNamespace = $generatedNamespace;
52
        } else {
53
            $this->generatedNamespace = $namespace.'\\Generated';
54
        }
55
        $this->namingStrategy = $namingStrategy ?: new DefaultNamingStrategy();
56
        $this->classNameMapper = $classNameMapper ?: ClassNameMapper::createFromComposerFile();
57
    }
58
59
    /**
60
     * @param ConfigurationInterface $configuration
61
     * @param BeanDescriptorInterface[] $beanDescriptors
62
     */
63
    public function onGenerate(ConfigurationInterface $configuration, array $beanDescriptors): void
64
    {
65
        $this->generateTypes($beanDescriptors);
66
        $this->generateTypeMapper($beanDescriptors);
67
    }
68
69
    /**
70
     * @param BeanDescriptorInterface[] $beanDescriptors
71
     */
72
    private function generateTypes(array $beanDescriptors): void
73
    {
74
        foreach ($beanDescriptors as $beanDescriptor) {
75
            $this->generateAbstractTypeFile($beanDescriptor);
76
            $this->generateMainTypeFile($beanDescriptor);
77
        }
78
    }
79
80
    private function generateAbstractTypeFile(BeanDescriptorInterface $beanDescriptor)
81
    {
82
        // FIXME: find a way around inheritance issues => we should have interfaces for inherited tables.
83
        // Ideally, the interface should have the same fields as the type (so no issue)
84
85
        $generatedTypeClassName = $this->namingStrategy->getGeneratedClassName($beanDescriptor->getBeanClassName());
86
        $typeName = var_export($this->namingStrategy->getGraphQLType($beanDescriptor->getBeanClassName()), true);
87
88
        $properties = $beanDescriptor->getExposedProperties();
89
90
        $properties = array_filter($properties, [$this, 'canBeCastToGraphQL']);
91
92
        $fieldsCodes = array_map([$this, 'generateFieldCode'], $properties);
93
94
        $fieldsCode = implode('', $fieldsCodes);
95
96
        $extendedBeanClassName = $beanDescriptor->getExtendedBeanClassName();
97
        if ($extendedBeanClassName === null) {
98
            $baseClassName = 'TdbmObjectType';
99
            $callParentBuild = '';
100
            $isExtended = false;
101
        } else {
102
            $baseClassName = '\\'.$this->namespace.'\\'.$this->namingStrategy->getClassName($extendedBeanClassName);
103
            $isExtended = true;
104
            $callParentBuild = "parent::build(\$config);\n        ";
105
        }
106
107
        // one to many and many to many relationships:
108
        $methodDescriptors = $beanDescriptor->getMethodDescriptors();
109
        $relationshipsCodes = array_map([$this, 'generateRelationshipsCode'], $methodDescriptors);
110
        $relationshipsCode = implode('', $relationshipsCodes);
111
112
        $fieldFetcherCodes = array_map(function (AbstractBeanPropertyDescriptor $propertyDescriptor) {
113
            return '            $this->'.$propertyDescriptor->getGetterName(). 'Field(),';
114
        }, $properties);
115
        $fieldFetcherCodes = array_merge($fieldFetcherCodes, array_map(function (MethodDescriptorInterface $propertyDescriptor) {
116
            return '            $this->'.$propertyDescriptor->getName(). 'Field(),';
117
        }, $methodDescriptors));
118
        $fieldFetcherCode = implode("\n", $fieldFetcherCodes);
119
120
121
        $str = <<<EOF
122
<?php
123
namespace {$this->generatedNamespace};
124
125
//use Youshido\GraphQL\Relay\Connection\Connection;
126
//use Youshido\GraphQL\Relay\Connection\ArrayConnection;
127
use TheCodingMachine\Tdbm\GraphQL\Field;
128
use TheCodingMachine\Tdbm\GraphQL\TdbmObjectType;
129
use TheCodingMachine\Tdbm\GraphQL\Registry\Registry;
130
use Youshido\GraphQL\Type\ListType\ListType;
131
use Youshido\GraphQL\Config\Object\ObjectTypeConfig;
132
use Youshido\GraphQL\Type\NonNullType;
133
134
abstract class $generatedTypeClassName extends $baseClassName
135
{
136
137
EOF;
138
        if (!$isExtended) {
139
            $str .= <<<EOF
140
    protected \$registry;
141
142
    public function __construct(Registry \$registry, array \$config = [])
143
    {
144
        parent::__construct(\$config);
145
        \$this->registry = \$registry;
146
    }
147
148
    /**
149
     * Alters the list of properties for this type.
150
     */
151
    abstract public function alter(): void;
152
153
154
EOF;
155
        }
156
        $str .= <<<EOF
157
    public function getName()
158
    {
159
        return $typeName;
160
    }
161
    
162
    /**
163
     * @param ObjectTypeConfig \$config
164
     */
165
    public function build(\$config)
166
    {
167
        $callParentBuild\$this->alter();
168
        \$config->addFields(array_filter(\$this->getFieldList(), function (\$field) {
169
            return !\$field->isHidden();
170
        }));
171
    }
172
    
173
    /**
174
     * @return Field[]
175
     */
176
    protected function getFieldList(): array
177
    {
178
        return array_merge(parent::getFieldList(), [
179
$fieldFetcherCode
180
        ]);
181
    }
182
    
183
$fieldsCode
184
$relationshipsCode
185
EOF;
186
187
        $str = rtrim($str, "\n ")."\n}\n";
188
189
        $fileSystem = new Filesystem();
190
191
        $fqcn = $this->generatedNamespace.'\\'.$generatedTypeClassName;
192
        $generatedFilePaths = $this->classNameMapper->getPossibleFileNames($this->generatedNamespace.'\\'.$generatedTypeClassName);
193
        if (empty($generatedFilePaths)) {
194
            throw new GraphQLGeneratorDirectForeignKeyMethodDescriptorNamespaceException('Unable to find a suitable autoload path for class '.$fqcn);
195
        }
196
197
        $fileSystem->dumpFile($generatedFilePaths[0], $str);
198
    }
199
200
    private function generateMainTypeFile(BeanDescriptorInterface $beanDescriptor)
201
    {
202
        $typeClassName = $this->namingStrategy->getClassName($beanDescriptor->getBeanClassName());
203
        $generatedTypeClassName = $this->namingStrategy->getGeneratedClassName($beanDescriptor->getBeanClassName());
204
205
        $fileSystem = new Filesystem();
206
207
        $fqcn = $this->namespace.'\\'.$typeClassName;
208
        $filePaths = $this->classNameMapper->getPossibleFileNames($fqcn);
209
        if (empty($filePaths)) {
210
            throw new GraphQLGeneratorNamespaceException('Unable to find a suitable autoload path for class '.$fqcn);
211
        }
212
        $filePath = $filePaths[0];
213
214
        if ($fileSystem->exists($filePath)) {
215
            return;
216
        }
217
218
        $isExtended = $beanDescriptor->getExtendedBeanClassName() !== null;
219
        if ($isExtended) {
220
            $alterParentCall = "parent::alter();\n        ";
221
        } else {
222
            $alterParentCall = '';
223
        }
224
225
        $str = <<<EOF
226
<?php
227
namespace {$this->namespace};
228
229
use {$this->generatedNamespace}\\$generatedTypeClassName;
230
231
class $typeClassName extends $generatedTypeClassName
232
{
233
    /**
234
     * Alters the list of properties for this type.
235
     */
236
    public function alter(): void
237
    {
238
        $alterParentCall// You can alter the fields of this type here.
239
        \$this->showAll();
240
    }
241
}
242
243
EOF;
244
245
        $fileSystem->dumpFile($filePaths[0], $str);
246
    }
247
248
    /**
249
     * Some fields cannot be bound to GraphQL fields (for instance JSON fields)
250
     */
251
    private function canBeCastToGraphQL(AbstractBeanPropertyDescriptor $descriptor) : bool
252
    {
253
        if ($descriptor instanceof ScalarBeanPropertyDescriptor) {
254
            $phpType = $descriptor->getPhpType();
255
            if ($phpType === 'array' || $phpType === 'resource') {
256
                // JSON or BLOB types cannot be casted since GraphQL does not allow for untyped arrays or BLOB.
257
                return false;
258
            }
259
        }
260
        return true;
261
    }
262
263
    private function generateFieldCode(AbstractBeanPropertyDescriptor $descriptor) : string
264
    {
265
        $getterName = $descriptor->getGetterName();
266
        $fieldNameAsCode = var_export($this->namingStrategy->getFieldName($descriptor), true);
267
        $variableName = $descriptor->getVariableName().'Field';
268
        $thisVariableName = '$this->'.substr($descriptor->getVariableName().'Field', 1);
269
270
        $type = $this->getType($descriptor);
271
272
        if ($type === null) {
273
            return <<<EOF
274
    // Field $getterName is ignored. Cannot represent a JSON  or BLOB field in GraphQL.
275
276
EOF;
277
        }
278
279
        $code = <<<EOF
280
    private $variableName;
281
        
282
    protected function {$getterName}Field() : Field
283
    {
284
        if ($thisVariableName === null) {
285
            $thisVariableName = new Field($fieldNameAsCode, $type, \$this->registry);
286
        }
287
        return $thisVariableName;
288
    }
289
290
EOF;
291
292
        return $code;
293
    }
294
295
    private function getType(AbstractBeanPropertyDescriptor $descriptor) : ?string
296
    {
297
        // FIXME: can there be several primary key? If yes, we might need to fix this.
298
        // Also, primary key should be named "ID"
299
        if ($descriptor->isPrimaryKey()) {
300
            return 'new \\'.IdType::class.'()';
301
        }
302
303
        $phpType = $descriptor->getPhpType();
304
        if ($descriptor instanceof ScalarBeanPropertyDescriptor) {
305
            if ($phpType === 'array' || $phpType === 'resource') {
306
                // JSON and BLOB type cannot be casted since GraphQL does not allow for untyped arrays or BLOB.
307
                return null;
308
            }
309
310
            $map = [
311
                'string' => '\\'.StringType::class,
312
                'bool' => '\\'.BooleanType::class,
313
                '\DateTimeImmutable' => '\\'.DateTimeType::class,
314
                'float' => '\\'.FloatType::class,
315
                'int' => '\\'.IntType::class,
316
            ];
317
318
            if (!isset($map[$phpType])) {
319
                throw new GraphQLGeneratorNamespaceException("Cannot map PHP type '$phpType' to any known GraphQL type in table '{$descriptor->getTable()->getName()}' for column '{$descriptor->getColumnName()}'.");
320
            }
321
322
            $newCode = 'new '.$map[$phpType].'()';
323
        } elseif ($descriptor instanceof ObjectBeanPropertyDescriptor) {
324
            $beanclassName = $descriptor->getClassName();
325
            $newCode = '$this->registry->get(\''.$this->namespace.'\\'.$this->namingStrategy->getClassName($beanclassName).'\')';
326
        } else {
327
            throw new GraphQLGeneratorNamespaceException('Unexpected property descriptor. Cannot handle class '.get_class($descriptor));
328
        }
329
330
        if ($descriptor->isCompulsory()) {
331
            $newCode = "new NonNullType($newCode)";
332
        }
333
334
        return $newCode;
335
    }
336
337
    private function generateRelationshipsCode(MethodDescriptorInterface $descriptor): string
338
    {
339
        $getterName = $descriptor->getName();
340
        $fieldName = $this->namingStrategy->getFieldNameFromRelationshipDescriptor($descriptor);
341
        $fieldNameAsCode = var_export($fieldName, true);
342
        $variableName = '$'.$fieldName.'Field';
343
        $thisVariableName = '$this->'.$fieldName.'Field';
344
345
        $type = 'new NonNullType(new ListType(new NonNullType($this->registry->get(\''.$this->namespace.'\\'.$this->namingStrategy->getClassName($descriptor->getBeanClassName()).'\'))))';
346
347
        // FIXME: suboptimal code! We need to be able to call ->take for pagination!!!
348
        /*$code = <<<EOF
349
    private $variableName;
350
351
    protected function {$getterName}Field() : Field
352
    {
353
        if ($thisVariableName === null) {
354
            $thisVariableName = new Field($fieldNameAsCode, Connection::connectionDefinition($type), [
355
                'args' => Connection::connectionArgs(),
356
                'resolve' => function (\$value = null, \$args = [], \$type = null) {
357
                    return ArrayConnection::connectionFromArray(\$value->$getterName(), \$args);
358
                }
359
            ]);
360
        }
361
        return $thisVariableName;
362
    }
363
364
365
EOF;*/
366
        $code = <<<EOF
367
    private $variableName;
368
        
369
    protected function {$getterName}Field() : Field
370
    {
371
        if ($thisVariableName === null) {
372
            $thisVariableName = new Field($fieldNameAsCode, $type, \$this->registry);
373
        }
374
        return $thisVariableName;
375
    }
376
377
378
EOF;
379
380
        return $code;
381
    }
382
383
    /**
384
     * @param BeanDescriptorInterface[] $beanDescriptors
385
     */
386
    private function generateTypeMapper(array $beanDescriptors)
387
    {
388
        $mapCode = '';
389
390
        foreach ($beanDescriptors as $beanDescriptor) {
391
            $fqcn = $beanDescriptor->getBeanNamespace().'\\'.$beanDescriptor->getBeanClassName();
392
            $graphqlType = $this->namespace.'\\'.$this->namingStrategy->getClassName($beanDescriptor->getBeanClassName());
393
394
            $beanToGraphQLMap[$fqcn] = $graphqlType;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$beanToGraphQLMap was never initialized. Although not strictly required by PHP, it is generally a good practice to add $beanToGraphQLMap = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
395
            $mapCode .= '            '.var_export($fqcn, true).' => '.var_export($graphqlType, true).",\n";
396
        }
397
398
399
        $str = <<<EOF
400
<?php
401
namespace {$this->namespace};
402
403
use TheCodingMachine\Tdbm\GraphQL\AbstractTdbmGraphQLTypeMapper;
404
405
class TdbmGraphQLTypeMapper extends AbstractTdbmGraphQLTypeMapper
406
{
407
    protected function getMap(): array
408
    {
409
        return [
410
$mapCode
411
        ];
412
    }
413
}
414
415
EOF;
416
417
        $classMapperFqcn = $this->namespace.'\\TdbmGraphQLTypeMapper';
418
419
        $fileSystem = new Filesystem();
420
        $filePaths = $this->classNameMapper->getPossibleFileNames($classMapperFqcn);
421
        if (empty($filePaths)) {
422
            throw new GraphQLGeneratorNamespaceException('Unable to find a suitable autoload path for class '.$fqcn);
423
        }
424
        $filePath = $filePaths[0];
425
        $fileSystem->dumpFile($filePath, $str);
426
    }
427
}
428