Completed
Push — master ( a77aaf...cf2c14 )
by Дмитрий
06:02 queued 03:00
created

ClassDefinition::compile()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 48
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 7.0009

Importance

Changes 0
Metric Value
cc 7
eloc 29
nc 10
nop 1
dl 0
loc 48
ccs 36
cts 37
cp 0.973
crap 7.0009
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Patsura Dmitry https://github.com/ovr <[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
/**
15
 * Class ClassDefinition
16
 * @package PHPSA\Definition
17
 */
18
class ClassDefinition extends ParentDefinition
19
{
20
    /**
21
     * @var int
22
     */
23
    protected $type;
24
25
    /**
26
     * Class methods
27
     *
28
     * @var ClassMethod[]
29
     */
30
    protected $methods = array();
31
32
    /**
33
     * Class properties
34
     *
35
     * @var Node\Stmt\Property[]
36
     */
37
    protected $properties = array();
38
39
    /**
40
     * Class constants
41
     *
42
     * @var Node\Stmt\Const_[]
43
     */
44
    protected $constants = array();
45
46
    /**
47
     * @todo Use Finder
48
     *
49
     * @var string
50
     */
51
    protected $filepath;
52
53
    /**
54
     * @var Node\Stmt\Class_|null
55
     */
56
    protected $statement;
57
58
    /**
59
     * @var string|null
60
     */
61
    protected $extendsClass;
62
63
    /**
64
     * @var ClassDefinition|null
65
     */
66
    protected $extendsClassDefinition;
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $extendsClassDefinition 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...
67
68
    /**
69
     * @var array
70
     */
71
    protected $interfaces = array();
72
73
    /**
74
     * @param string $name
75
     * @param Node\Stmt\Class_ $statement
76
     * @param integer $type
77
     */
78 396
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
79
    {
80 396
        $this->name = $name;
81 396
        $this->statement = $statement;
82 396
        $this->type = $type;
83 396
    }
84
85
    /**
86
     * @param ClassMethod $methodDefintion
87
     */
88 18
    public function addMethod(ClassMethod $methodDefintion)
89
    {
90 18
        $this->methods[$methodDefintion->getName()] = $methodDefintion;
91 18
    }
92
93
    /**
94
     * @param Node\Stmt\Property $property
95
     */
96 2
    public function addProperty(Node\Stmt\Property $property)
97
    {
98 2
        foreach ($property->props as $propertyDefinition) {
99 2
            $this->properties[$propertyDefinition->name] = $propertyDefinition;
100 2
        }
101 2
    }
102
103
    /**
104
     * @param Node\Stmt\ClassConst $const
105
     */
106 2
    public function addConst(Node\Stmt\ClassConst $const)
107
    {
108 2
        $this->constants[$const->consts[0]->name] = $const;
109 2
    }
110
111
    /**
112
     * @param Context $context
113
     * @return $this
114
     */
115 17
    public function compile(Context $context)
116
    {
117 17
        if ($this->compiled) {
118
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type documented by PHPSA\Definition\ClassDefinition::compile of type PHPSA\Definition\ClassDefinition.

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 17
        $context->getEventManager()->fire(
122 17
            Event\StatementBeforeCompile::EVENT_NAME,
123 17
            new Event\StatementBeforeCompile(
124 17
                $this->statement,
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...
125
                $context
126 17
            )
127 17
        );
128
129 17
        $this->compiled = true;
130 17
        $context->setFilepath($this->filepath);
131 17
        $context->setScope($this);
132
133 17
        foreach ($this->methods as $method) {
134 17
            $context->clearSymbols();
135
136 17
            if (!$method->isStatic()) {
137 17
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
138 17
                $thisPtr->incGets();
139 17
                $context->addVariable($thisPtr);
140 17
            }
141
142 17
            $method->compile($context);
143
144 17
            $symbols = $context->getSymbols();
145 17
            if (count($symbols) > 0) {
146 17
                foreach ($symbols as $name => $variable) {
147 17
                    if ($variable->isUnused()) {
148 2
                        $context->warning(
149 2
                            'unused-' . $variable->getSymbolType(),
150 2
                            sprintf(
151 2
                                'Unused ' . $variable->getSymbolType() . ' $%s in method %s()',
152 2
                                $variable->getName(),
153 2
                                $method->getName()
154 2
                            )
155 2
                        );
156 2
                    }
157 17
                }
158 17
            }
159 17
        }
160
161 17
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (PHPSA\Definition\ClassDefinition) 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...
162
    }
163
164
    /**
165
     * @param string $name
166
     * @param boolean|false $inherit
167
     * @return bool
168
     */
169 1
    public function hasMethod($name, $inherit = false)
170
    {
171 1
        if (isset($this->methods[$name])) {
172 1
            return true;
173
        }
174
175 1
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasMethod($name, $inherit)) {
176
            $method = $this->extendsClassDefinition->getMethod($name, $inherit);
177
            return $method && ($method->isPublic() || $method->isProtected());
178
        }
179
180 1
        return false;
181
    }
182
183
    /**
184
     * @param string $name
185
     * @param bool $inherit
186
     * @return bool
187
     */
188 2
    public function hasConst($name, $inherit = false)
189
    {
190 2
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasConst($name, $inherit)) {
191 1
            return true;
192
        }
193
194 2
        return isset($this->constants[$name]);
195
    }
