Passed
Pull Request — master (#132)
by
unknown
03:48
created

Field::authorizeToStore()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 11
rs 10
cc 3
nc 3
nop 3
1
<?php
2
3
namespace Bakery\Fields;
4
5
use Illuminate\Support\Arr;
6
use Bakery\Support\Arguments;
7
use Bakery\Support\TypeRegistry;
8
use Illuminate\Support\Facades\Gate;
9
use Bakery\Types\Definitions\RootType;
10
use function Bakery\is_callable_tuple;
11
use GraphQL\Type\Definition\ResolveInfo;
12
use Illuminate\Auth\Access\AuthorizationException;
13
14
class Field
15
{
16
    /**
17
     * @var \Bakery\Support\TypeRegistry
18
     */
19
    protected $registry;
20
21
    /**
22
     * @var \GraphQL\Type\Definition\Type
23
     */
24
    protected $type;
25
26
    /**
27
     * @var array
28
     */
29
    protected $args;
30
31
    /**
32
     * @var string
33
     */
34
    protected $name;
35
36
    /**
37
     * @var string
38
     */
39
    protected $description;
40
41
    /**
42
     * @var string
43
     */
44
    protected $accessor;
45
46
    /**
47
     * @var bool
48
     */
49
    protected $list = false;
50
51
    /**
52
     * @var bool
53
     */
54
    protected $nullable = false;
55
56
    /**
57
     * @var bool
58
     */
59
    protected $nullableItems = false;
60
61
    /**
62
     * @var bool
63
     */
64
    protected $fillable = true;
65
66
    /**
67
     * @var array
68
     */
69
    protected $with;
70
71
    /**
72
     * @var bool
73
     */
74
    protected $searchable = false;
75
76
    /**
77
     * @var bool
78
     */
79
    protected $unique = false;
80
81
    /**
82
     * @var mixed
83
     */
84
    protected $storePolicy;
85
86
    /**
87
     * @var mixed
88
     */
89
    protected $viewPolicy;
90
91
    /**
92
     * @var callable
93
     */
94
    protected $resolver;
95
96
    /**
97
     * Construct a new field.
98
     *
99
     * @param \Bakery\Support\TypeRegistry $registry
100
     * @param \Bakery\Types\Definitions\RootType|null $type
101
     */
102
    public function __construct(TypeRegistry $registry, RootType $type = null)
103
    {
104
        $this->registry = $registry;
105
106
        if ($type) {
107
            $this->type = $type;
0 ignored issues
show
Documentation Bug introduced by
It seems like $type of type Bakery\Types\Definitions\RootType is incompatible with the declared type GraphQL\Type\Definition\Type of property $type.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
108
        }
109
    }
110
111
    /**
112
     * @return \Bakery\Support\TypeRegistry
113
     */
114
    public function getRegistry(): TypeRegistry
115
    {
116
        return $this->registry;
117
    }
118
119
    /**
120
     * @param \Bakery\Support\TypeRegistry $registry
121
     * @return $this
122
     */
123
    public function setRegistry(TypeRegistry $registry): self
124
    {
125
        $this->registry = $registry;
126
127
        return $this;
128
    }
129
130
    /**
131
     * Define the type of the field.
132
     * This method can be overridden.
133
     *
134
     * @return \Bakery\Types\Definitions\RootType
135
     */
136
    protected function type(): RootType
137
    {
138
        return $this->type;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->type returns the type GraphQL\Type\Definition\Type which is incompatible with the type-hinted return Bakery\Types\Definitions\RootType.
Loading history...
139
    }
140
141
    /**
142
     * Return the type of the field.
143
     *
144
     * @return \Bakery\Types\Definitions\RootType
145
     */
146
    public function getType(): RootType
147
    {
148
        $type = $this->type();
149
150
        $type->nullable($this->isNullable());
151
        $type->list($this->isList());
152
        $type->nullableItems($this->hasNullableItems());
153
154
        return $type->setRegistry($this->getRegistry());
155
    }
156
157
    /**
158
     * Return if the field represents a relationship.
159
     */
160
    public function isRelationship(): bool
161
    {
162
        return $this instanceof EloquentField || $this instanceof PolymorphicField;
163
    }
164
165
    /**
166
     * Define the name of the field.
167
     *
168
     * This method can be overridden when extending the Field.
169
     *
170
     * @return string
171
     */
172
    protected function name(): string
173
    {
174
        return $this->name;
175
    }
176
177
    /**
178
     * Get the name of the type.
179
     *
180
     * @return string
181
     */
182
    public function getName(): string
183
    {
184
        return $this->name();
185
    }
186
187
    /**
188
     * Set the name of the type.
189
     *
190
     * @param string $name
191
     * @return $this
192
     */
193
    public function setName(string $name): self
194
    {
195
        $this->name = $name;
196
197
        return $this;
198
    }
199
200
    /**
201
     * Get the description of the field.
202
     *
203
     * @return string
204
     */
205
    public function getDescription(): ?string
206
    {
207
        return $this->description;
208
    }
209
210
    /**
211
     * Set the description of the field.
212
     *
213
     * @param string $description
214
     * @return $this
215
     */
216
    public function description(string $description): self
217
    {
218
        $this->description = $description;
219
220
        return $this;
221
    }
222
223
    /**
224
     * Return the name of the database column or method associated with this field.
225
     *
226
     * @return string|null
227
     */
228
    public function getAccessor(): ?string
229
    {
230
        return $this->accessor;
231
    }
232
233
    /**
234
     * Define the name of the database column associated with this field.
235
     *
236
     * @param string $accessor
237
     * @return Field
238
     */
239
    public function accessor(string $accessor): self
240
    {
241
        $this->accessor = $accessor;
242
243
        return $this;
244
    }
245
246
    /**
247
     * Set the args of the field.
248
     *
249
     * @param array $args
250
     * @return $this
251
     */
252
    public function args(array $args): self
253
    {
254
        $this->args = $args;
255
256
        return $this;
257
    }
258
259
    /**
260
     * Get the args of the field.
261
     *
262
     * @return array
263
     */
264
    public function getArgs(): array
265
    {
266
        return $this->args ?? [];
267
    }
268
269
    /**
270
     * @param bool $list
271
     * @return $this
272
     */
273
    public function list(bool $list = true): self
274
    {
275
        $this->list = $list;
276
277
        return $this;
278
    }
279
280
    /**
281
     * @return bool
282
     */
283
    public function isList(): bool
284
    {
285
        return $this->list;
286
    }
287
288
    /**
289
     * Set if the field is nullable.
290
     *
291
     * @param bool $nullable
292
     * @return $this
293
     */
294
    public function nullable(bool $nullable = true): self
295
    {
296
        $this->nullable = $nullable;
297
298
        return $this;
299
    }
300
301
    /**
302
     * Return if the field is nullable.
303
     *
304
     * @return bool
305
     */
306
    public function isNullable(): bool
307
    {
308
        return $this->nullable;
309
    }
310
311
    /**
312
     * Set if the field has nullable items.
313
     *
314
     * @param bool $nullable
315
     * @return \Bakery\Fields\Field
316
     */
317
    public function nullableItems(bool $nullable = true): self
318
    {
319
        $this->nullableItems = $nullable;
320
321
        return $this;
322
    }
323
324
    /**
325
     * Return if the field has nullable items.
326
     *
327
     * @return bool
328
     */
329
    public function hasNullableItems(): bool
330
    {
331
        return $this->nullableItems;
332
    }
333
334
    /**
335
     * Set if the field is fillable.
336
     *
337
     * @param bool $fillable
338
     * @return $this
339
     */
340
    public function fillable(bool $fillable = true): self
341
    {
342
        $this->fillable = $fillable;
343
344
        return $this;
345
    }
346
347
    /**
348
     * Set the field to read only.
349
     *
350
     * @return $this
351
     */
352
    public function readOnly(): self
353
    {
354
        $this->fillable(false);
355
356
        return $this;
357
    }
358
359
    /**
360
     * Return if the field is fillable.
361
     *
362
     * @return bool
363
     */
364
    public function isFillable(): bool
365
    {
366
        return $this->fillable;
367
    }
368
369
    /**
370
     * Set the relations that should be eager loaded.
371
     *
372
     * @param  string[]|string  $relations
373
     * @return Field
374
     */
375
    public function with($relations): self
376
    {
377
        $this->with = Arr::wrap($relations);
378
379
        return $this;
380
    }
381
382
    /**
383
     * Get the relations that should be eager loaded.
384
     */
385
    public function getWith(): ?array
386
    {
387
        return $this->with;
388
    }
389
390
    /**
391
     * Set if the field is searchable.
392
     *
393
     * @param bool $searchable
394
     * @return \Bakery\Fields\Field
395
     */
396
    public function searchable(bool $searchable = true): self
397
    {
398
        $this->searchable = $searchable;
399
400
        return $this;
401
    }
402
403
    /**
404
     * Return if the field is searchable.
405
     *
406
     * @return bool
407
     */
408
    public function isSearchable(): bool
409
    {
410
        return $this->searchable;
411
    }
412
413
    /**
414
     * Set if the field is unique.
415
     *
416
     * @param bool $unique
417
     * @return \Bakery\Fields\Field
418
     */
419
    public function unique(bool $unique = true): self
420
    {
421
        $this->unique = $unique;
422
423
        return $this;
424
    }
425
426
    /**
427
     * Return if the field is unique.
428
     *
429
     * @return bool
430
     */
431
    public function isUnique(): bool
432
    {
433
        return $this->unique;
434
    }
435
436
    /**
437
     * Set the story policy.
438
     *
439
     * @param $policy
440
     * @return \Bakery\Fields\Field
441
     */
442
    public function storePolicy($policy): self
443
    {
444
        $this->storePolicy = $policy;
445
446
        return $this;
447
    }
448
449
    /**
450
     * Set the store policy with a callable.
451
     *
452
     * @param callable $closure
453
     * @return $this
454
     */
455
    public function canStore(callable $closure): self
456
    {
457
        return $this->storePolicy($closure);
458
    }
459
460
    /**
461
     * Set the store policy with a reference to a policy method.
462
     *
463
     * @param string $policy
464
     * @return $this
465
     */
466
    public function canStoreWhen(string $policy): self
467
    {
468
        return $this->storePolicy($policy);
469
    }
470
471
    /**
472
     * Set the view policy.
473
     *
474
     * @param $policy
475
     * @return $this
476
     */
477
    public function viewPolicy($policy): self
478
    {
479
        $this->viewPolicy = $policy;
480
481
        return $this;
482
    }
483
484
    /**
485
     * Set the store policy with a callable.
486
     *
487
     * @param callable $closure
488
     * @return $this
489
     */
490
    public function canSee(callable $closure = null): self
491
    {
492
        return $this->viewPolicy($closure);
493
    }
494
495
    /**
496
     * Set the store policy with a reference to a policy method.
497
     *
498
     * @param string $policy
499
     * @return $this
500
     */
501
    public function canSeeWhen(string $policy): self
502
    {
503
        return $this->viewPolicy($policy);
504
    }
505
506
    /**
507
     * @return mixed
508
     */
509
    public function getViewPolicy()
510
    {
511
        return $this->viewPolicy;
512
    }
513
514
    /**
515
     * Set the resolver.
516
     *
517
     * @param $resolver
518
     * @return $this
519
     */
520
    public function resolve(callable $resolver): self
521
    {
522
        $this->resolver = $resolver;
523
524
        return $this;
525
    }
526
527
    /**
528
     * Get the resolver.
529
     */
530
    public function getResolver(): ?callable
531
    {
532
        return $this->resolver;
533
    }
534
535
    /**
536
     * Resolve the field.
537
     *
538
     * @param $root
539
     * @param array $args
540
     * @param $context
541
     * @param \GraphQL\Type\Definition\ResolveInfo $info
542
     * @return mixed|null
543
     * @throws AuthorizationException
544
     */
545
    public function resolveField($root, array $args, $context, ResolveInfo $info)
546
    {
547
        $accessor = $this->getAccessor() ?: $info->fieldName;
548
        $args = new Arguments($args);
549
550
        if (isset($this->viewPolicy)) {
551
            if (! $this->authorizeToRead($root, $info->fieldName)) {
552
                return null;
553
            }
554
        }
555
556
        if (isset($this->resolver)) {
557
            return call_user_func_array($this->resolver, [$root, $accessor, $args, $context, $info]);
558
        }
559
560
        return self::defaultResolver($root, $accessor, $args, $context, $info);
561
    }
562
563
    /**
564
     * Determine if the current user can read the field of the model or throw an exception if not nullable.
565
     *
566
     * @param mixed $source
567
     * @param string $fieldName
568
     * @return bool
569
     * @throws \Illuminate\Auth\Access\AuthorizationException
570
     */
571
    public function authorizeToRead($source, $fieldName)
572
    {
573
        $result = $this->authorizedToRead($source);
574
        if ($result || $this->nullable) {
575
            return $result;
576
        }
577
578
        throw new AuthorizationException('Cannot read property "'.$fieldName.'" of '.get_class($source));
579
    }
580
581
    /**
582
     * Determine if the current user can read the field of the model.
583
     *
584
     * @param mixed $source
585
     * @return bool
586
     */
587
    public function authorizedToRead($source): bool
588
    {
589
        $policy = $this->viewPolicy;
590
591
        // Check if there is a policy.
592
        if (! $policy) {
593
            return true;
594
        }
595
596
        // Check if the policy method is callable
597
        if (($policy instanceof \Closure || is_callable_tuple($policy)) && $policy($source)) {
598
            return true;
599
        }
600
601
        // Check if there is a policy with this name
602
        if (is_string($policy) && Gate::check($policy, $source)) {
603
            return true;
604
        }
605
606
        return false;
607
    }
608
609
    /**
610
     * Determine if the current user can store the value on the model or throw an exception.
611
     *
612
     * @param mixed $source
613
     * @param mixed $value
614
     * @param string $fieldName
615
     * @return bool
616
     * @throws \Illuminate\Auth\Access\AuthorizationException
617
     */
618
    public function authorizeToStore($source, $value, $fieldName): bool
619
    {
620
        if ($this->authorizedToRead($source)) {
621
            $result = $this->authorizedToStore($source, $value);
622
623
            if ($result) {
624
                return $result;
625
            }
626
        }
627
628
        throw new AuthorizationException('Cannot set property "'.$fieldName.'" of '.get_class($source));
629
    }
630
631
    /**
632
     * Determine if the current user can store the value on the model.
633
     *
634
     * @param mixed $source
635
     * @param mixed $value
636
     * @return bool
637
     */
638
    public function authorizedToStore($source, $value): bool
639
    {
640
        $policy = $this->storePolicy;
641
642
        // Check if there is a policy.
643
        if (! $policy) {
644
            return true;
645
        }
646
647
        // Check if the policy method is a closure.
648
        if (($policy instanceof \Closure || is_callable_tuple($policy)) && $policy($source, $value)) {
649
            return true;
650
        }
651
652
        // Check if there is a policy with this name
653
        if (is_string($policy) && Gate::check($policy, [$source, $value])) {
654
            return true;
655
        }
656
657
        return false;
658
    }
659
660
    /**
661
     * Convert the field to an array.
662
     *
663
     * @return array
664
     */
665
    public function toArray()
666
    {
667
        return [
668
            'type' => $this->getType()->toType(),
669
            'args' => collect($this->getArgs())->map(function (RootType $type) {
670
                return $type->toType();
671
            })->toArray(),
672
            'resolve' => [$this, 'resolveField'],
673
            'description' => $this->getDescription(),
674
        ];
675
    }
676
677
    /**
678
     * The default resolver for resolving the value of the type.
679
     * This gets called when there is no custom resolver defined.
680
     *
681
     * @param string $accessor
682
     * @param Arguments $args
683
     * @param $root
684
     * @param $context
685
     * @param \GraphQL\Type\Definition\ResolveInfo $info
686
     * @return mixed|null
687
     */
688
    public static function defaultResolver($root, string $accessor, Arguments $args, $context, ResolveInfo $info)
689
    {
690
        $property = null;
691
692
        if (Arr::accessible($root)) {
693
            $property = $root[$accessor];
694
        } elseif (is_object($root)) {
695
            $property = $root->{$accessor};
696
        }
697
698
        return $property instanceof \Closure ? $property($args, $root, $context, $info) : $property;
699
    }
700
701
    /**
702
     * Invoked when the object is being serialized.
703
     * Returns the field that should be serialized.
704
     *
705
     * @return array
706
     */
707
    public function __sleep()
708
    {
709
        return ['registry'];
710
    }
711
}
712