Schema::__sleep()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Bakery\Support;
4
5
use Bakery\Types;
6
use Bakery\Utils\Utils;
7
use GraphQL\Type\SchemaConfig;
8
use Bakery\Eloquent\ModelSchema;
9
use Illuminate\Support\Collection;
10
use Bakery\Fields\PolymorphicField;
11
use Bakery\Mutations\CreateMutation;
12
use Bakery\Mutations\DeleteMutation;
13
use Bakery\Mutations\UpdateMutation;
14
use Symfony\Component\Finder\Finder;
15
use Bakery\Queries\SingleEntityQuery;
16
use Bakery\Types\Definitions\RootType;
17
use GraphQL\Type\Definition\Directive;
18
use GraphQL\Type\Definition\ObjectType;
19
use Bakery\Mutations\AttachPivotMutation;
20
use Bakery\Mutations\DetachPivotMutation;
21
use GraphQL\Type\Schema as GraphQLSchema;
22
use Bakery\Queries\EloquentCollectionQuery;
23
use Illuminate\Database\Eloquent\Relations\Pivot;
24
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
25
26
class Schema
27
{
28
    /**
29
     * @var TypeRegistry
30
     */
31
    protected $registry;
32
33
    /**
34
     * @var array
35
     */
36
    protected $data;
37
38
    /**
39
     * The models of the schema.
40
     *
41
     * @var array
42
     */
43
    protected $models = [];
44
45
    /**
46
     * The types of the schema.
47
     *
48
     * @var array
49
     */
50
    protected $types = [];
51
52
    /**
53
     * The queries of the schema.
54
     *
55
     * @var array
56
     */
57
    protected $queries = [];
58
59
    /**
60
     * The mutations of the schema.
61
     *
62
     * @var array
63
     */
64
    protected $mutations = [];
65
66
    /**
67
     * The directives of the schema.
68
     *
69
     * @var array
70
     */
71
    protected $directives = [];
72
73
    /**
74
     * The generated GraphQLSchema.
75
     *
76
     * @var GraphQLSchema
77
     */
78
    protected $graphQLSchema;
79
80
    /**
81
     * Schema constructor.
82
     *
83
     * @param \Bakery\Bakery
84
     */
85
    public function __construct()
86
    {
87
        $this->registry = new TypeRegistry();
88
    }
89
90
    /**
91
     * Define the models of the schema.
92
     * This method can be overridden for complex implementations.
93
     *
94
     * @return array
95
     */
96
    protected function models(): array
97
    {
98
        return $this->models;
99
    }
100
101
    /**
102
     * Define the types of the schema.
103
     * This method can be overridden for complex implementations.
104
     *
105
     * @return array
106
     */
107
    protected function types(): array
108
    {
109
        return $this->types;
110
    }
111
112
    /**
113
     * @return \Bakery\Support\TypeRegistry
114
     */
115
    public function getRegistry(): TypeRegistry
116
    {
117
        return $this->registry;
118
    }
119
120
    /**
121
     * @param \Bakery\Support\TypeRegistry $registry
122
     */
123
    public function setRegistry(TypeRegistry $registry)
124
    {
125
        $this->registry = $registry;
126
    }
127
128
    /**
129
     * Define the queries of the schema.
130
     * This method can be overridden for complex implementations.
131
     *
132
     * @return array
133
     */
134
    protected function queries(): array
135
    {
136
        return $this->queries;
137
    }
138
139
    /**
140
     * Define the mutations of the schema.
141
     * This method can be overridden for complex implementations.
142
     *
143
     * @return array
144
     */
145
    protected function mutations(): array
146
    {
147
        return $this->mutations;
148
    }
149
150
    /**
151
     * Define the directives of the schema.
152
     * This method can be overridden for complex implementations.
153
     *
154
     * @return array
155
     */
156
    protected function directives(): array
157
    {
158
        return $this->directives;
159
    }
160
161
    /**
162
     * Get the models of the schema.
163
     * TODO: Rename this to getModelSchemas ?
164
     *
165
     * @return \Illuminate\Support\Collection
166
     */
167
    protected function getModels(): Collection
168
    {
169
        return collect($this->models());
170
    }
171
172
    /**
173
     * Get a collection of all the types of the schema.
174
     *
175
     * @return array
176
     */
177
    public function getTypes(): array
178
    {
179
        return collect()
180
            ->merge($this->getModelTypes())
181
            ->merge($this->getStandardTypes())
182
            ->merge($this->types)
183
            ->merge($this->types())
184
            ->toArray();
185
    }
186
187
    /**
188
     * Get all the types for the models in the schema.
189
     *
190
     * @return Collection
191
     */
192
    protected function getModelTypes(): Collection
193
    {
194
        return $this->getModels()->reduce(function (Collection $types, string $class) {
195
            $schema = $this->registry->getModelSchema($class);
196
197
            if ($schema->getModel() instanceof Pivot) {
198
                $types = $types->merge($this->getPivotModelTypes($schema));
199
            } else {
200
                $types->push(new Types\EntityType($this->registry, $schema))
201
                      ->push(new Types\EntityLookupType($this->registry, $schema))
202
                      ->push(new Types\CollectionFilterType($this->registry, $schema));
203
204
                if ($schema->isSortable()) {
205
                    $types->push(new types\CollectionOrderByType($this->registry, $schema));
206
                }
207
208
                if ($schema->isSearchable()) {
209
                    $types->push(new Types\CollectionSearchType($this->registry, $schema));
210
                }
211
212
                if ($schema->isIndexable()) {
213
                    $types->push(new Types\EntityCollectionType($this->registry, $schema))
214
                          ->push(new Types\CollectionRootSearchType($this->registry, $schema));
215
                }
216
217
                if ($schema->isMutable()) {
218
                    $types->push(new Types\CreateInputType($this->registry, $schema))
219
                          ->push(new Types\UpdateInputType($this->registry, $schema));
220
                }
221
            }
222
223
            // Filter through the regular fields and get the polymorphic types.
224
            $schema->getFields()->filter(function ($field) {
225
                return $field instanceof PolymorphicField;
226
            })->each(function ($field, $key) use ($schema, &$types) {
227
                $types = $types->merge($this->getPolymorphicFieldTypes($schema, $key, $field));
228
            });
229
230
            // Filter through the relations of the model and get the
231
            // belongsToMany relations and get the pivot input types
232
            // for that relation.
233
            $schema->getRelations()->filter(function ($relation) {
234
                return $relation instanceof BelongsToMany;
235
            })->each(function ($relation) use (&$types) {
236
                $types = $types->merge($this->getPivotInputTypes($relation));
237
            });
238
239
            // Filter through the relation fields and get the the
240
            // polymorphic types for the relations.
241
            $schema->getRelationFields()->filter(function ($field) {
242
                return $field instanceof PolymorphicField;
243
            })->each(function ($field, $key) use ($schema, &$types) {
244
                $types = $types->merge($this->getPolymorphicRelationshipTypes($schema, $key, $field));
245
            });
246
247
            return $types;
248
        }, collect());
249
    }
250
251
    /**
252
     * Get the types for a pivot model.
253
     *
254
     * @param \Bakery\Eloquent\ModelSchema $modelSchema
255
     * @return array
256
     */
257
    protected function getPivotModelTypes(ModelSchema $modelSchema): array
258
    {
259
        return [
260
            new Types\EntityType($this->registry, $modelSchema),
261
            new Types\CreatePivotInputType($this->registry, $modelSchema),
262
        ];
263
    }
264
265
    /**
266
     * Get the pivot input types.
267
     *
268
     * @param BelongsToMany $relation
269
     * @return array
270
     */
271
    protected function getPivotInputTypes(BelongsToMany $relation): array
272
    {
273
        // We actually want to create pivot input types for the reverse side here, but we approach
274
        // it from this side because we have the relevant information here (relation name, pivot accessor)
275
        // so we grab the model schema from the related one and pass it through.
276
        $related = $this->registry->getSchemaForModel($relation->getRelated());
277
278
        return [
279
            (new Types\CreateWithPivotInputType($this->registry, $related))->setPivotRelation($relation),
280
            (new Types\PivotInputType($this->registry, $related))->setPivotRelation($relation),
281
        ];
282
    }
283
284
    /**
285
     * Get the types for a polymorphic field.
286
     *
287
     * @param \Bakery\Eloquent\ModelSchema $modelSchema
288
     * @param string $key
289
     * @param \Bakery\Fields\PolymorphicField $field
290
     * @return array
291
     */
292
    protected function getPolymorphicFieldTypes(ModelSchema $modelSchema, string $key, PolymorphicField $field): array
293
    {
294
        $typename = Utils::typename($key).'On'.$modelSchema->typename();
295
        $modelSchemas = $field->getModelSchemas();
296
        $typeResolver = $field->getTypeResolver();
297
298
        return [
299
            (new Types\UnionEntityType($this->registry))
300
                ->setName($typename)
301
                ->typeResolver($typeResolver)
302
                ->setModelSchemas($modelSchemas),
303
        ];
304
    }
305
306
    /**
307
     * Get the types for a polymorphic relationship.
308
     *
309
     * @param \Bakery\Eloquent\ModelSchema $modelSchema
310
     * @param string $key
311
     * @param \Bakery\Fields\PolymorphicField $type
312
     * @return array
313
     */
314
    protected function getPolymorphicRelationshipTypes(ModelSchema $modelSchema, string $key, PolymorphicField $type): array
315
    {
316
        $typename = Utils::typename($key).'On'.$modelSchema->typename();
317
        $modelSchemas = $type->getModelSchemas();
318
        $typeResolver = $type->getTypeResolver();
319
320
        return [
321
            (new Types\UnionEntityType($this->registry))->setName($typename)->typeResolver($typeResolver)->setModelSchemas($modelSchemas),
322
            (new Types\CreateUnionEntityInputType($this->registry))->setName($typename)->setModelSchemas($modelSchemas),
323
            (new Types\AttachUnionEntityInputType($this->registry))->setName($typename)->setModelSchemas($modelSchemas),
324
        ];
325
    }
326
327
    /**
328
     * Get the standard types that we use throughout Bakery.
329
     *
330
     * @return array
331
     */
332
    public function getStandardTypes(): array
333
    {
334
        return [
335
            Types\PaginationType::class,
336
            Types\OrderType::class,
337
        ];
338
    }
339
340
    /**
341
     * Get the queries of the schema.
342
     *
343
     * @return array
344
     */
345
    public function getQueries(): array
346
    {
347
        $queries = collect()
348
            ->merge($this->getModelQueries());
349
350
        foreach ($this->queries as $name => $query) {
351
            $query = is_object($query) ?: new $query($this->registry);
352
            $name = is_string($name) ? $name : $query->getName();
353
            $queries->put($name, $query);
354
        }
355
356
        return $queries->toArray();
357
    }
358
359
    /**
360
     * Get the queries of the models of the schema.
361
     *
362
     * @return Collection
363
     */
364
    public function getModelQueries(): Collection
365
    {
366
        $queries = collect();
367
368
        foreach ($this->getModels() as $modelSchema) {
369
            $modelSchema = $this->registry->getModelSchema($modelSchema);
370
371
            if (! $modelSchema->getModel() instanceof Pivot) {
372
                $entityQuery = new SingleEntityQuery($this->registry, $modelSchema);
373
                $queries->put($entityQuery->getName(), $entityQuery);
374
375
                if ($modelSchema->isIndexable()) {
376
                    $collectionQuery = new EloquentCollectionQuery($this->registry, $modelSchema);
377
                    $queries->put($collectionQuery->getName(), $collectionQuery);
378
                }
379
            }
380
        }
381
382
        return $queries;
383
    }
384
385
    /**
386
     * Get the mutation of the schema.
387
     *
388
     * @return array
389
     */
390
    public function getMutations(): array
391
    {
392
        $mutations = collect()
393
            ->merge($this->getModelMutations());
394
395
        foreach ($this->mutations as $name => $mutation) {
396
            $mutation = is_object($mutation) ?: new $mutation($this->registry);
397
            $name = is_string($name) ? $name : $mutation->getName();
398
            $mutations->put($name, $mutation);
399
        }
400
401
        return $mutations->toArray();
402
    }
403
404
    /**
405
     * Get the mutations of the models of the schema.
406
     *
407
     * @return Collection
408
     */
409
    protected function getModelMutations(): Collection
410
    {
411
        $mutations = collect();
412
413
        foreach ($this->getModels() as $class) {
414
            $modelSchema = $this->registry->getModelSchema($class);
415
416
            $pivotRelations = $modelSchema->getRelations()->filter(function ($relation) {
417
                return $relation instanceof BelongsToMany;
418
            });
419
420
            if ($modelSchema->isMutable() && ! $modelSchema->getModel() instanceof Pivot) {
421
                $createMutation = new CreateMutation($this->registry, $modelSchema);
422
                $mutations->put($createMutation->getName(), $createMutation);
423
424
                $updateMutation = new UpdateMutation($this->registry, $modelSchema);
425
                $mutations->put($updateMutation->getName(), $updateMutation);
426
427
                $deleteMutation = new DeleteMutation($this->registry, $modelSchema);
428
                $mutations->put($deleteMutation->getName(), $deleteMutation);
429
            }
430
431
            foreach ($pivotRelations as $relation) {
432
                $mutations = $mutations->merge(
433
                    $this->getModelPivotMutations($modelSchema, $relation)
434
                );
435
            }
436
        }
437
438
        return $mutations;
439
    }
440
441
    /**
442
     * Get the pivot mutations for a model and a relationship.
443
     *
444
     * @param \Bakery\Eloquent\ModelSchema $modelSchema
445
     * @param BelongsToMany $relation
446
     * @return Collection
447
     */
448
    protected function getModelPivotMutations(ModelSchema $modelSchema, BelongsToMany $relation): Collection
449
    {
450
        $mutations = collect();
451
452
        $mutation = (new AttachPivotMutation($this->registry, $modelSchema))->setPivotRelation($relation);
453
        $mutations->put($mutation->getName(), $mutation);
454
455
        $mutation = (new DetachPivotMutation($this->registry, $modelSchema))->setPivotRelation($relation);
456
        $mutations->put($mutation->getName(), $mutation);
457
458
        return $mutations;
459
    }
460
461
    /**
462
     * Prepare the schema.
463
     *
464
     * @return array
465
     */
466
    protected function prepareSchema(): array
467
    {
468
        $models = $this->getModels();
469
        $this->registry->addModelSchemas($models);
470
471
        $types = $this->getTypes();
472
        $this->registry->addTypes($types);
473
474
        $queries = $this->getQueries();
475
        $mutations = $this->getMutations();
476
477
        $this->data = [
478
            'queries' => $queries,
479
            'mutations' => $mutations,
480
        ];
481
482
        return $this->data;
483
    }
484
485
    /**
486
     * Convert the bakery schema to a GraphQL schema.
487
     *
488
     * @return GraphQLSchema
489
     * @throws \Exception
490
     */
491
    public function toGraphQLSchema(): GraphQLSchema
492
    {
493
        $this->bindTypeRegistry();
494
495
        if (isset($this->data)) {
496
            $data = $this->data;
497
        } else {
498
            $data = $this->prepareSchema();
499
        }
500
501
        $config = SchemaConfig::create();
502
503
        Utils::invariant(count($data['queries']) > 0, 'There must be query fields defined in the schema.');
504
505
        // Build the query
506
        $query = (new RootQuery($this->registry, $data['queries']))->toType();
507
        $config->setQuery($query);
508
509
        // Build the mutation
510
        $mutation = null;
511
512
        if (count($data['mutations']) > 0) {
513
            $mutation = (new RootMutation($this->registry, $data['mutations']))->toType();
514
            $config->setMutation($mutation);
515
        }
516
517
        // Set directives
518
        $config->setDirectives(array_merge(Directive::getInternalDirectives(), $this->directives()));
519
520
        // Set the type loader
521
        $config->setTypeLoader(function ($name) use ($query, $mutation) {
522
            if ($name === $query->name) {
523
                return $query;
524
            }
525
526
            if ($name === $mutation->name) {
527
                return $mutation;
528
            }
529
530
            return $this->registry->resolve($name);
531
        });
532
533
        return new GraphQLSchema($config);
534
    }
535
536
    /**
537
     * Bind the type registry of the schema to the IoC container of Laravel.
538
     * This lets us resolve the type registry from static calls via the `Type` and `Field` helpers.
539
     * From now on every call to `resolve(TypeRegistry::class)` will resolve in this instance.
540
     */
541
    protected function bindTypeRegistry()
542
    {
543
        app()->instance(TypeRegistry::class, $this->registry);
544
    }
545
546
    /**
547
     * @param $type
548
     * @param array $options
549
     * @return \GraphQL\Type\Definition\ObjectType
550
     */
551
    protected function makeObjectType($type, array $options = []): ObjectType
552
    {
553
        $objectType = null;
554
555
        if ($type instanceof ObjectType) {
556
            $objectType = $type;
557
        } elseif (is_array($type)) {
558
            $objectType = $this->makeObjectTypeFromFields($type, $options);
559
        } else {
560
            $objectType = $this->makeObjectTypeFromClass($type);
561
        }
562
563
        return $objectType;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $objectType could return the type GraphQL\Type\Definition\...Type\Definition\NonNull which is incompatible with the type-hinted return GraphQL\Type\Definition\ObjectType. Consider adding an additional type-check to rule them out.
Loading history...
564
    }
565
566
    /**
567
     * @param $fields
568
     * @param array $options
569
     * @return \GraphQL\Type\Definition\ObjectType
570
     */
571
    protected function makeObjectTypeFromFields($fields, $options = []): ObjectType
572
    {
573
        return new ObjectType(array_merge([
574
            'fields' => $fields,
575
        ], $options));
576
    }
577
578
    /**
579
     * @param \Bakery\Types\Definitions\RootType $class
580
     * @return \GraphQL\Type\Definition\Type
581
     */
582
    protected function makeObjectTypeFromClass(RootType $class): \GraphQL\Type\Definition\Type
583
    {
584
        return $class->toType();
585
    }
586
587
    /**
588
     * Get the models in the given directory.
589
     *
590
     * @param string $directory
591
     * @param string|null $namespace
592
     * @return array
593
     */
594
    public static function modelsIn($directory, $namespace = null)
595
    {
596
        if (is_null($namespace)) {
597
            $namespace = app()->getNamespace();
0 ignored issues
show
introduced by
The method getNamespace() does not exist on Illuminate\Container\Container. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

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

597
            $namespace = app()->/** @scrutinizer ignore-call */ getNamespace();
Loading history...
598
        }
599
600
        $models = [];
601
602
        foreach ((new Finder)->files()->in($directory) as $file) {
603
            $class = $namespace.str_replace(['/', '.php'], ['\\', ''], $file->getRelativePathname());
604
605
            if (is_subclass_of($class, ModelSchema::class)) {
606
                $models[] = $class;
607
            }
608
        }
609
610
        return $models;
611
    }
612
613
    /**
614
     * @return array
615
     * @throws \Exception
616
     */
617
    public function __sleep()
618
    {
619
        $schema = $this->toGraphQLSchema();
620
        $schema->assertValid();
621
622
        return ['registry', 'data'];
623
    }
624
}
625