Passed
Push — develop ( 9eaf21...31cd7f )
by Maarten
02:46
created

concatSchemaDefinitionFilesFromPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace DeInternetJongens\LighthouseUtils\Generators;
4
5
use DeInternetJongens\LighthouseUtils\Events\GraphQLSchemaGenerated;
6
use DeInternetJongens\LighthouseUtils\Exceptions\InvalidConfigurationException;
7
use DeInternetJongens\LighthouseUtils\Generators\Classes\ParseDefinitions;
8
use DeInternetJongens\LighthouseUtils\Generators\Mutations\CreateMutationWithInputTypeGenerator;
9
use DeInternetJongens\LighthouseUtils\Generators\Mutations\DeleteMutationGenerator;
10
use DeInternetJongens\LighthouseUtils\Generators\Mutations\UpdateMutationWithInputTypeGenerator;
11
use DeInternetJongens\LighthouseUtils\Generators\Queries\FindQueryGenerator;
12
use DeInternetJongens\LighthouseUtils\Generators\Queries\PaginateAllQueryGenerator;
13
use DeInternetJongens\LighthouseUtils\Models\GraphQLSchema;
14
use DeInternetJongens\LighthouseUtils\Schema\Scalars\Date;
15
use DeInternetJongens\LighthouseUtils\Schema\Scalars\DateTimeTz;
16
use DeInternetJongens\LighthouseUtils\Schema\Scalars\Email;
17
use DeInternetJongens\LighthouseUtils\Schema\Scalars\FullTextSearch;
18
use DeInternetJongens\LighthouseUtils\Schema\Scalars\PostalCodeNl;
19
use GraphQL\Type\Definition\BooleanType;
20
use GraphQL\Type\Definition\EnumType;
21
use GraphQL\Type\Definition\FieldDefinition;
22
use GraphQL\Type\Definition\FloatType;
23
use GraphQL\Type\Definition\IDType;
24
use GraphQL\Type\Definition\IntType;
25
use GraphQL\Type\Definition\ObjectType;
26
use GraphQL\Type\Definition\StringType;
27
use GraphQL\Type\Definition\Type;
28
use GraphQL\Type\Schema;
29
use Nuwave\Lighthouse\Events\BuildingAST;
30
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
31
use Nuwave\Lighthouse\Schema\Types\Scalars\DateTime;
32
33
class SchemaGenerator
34
{
35
    /** @var array */
36
    private $requiredSchemaFileKeys = ['mutations', 'queries', 'types'];
37
38
    /** @var array */
39
    private $supportedGraphQLTypes = [
40
        IDType::class,
41
        StringType::class,
42
        IntType::class,
43
        FloatType::class,
44
        ObjectType::class,
45
        Date::class,
46
        DateTime::class,
47
        DateTimeTZ::class,
48
        PostalCodeNl::class,
49
        EnumType::class,
50
        Email::class,
51
        FullTextSearch::class,
52
        BooleanType::class,
53
    ];
54
55
    /**
56
     * @var \DeInternetJongens\LighthouseUtils\Generators\Classes\ParseDefinitions
57
     */
58
    private $definitionsParser;
59
60
    /** @var SchemaSourceProvider */
61
    private $schemaSourceProvider;
62
63
    /**
64
     * SchemaGenerator constructor.
65
     *
66
     * @param \DeInternetJongens\LighthouseUtils\Generators\Classes\ParseDefinitions $definitionsParser
67
     * @param SchemaSourceProvider $schemaSourceProvider
68
     */
69
    public function __construct(ParseDefinitions $definitionsParser, SchemaSourceProvider $schemaSourceProvider)
70
    {
71
        $this->definitionsParser = $definitionsParser;
72
        $this->schemaSourceProvider = $schemaSourceProvider;
73
    }
74
75
    /**
76
     * Generates a schema from an array of definition file directories
77
     * For now, this only supports Types.
78
     * In the future it should also support Mutations and Queries.
79
     *
80
     * @param array $definitionFileDirectories
81
     * @return string Generated Schema with Types and Queries
82
     * @throws InvalidConfigurationException
83
     * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
84
     * @throws \Nuwave\Lighthouse\Exceptions\ParseException
85
     */
86
    public function generate(array $definitionFileDirectories): string
87
    {
88
        $authEnabled = config('lighthouse-utils.authorization');
89
        if ($authEnabled) {
90
            GraphQLSchema::truncate();
91
        }
92
93
        $this->validateFilesPaths($definitionFileDirectories);
94
95
        $schema = $this->getSchemaForFiles($definitionFileDirectories);
96
97
        $definedTypes = $this->getDefinedTypesFromSchema($schema, $definitionFileDirectories);
98
99
        $queries = $this->generateQueriesForDefinedTypes($definedTypes, $definitionFileDirectories);
100
        $typesImports = $this->concatSchemaDefinitionFilesFromPath(
101
            $this->definitionsParser->getGraphqlDefinitionFilePaths($definitionFileDirectories['types'])
102
        );
103
104
        if ($authEnabled) {
105
            event(new GraphQLSchemaGenerated(GraphQLSchema::all()));
106
        }
107
108
        //Merge queries and types into one file with required newlines
109
        return sprintf("%s\r\n\r\n%s\r\n", $typesImports, $queries);
110
    }
111
112
    /**
113
     * Validates if the given defintionFileDirectories contains;
114
     * - All required keys
115
     * - Filled values for each key
116
     * - Existing paths for each key
117
     *
118
     * @param array $definitionFileDirectories
119
     * @return bool
120
     * @throws InvalidConfigurationException
121
     */
122
    private function validateFilesPaths(array $definitionFileDirectories): bool
123
    {
124
        if (count($definitionFileDirectories) < 1) {
125
            throw new InvalidConfigurationException(
126
                'The "schema_paths" config value is empty, it should contain a value with a valid path for the following keys: mutations, queries, types'
127
            );
128
        }
129
130
        if (array_diff(array_keys($definitionFileDirectories), $this->requiredSchemaFileKeys)) {
131
            throw new InvalidConfigurationException(
132
                'The "schema_paths" config value is incomplete, it should contain a value with a valid path for the following keys: mutations, queries, types'
133
            );
134
        }
135
        foreach ($definitionFileDirectories as $key => $path) {
136
            if (empty($path)) {
137
                throw new InvalidConfigurationException(
138
                    sprintf(
139
                        'The "schema_paths" config value for key "%s" is empty, it should contain a value with a valid path',
140
                        $key
141
                    )
142
                );
143
            }
144
145
            if (! file_exists($path)) {
146
                throw new InvalidConfigurationException(
147
                    sprintf('The "schema_paths" config value for key "%s" contains a path that does not exist', $key)
148
                );
149
            }
150
        }
151
152
        return true;
153
    }
154
155
    /**
156
     * Generates a GraphQL schema for a set of definition files
157
     * Definition files can only be Types at this time
158
     * In the future this should also support Mutations and Queries
159
     *
160
     * @param array $definitionFileDirectories
161
     * @return Schema
162
     * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
163
     * @throws \Nuwave\Lighthouse\Exceptions\ParseException
164
     */
165
    private function getSchemaForFiles(array $definitionFileDirectories): Schema
166
    {
167
        resolve('events')->listen(
168
            BuildingAST::class,
169
            function () use ($definitionFileDirectories) {
170
                $typeDefinitionPaths = $this->definitionsParser->getGraphqlDefinitionFilePaths(
171
                    $definitionFileDirectories['types']
172
                );
173
                $relativeTypeImports = $this->concatSchemaDefinitionFilesFromPath($typeDefinitionPaths);
174
175
                // Webonyx GraphQL will not generate a schema if there is not at least one query
176
                // So just pretend we have one
177
                $placeholderQuery = 'type Query{placeholder:String}';
178
                return "$relativeTypeImports\r\n$placeholderQuery";
179
            }
180
        );
181
182
        $schema = graphql()->prepSchema();
183
184
        return $schema;
185
    }
186
187
    /**
188
     * @param array $schemaDefinitionFilePaths
189
     * @return string
190
     */
191
    private function concatSchemaDefinitionFilesFromPath(array $schemaDefinitionFilePaths): string
192
    {
193
        $concatenatedImports = '';
194
        foreach ($schemaDefinitionFilePaths as $filePath) {
195
            $concatenatedImports .= file_get_contents($filePath);
196
            $concatenatedImports .= "\r\n";
197
        }
198
199
        return $concatenatedImports;
200
    }
201
202
    /**
203
     * Parse defined types from a schema into an array with the native GraphQL Scalar types for each field
204
     *
205
     * @param Schema $schema
206
     * @param array $definitionFileDirectories
207
     * @return Type[]
208
     */
209
    private function getDefinedTypesFromSchema(Schema $schema, array $definitionFileDirectories): array
210
    {
211
        $definedTypes = $this->definitionsParser->getGraphqlDefinitionFilePaths(
212
            $definitionFileDirectories['types']
213
        );
214
215
        foreach ($definedTypes as $key => $type) {
216
            $definedTypes[$key] = str_replace('.graphql', '', basename($type));
217
        }
218
219
        $internalTypes = [];
220
        /**
221
         * @var string $typeName
222
         * @var ObjectType $type
223
         */
224
        foreach ($schema->getTypeMap() as $typeName => $type) {
225
            if (! in_array($typeName, $definedTypes) || ! method_exists($type, 'getFields')) {
226
                continue;
227
            }
228
229
            /**
230
             * @var string $fieldName
231
             * @var FieldDefinition $fieldType
232
             */
233
            foreach ($type->getFields() as $fieldName => $fieldType) {
234
                $graphQLType = $fieldType->getType();
235
236
                //Every required field is defined by a parent 'NonNullType'
237
                if (method_exists($graphQLType, 'getWrappedType')) {
238
                    // Clone the field to prevent pass by reference,
239
                    // because we want to add a config value unique to this field.
240
                    $graphQLType = clone $graphQLType->getWrappedType();
241
242
                    //We want to know later on wether or not a field is required
243
                    $graphQLType->config['generator-required'] = true;
244
                }
245
246
                if (! in_array(get_class($graphQLType), $this->supportedGraphQLTypes)) {
247
                    continue;
248
                };
249
250
                // This retrieves the GraphQL type for this field from the webonyx/graphql-php package
251
                $internalTypes[$typeName][$fieldName] = $graphQLType;
252
            }
253
        }
254
255
        return $internalTypes;
256
    }
257
258
    /**
259
     * Auto-generates a query for each definedType
260
     * These queries contain arguments for each field defined in the Type
261
     *
262
     * @param array $definedTypes
263
     * @param array $definitionFileDirectories
264
     * @return string
265
     */
266
    private function generateQueriesForDefinedTypes(array $definedTypes, array $definitionFileDirectories): string
267
    {
268
        $queries = [];
269
        $mutations = [];
270
        $inputTypes = [];
271
272
        /**
273
         * @var string $typeName
274
         * @var Type $type
275
         */
276
        foreach ($definedTypes as $typeName => $type) {
277
            $paginateAndAllQuery = PaginateAllQueryGenerator::generate($typeName, $type);
278
279
            if (! empty($paginateAndAllQuery)) {
280
                $queries[] = $paginateAndAllQuery;
281
            }
282
            $findQuery = FindQueryGenerator::generate($typeName, $type);
283
284
            if (! empty($findQuery)) {
285
                $queries[] = $findQuery;
286
            }
287
288
            $createMutation = createMutationWithInputTypeGenerator::generate($typeName, $type);
289
            if ($createMutation->isNotEmpty()) {
290
                $mutations[] = $createMutation->getMutation();
291
                $inputTypes[] = $createMutation->getInputType();
292
            }
293
294
            $updateMutation = updateMutationWithInputTypeGenerator::generate($typeName, $type);
295
            if ($updateMutation->isNotEmpty()) {
296
                $mutations[] = $updateMutation->getMutation();
297
                $inputTypes[] = $updateMutation->getInputType();
298
            }
299
300
            $deleteMutation = DeleteMutationGenerator::generate($typeName, $type);
301
            if (! empty($deleteMutation)) {
302
                $mutations[] = $deleteMutation;
303
            }
304
        }
305
306
        $queries = array_merge(
307
            $queries,
308
            $this->definitionsParser->parseCustomQueriesFrom($definitionFileDirectories['queries'])
309
        );
310
311
        $mutations = array_merge(
312
            $mutations,
313
            $this->definitionsParser->parseCustomMutationsFrom($definitionFileDirectories['mutations'])
314
        );
315
316
        $return = sprintf("type Query{\r\n%s\r\n}", implode("\r\n", $queries));
317
        $return .= "\r\n\r\n";
318
        $return .= sprintf("type Mutation{\r\n%s\r\n}", implode("\r\n", $mutations));
319
        $return .= "\r\n\r\n";
320
        $return .= implode("\r\n", $inputTypes);
321
322
        return $return;
323
    }
324
}
325