Completed
Pull Request — master (#157)
by Kévin
03:46
created

InterfaceDefinition::getMethod()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 8
nc 4
nop 2
dl 0
loc 16
ccs 0
cts 10
cp 0
crap 30
rs 8.8571
c 1
b 0
f 0
1
<?php
2
/**
3
 * @author Kévin Gomez https://github.com/K-Phoen <[email protected]>
4
 */
5
6
namespace PHPSA\Definition;
7
8
use PHPSA\CompiledExpression;
9
use PHPSA\Context;
10
use PhpParser\Node;
11
use PHPSA\Variable;
12
use PHPSA\Compiler\Event;
13
14
class InterfaceDefinition extends ParentDefinition
15
{
16
    /**
17
     * Class methods
18
     *
19
     * @var ClassMethod[]
20
     */
21
    protected $methods = [];
22
23
    /**
24
     * Class constants
25
     *
26
     * @var Node\Stmt\Const_[]
27
     */
28
    protected $constants = [];
29
30
    /**
31
     * @todo Use Finder
32
     *
33
     * @var string
34
     */
35
    protected $filepath;
36
37
    /**
38
     * @var Node\Stmt\Interface_|null
39
     */
40
    protected $statement;
41
42
    /**
43
     * @var string[]
44
     */
45
    protected $extendsInterfaces = [];
46
47
    /**
48
     * @var InterfaceDefinition[]
49
     */
50
    protected $extendsInterfacesDefinitions;
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $extendsInterfacesDefinitions exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
51
52
    /**
53
     * @param string $name
54
     * @param Node\Stmt\Interface_ $statement
55
     */
56
    public function __construct($name, Node\Stmt\Interface_ $statement = null)
57
    {
58
        $this->name = $name;
59
        $this->statement = $statement;
60
    }
61
62
    /**
63
     * @param ClassMethod $methodDefintion
64
     */
65
    public function addMethod(ClassMethod $methodDefintion)
66
    {
67
        $this->methods[$methodDefintion->getName()] = $methodDefintion;
68
    }
69
70
    /**
71
     * @param Node\Stmt\ClassConst $const
72
     */
73
    public function addConst(Node\Stmt\ClassConst $const)
74
    {
75
        $this->constants[$const->consts[0]->name] = $const;
76
    }
77
78
    /**
79
     * @param Context $context
80
     * @return $this
81
     */
82
    public function compile(Context $context)
83
    {
84
        if ($this->compiled) {
85
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (PHPSA\Definition\InterfaceDefinition) is incompatible with the return type declared by the abstract method PHPSA\Definition\AbstractDefinition::compile of type boolean.

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...
86
        }
87
88
        $context->getEventManager()->fire(
89
            Event\StatementBeforeCompile::EVENT_NAME,
90
            new Event\StatementBeforeCompile($this->statement, $context)
0 ignored issues
show
Bug introduced by
It seems like $this->statement can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
91
        );
92
93
        $this->compiled = true;
94
        $context->setFilepath($this->filepath);
95
        $context->setScope($this);
96
97
        // Compile event for constants
98
        foreach ($this->constants as $const) {
99
            $context->getEventManager()->fire(
100
                Event\StatementBeforeCompile::EVENT_NAME,
101
                new Event\StatementBeforeCompile($const, $context)
102
            );
103
        }
104
105
        // Compile each method
106
        foreach ($this->methods as $method) {
107
            $context->clearSymbols();
108
109
            if (!$method->isStatic()) {
110
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
111
                $thisPtr->incGets();
112
                $context->addVariable($thisPtr);
113
            }
114
115
            $method->compile($context);
116
        }
117
118
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (PHPSA\Definition\InterfaceDefinition) is incompatible with the return type declared by the abstract method PHPSA\Definition\AbstractDefinition::compile of type boolean.

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...
119
    }
120
121
    /**
122
     * @return bool
123
     */
124
    public function isAbstract()
125
    {
126
        return true;
127
    }
128
129
    /**
130
     * @param string $name
131
     * @param boolean|false $inherit
132
     * @return bool
133
     */
134
    public function hasMethod($name, $inherit = false)
135
    {
136
        if (isset($this->methods[$name])) {
137
            return true;
138
        }
139
140
        if (!$inherit) {
141
            return false;
142
        }
143
144
        /** @var InterfaceDefinition $interfacesDefinition */
145
        foreach ($this->extendsInterfacesDefinitions as $interfacesDefinition) {
146
            if ($interfacesDefinition->hasMethod($name, true)) {
147
                return true;
148
            }
149
        }
150
151
        return false;
152
    }
153
154
    /**
155
     * @param string $name
156
     * @param bool $inherit
157
     * @return bool
158
     */
159
    public function hasConst($name, $inherit = false)
160
    {
161
        if (isset($this->constants[$name])) {
162
            return true;
163
        }
164
165
        if ($inherit) {
166
            foreach ($this->extendsInterfacesDefinitions as $interface) {
167
                if ($interface->hasConst($name, true)) {
168
                    return true;
169
                }
170
            }
171
        }
172
173
        return false;
174
    }
175
176
    /**
177
     * @param string $name
178
     * @param boolean|false $inherit
179
     * @return ClassMethod|null
180
     */
181
    public function getMethod($name, $inherit = false)
182
    {
183
        if (isset($this->methods[$name])) {
184
            return $this->methods[$name];
185
        }
186
187
        if ($inherit) {
188
            foreach ($this->extendsInterfacesDefinitions as $interface) {
189
                if ($method = $interface->getMethod($name, true)) {
190
                    return $method;
191
                }
192
            }
193
        }
194
195
        return null;
196
    }
197
198
    /**
199
     * @return string
200
     */
201
    public function getFilepath()
202
    {
203
        return $this->filepath;
204
    }
205
206
    /**
207
     * @param string $filepath
208
     */
209
    public function setFilepath($filepath)
210
    {
211
        $this->filepath = $filepath;
212
    }
213
214
    /**
215
     * @return string[]
216
     */
217
    public function getExtendsInterface()
218
    {
219
        return $this->extendsInterfaces;
220
    }
221
222
    /**
223
     * @param string $interfaceName
224
     */
225
    public function addExtendsInterface($interfaceName)
226
    {
227
        $this->extendsInterfaces[] = $interfaceName;
228
    }
229
230
    /**
231
     * @param InterfaceDefinition $interfaceDefinition
232
     */
233
    public function addExtendsInterfaceDefinition(InterfaceDefinition $interfaceDefinition)
234
    {
235
        $this->extendsInterfacesDefinitions[$interfaceDefinition->getName()] = $interfaceDefinition;
236
    }
237
}
238