Completed
Push — master ( 96b2f8...da2c9c )
by Kirill
05:58
created

Reflection::addType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 7
cp 0
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 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\Reflection;
11
12
use Railt\Io\Exception\ExternalFileException;
13
use Railt\Reflection\Common\Serializable;
14
use Railt\Reflection\Contracts\Definition;
15
use Railt\Reflection\Contracts\Definition\TypeDefinition;
16
use Railt\Reflection\Contracts\Document as DocumentInterface;
17
use Railt\Reflection\Contracts\Definition\SchemaDefinition as SchemaInterface;
18
use Railt\Reflection\Contracts\Reflection as ReflectionInterface;
19
use Railt\Reflection\Exception\TypeConflictException;
20
use Railt\Reflection\Exception\TypeNotFoundException;
21
22
/**
23
 * Class Reflection
24
 */
25
class Reflection implements ReflectionInterface
26
{
27
    use Serializable;
28
29
    /**
30
     * @var array|TypeDefinition[]
31
     */
32
    protected $types = [];
33
34
    /**
35
     * @var array|DocumentInterface[]
36
     */
37
    protected $documents = [];
38
39
    /**
40
     * @var array|string[]
41
     */
42
    protected $schema = [];
43
44
    /**
45
     * @return iterable|DocumentInterface[]
46
     */
47
    public function getDocuments(): iterable
48
    {
49
        return \array_values($this->documents);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \array_values($this->documents); (array) is incompatible with the return type declared by the interface Railt\Reflection\Contrac...eflection::getDocuments of type Railt\Reflection\Contrac...on\Contracts\Document[].

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...
50
    }
51
52
    /**
53
     * @param string|null $name
54
     * @return null|SchemaInterface|TypeDefinition
55
     */
56
    public function getSchema(string $name = null): ?SchemaInterface
57
    {
58
        $name = $name ?? SchemaInterface::DEFAULT_SCHEMA_NAME;
59
60
        if (! \in_array($name, $this->schema, true)) {
61
            return null;
62
        }
63
64
        return $this->getTypeDefinition($name);
65
    }
66
67
    /**
68
     * @param string $name
69
     * @return TypeDefinition
70
     */
71
    public function getTypeDefinition(string $name): ?TypeDefinition
72
    {
73
        return $this->types[$name] ?? null;
74
    }
75
76
    /**
77
     * @param string $type
78
     * @param Definition $from
79
     * @return TypeDefinition
80
     * @throws ExternalFileException
81
     */
82
    public function fetch(string $type, Definition $from): TypeDefinition
83
    {
84
        if (($result = $this->getTypeDefinition($type)) !== null) {
85
            return $result;
86
        }
87
88
        throw $this->typeNotFound($type)
89
            ->throwsIn($from->getFile(), $from->getLine(), $from->getColumn());
90
    }
91
92
    /**
93
     * @param string $name
94
     * @return TypeNotFoundException
95
     */
96
    private function typeNotFound(string $name): TypeNotFoundException
97
    {
98
        $error = \sprintf('Type %s not found or could not be loaded', $name);
99
100
        return new TypeNotFoundException($error);
101
    }
102
103
    /**
104
     * @param TypeDefinition $type
105
     * @throws ExternalFileException|TypeConflictException
106
     */
107
    public function addType(TypeDefinition $type): void
108
    {
109
        $this->checkTypeExistence($type);
110
111
        $this->preCacheSchema($type);
112
        $this->preCacheDocument($type);
113
114
        $this->types[$type->getName()] = $type;
115
    }
116
117
    /**
118
     * @param TypeDefinition $type
119
     * @throws ExternalFileException
120
     * @throws TypeConflictException
121
     */
122
    private function checkTypeExistence(TypeDefinition $type): void
123
    {
124
        if (\array_key_exists($type->getName(), $this->types)) {
125
            throw $this->typeRedefinition($type);
126
        }
127
    }
128
129
    /**
130
     * @param TypeDefinition $type
131
     * @return TypeConflictException|ExternalFileException
132
     */
133
    private function typeRedefinition(TypeDefinition $type): TypeConflictException
134
    {
135
        $def = $this->types[$type->getName()];
136
137
        $error = 'Could not define type %s, because type with same name already has been defined in %s:%d:%d';
138
        $error = \sprintf($error, $type, $def->getFile()->getPathname(), $def->getLine(), $def->getColumn());
139
140
        return (new TypeConflictException($error))->throwsIn($type->getFile(), $type->getLine(), $type->getColumn());
141
    }
142
143
    /**
144
     * @param TypeDefinition $type
145
     */
146
    private function preCacheSchema(TypeDefinition $type): void
147
    {
148
        if ($type instanceof SchemaInterface) {
149
            $this->schema[] = $type->getName();
150
        }
151
    }
152
153
    /**
154
     * @param TypeDefinition $type
155
     */
156
    private function preCacheDocument(TypeDefinition $type): void
157
    {
158
        $document = $type->getDocument();
159
160
        if (! \array_key_exists($document->getName(), $this->documents)) {
161
            $this->documents[$document->getName()] = $document;
162
        }
163
    }
164
165
    /**
166
     * @return iterable|TypeDefinition[]
167
     */
168
    public function getTypeDefinitions(): iterable
169
    {
170
        return \array_values($this->types);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \array_values($this->types); (array) is incompatible with the return type declared by the interface Railt\Reflection\Contrac...ons::getTypeDefinitions of type Railt\Reflection\Contrac...nition\TypeDefinition[].

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...
171
    }
172
173
    /**
174
     * @param string $name
175
     * @return bool
176
     */
177
    public function hasTypeDefinition(string $name): bool
178
    {
179
        return \array_key_exists($name, $this->types);
180
    }
181
182
    /**
183
     * @return int
184
     */
185
    public function getNumberOfTypeDefinitions(): int
186
    {
187
        return \count($this->types);
188
    }
189
}
190