196
197
    /**
198
     * @param $name
199
     * @param boolean|false $inherit
200
     * @return ClassMethod|null
201
     */
202 1
    public function getMethod($name, $inherit = false)
203
    {
204 1
        if (isset($this->methods[$name])) {
205 1
            return $this->methods[$name];
206
        }
207
208
        if ($inherit && $this->extendsClassDefinition) {
209
            return $this->extendsClassDefinition->getMethod($name, $inherit);
210
        }
211
212
        return null;
213
    }
214
215
    /**
216
     * @param $name
217
     * @param bool $inherit
218
     * @return bool
219
     */
220 1
    public function hasProperty($name, $inherit = false)
221
    {
222 1
        if (isset($this->properties[$name])) {
223 1
            return isset($this->properties[$name]);
224
        }
225
226 1
        return $inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasProperty($name, true);
227
    }
228
229
    /**
230
     * @param string $name
231
     * @param bool $inherit
232
     * @return Node\Stmt\Property
233
     */
234
    public function getProperty($name, $inherit = false)
235
    {
236
        assert($this->hasProperty($name, $inherit));
237
238
        if (isset($this->properties[$name])) {
239
            return $this->properties[$name];
240
        }
241
242
        if ($inherit && $this->extendsClassDefinition) {
243
            return $this->extendsClassDefinition->getProperty($name, true);
244
        }
245
246
        return null;
247
    }
248
249
    /**
250
     * @return string
251
     */
252 17
    public function getFilepath()
253
    {
254 17
        return $this->filepath;
255
    }
256
257
    /**
258
     * @param string $filepath
259
     */
260 17
    public function setFilepath($filepath)
261
    {
262 17
        $this->filepath = $filepath;
263 17
    }
264
265
    /**
266
     * @return bool
267
     */
268
    public function isAbstract()
269
    {
270
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_ABSTRACT);
271
    }
272
273
    /**
274
     * @param null|string $extendsClass
275
     */
276
    public function setExtendsClass($extendsClass)
277
    {
278
        $this->extendsClass = $extendsClass;
279
    }
280
281
    /**
282
     * @return null|ClassDefinition
283
     */
284
    public function getExtendsClassDefinition()
285
    {
286
        return $this->extendsClassDefinition;
287
    }
288
289
    /**
290
     * @param ClassDefinition $extendsClassDefinition
291
     */
292 1
    public function setExtendsClassDefinition(ClassDefinition $extendsClassDefinition)
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $extendsClassDefinition 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...
293
    {
294 1
        $this->extendsClassDefinition = $extendsClassDefinition;
295 1
    }
296
297
    /**
298
     * @param array $interface
299
     */
300
    public function addInterface($interface)
301
    {
302
        $this->interfaces[] = $interface;
303
    }
304
305
    /**
306
     * @return null|string
307
     */
308 17
    public function getExtendsClass()
309
    {
310 17
        return $this->extendsClass;
311
    }
312
}
313