Completed
Push — master ( 5ac91e...39483e )
by Kirill
38:30
created

BaseDirective   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 172
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 3

Test Coverage

Coverage 79.49%

Importance

Changes 0
Metric Value
wmc 21
lcom 2
cbo 3
dl 0
loc 172
ccs 31
cts 39
cp 0.7949
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getLocations() 0 4 1
A getNumberOfLocations() 0 4 1
C isAllowedFor() 0 27 7
A hasLocation() 0 6 1
A allowedForArgument() 0 8 2
A isAllowedForQueries() 0 10 3
A isAllowedForSchemaDefinitions() 0 10 3
A __sleep() 0 7 1
A getAllAllowedLocations() 0 10 2
1
<?php
2
/**
3
 * This file is part of Railt package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace Railt\SDL\Base\Definitions;
11
12
use Railt\SDL\Base\Dependent\Argument\BaseArgumentsContainer;
13
use Railt\SDL\Contracts\Definitions;
14
use Railt\SDL\Contracts\Definitions\Definition;
15
use Railt\SDL\Contracts\Definitions\Directive\Location;
16
use Railt\SDL\Contracts\Definitions\DirectiveDefinition;
17
use Railt\SDL\Contracts\Dependent;
18
use Railt\SDL\Contracts\Document;
19
use Railt\SDL\Contracts\Type;
20
21
/**
22
 * Class BaseDirectiveDefinition
23
 */
24
abstract class BaseDirective extends BaseTypeDefinition implements DirectiveDefinition
25
{
26
    use BaseArgumentsContainer;
27
28
    /**
29
     * Type name
30
     */
31
    protected const TYPE_NAME = Type::DIRECTIVE;
32
33
    /**
34
     * Directive location type name
35
     */
36
    protected const LOCATION_TYPE_NAME = 'DirectiveLocation';
37
38
    /**
39
     * Mappings location to allowed type
40
     */
41
    protected const LOCATION_TARGET_MAPPINGS = [
42
        Location::TARGET_SCHEMA       => Definitions\SchemaDefinition::class,
43
        Location::TARGET_INPUT_OBJECT => Definitions\InputDefinition::class,
44
45
        Location::TARGET_OBJECT     => Definitions\ObjectDefinition::class,
46
        Location::TARGET_INTERFACE  => Definitions\InterfaceDefinition::class,
47
        Location::TARGET_UNION      => Definitions\UnionDefinition::class,
48
        Location::TARGET_SCALAR     => Definitions\ScalarDefinition::class,
49
        Location::TARGET_ENUM       => Definitions\EnumDefinition::class,
50
        Location::TARGET_ENUM_VALUE => Definitions\Enum\ValueDefinition::class,
51
52
        Location::TARGET_FIELD_DEFINITION    => Dependent\FieldDefinition::class,
53
        Location::TARGET_ARGUMENT_DEFINITION => Dependent\ArgumentDefinition::class,
54
55
        Location::TARGET_INPUT_FIELD_DEFINITION => Dependent\ArgumentDefinition::class,
56
57
        // Custom
58
        Location::TARGET_DOCUMENT => Document::class,
59
    ];
60
61
    /**
62
     * @var array|string[]
63
     */
64
    protected $locations = [];
65
66
    /**
67
     * @var array|null
68
     */
69
    private $allLocations;
70
71
    /**
72
     * @return iterable|string[]
73
     */
74 2448
    public function getLocations(): iterable
75
    {
76 2448
        return \array_values($this->locations);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \array_values($this->locations); (array) is incompatible with the return type declared by the interface Railt\SDL\Contracts\Defi...efinition::getLocations of type Railt\SDL\Contracts\Definitions\iterable|string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
77
    }
78
79
    /**
80
     * @return int
81
     */
82
    public function getNumberOfLocations(): int
83
    {
84
        return \count($this->locations);
85
    }
86
87
    /**
88
     * @param null|Definition $type
89
     * @return bool
90
     */
91 894
    public function isAllowedFor(?Definition $type): bool
92
    {
93 894
        if ($type === null) {
94 12
            return false;
95
        }
96
97 894
        foreach (self::LOCATION_TARGET_MAPPINGS as $out => $allowedType) {
98 894
            if ($type instanceof $allowedType && $this->hasLocation($out)) {
99
                //
100
                // If type is ArgumentType but:
101
                //
102
                // 1) Position: InputType > ArgumentType
103
                //      + Directive Location: ARGUMENT_DEFINITION
104
                //
105
                // 2) Position: FieldType > ArgumentType
106
                //      + Directive Location: INPUT_FIELD_DEFINITION
107
                //
108 114
                if ($type instanceof Dependent\ArgumentDefinition && ! $this->allowedForArgument($type)) {
109 12
                    continue;
110
                }
111
112 894
                return true;
113
            }
114
        }
115
116 804
        return false;
117
    }
118
119
    /**
120
     * @param string $name
121
     * @return bool
122
     */
123 894
    public function hasLocation(string $name): bool
124
    {
125 894
        $name = \strtoupper($name);
126
127 894
        return \in_array($name, $this->locations, true);
128
    }
129
130
    /**
131
     * @param Dependent\ArgumentDefinition $type
132
     * @return bool
133
     */
134 36
    private function allowedForArgument(Dependent\ArgumentDefinition $type): bool
135
    {
136 36
        $location = $type->getParent() instanceof Definitions\InputDefinition
137 12
            ? Location::TARGET_INPUT_FIELD_DEFINITION
138 36
            : Location::TARGET_ARGUMENT_DEFINITION;
139
140 36
        return $this->hasLocation($location);
141
    }
142
143
    /**
144
     * @return bool
145
     */
146 12
    public function isAllowedForQueries(): bool
147
    {
148 12
        foreach ($this->locations as $location) {
149 12
            if (\in_array($location, Location::TARGET_GRAPHQL_QUERY, true)) {
150 12
                return true;
151
            }
152
        }
153
154 6
        return false;
155
    }
156
157
    /**
158
     * @return bool
159
     */
160 12
    public function isAllowedForSchemaDefinitions(): bool
161
    {
162 12
        foreach ($this->locations as $location) {
163 12
            if (\in_array($location, Location::TARGET_GRAPHQL_SDL, true)) {
164 12
                return true;
165
            }
166
        }
167
168
        return false;
169
    }
170
171
    /**
172
     * @return array
173
     */
174 1236
    public function __sleep(): array
175
    {
176 1236
        return \array_merge(parent::__sleep(), [
177 1236
            'locations',
178
            'arguments',
179
        ]);
180
    }
181
182
    /**
183
     * @return array
184
     */
185
    protected function getAllAllowedLocations(): array
186
    {
187
        if ($this->allLocations === null) {
188
            $locations = new \ReflectionClass(Location::class);
189
190
            $this->allLocations = \array_filter(\array_values($locations->getConstants()), '\\is_string');
191
        }
192
193
        return $this->allLocations;
194
    }
195
}
196