Completed
Pull Request — master (#134)
by Enrico
03:46
created

ClassDefinition   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 11

Test Coverage

Coverage 82.03%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 343
ccs 105
cts 128
cp 0.8203
rs 8.439
wmc 47
lcom 3
cbo 11

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A addMethod() 0 4 1
A addProperty() 0 8 2
A addConst() 0 4 1
C compile() 0 87 11
B hasMethod() 0 13 7
A hasConst() 0 8 4
A getMethod() 0 12 4
A hasProperty() 0 8 4
A getProperty() 0 14 4
A getFilepath() 0 4 1
A setFilepath() 0 4 1
A isAbstract() 0 4 1
A setExtendsClass() 0 4 1
A getExtendsClassDefinition() 0 4 1
A setExtendsClassDefinition() 0 4 1
A addInterface() 0 4 1
A getExtendsClass() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ClassDefinition often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClassDefinition, and based on these observations, apply Extract Interface, too.

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\PropertyProperty[]
36
     */
37
    protected $properties = array();
38
39
    /**
40
     * Property Statements
41
     *
42
     * @var Node\Stmt\Property[]
43
     */
44
    protected $propertyStatements = array();
45
46
    /**
47
     * Class constants
48
     *
49
     * @var Node\Stmt\Const_[]
50
     */
51
    protected $constants = array();
52
53
    /**
54
     * @todo Use Finder
55
     *
56
     * @var string
57
     */
58
    protected $filepath;
59
60
    /**
61
     * @var Node\Stmt\Class_|null
62
     */
63
    protected $statement;
64
65
    /**
66
     * @var string|null
67
     */
68
    protected $extendsClass;
69
70
    /**
71
     * @var ClassDefinition|null
72
     */
73
    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...
74
75
    /**
76
     * @var array
77
     */
78
    protected $interfaces = array();
79
80
    /**
81
     * @param string $name
82
     * @param Node\Stmt\Class_ $statement
83
     * @param integer $type
84
     */
85 776
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
86
    {
87 776
        $this->name = $name;
88 776
        $this->statement = $statement;
89 776
        $this->type = $type;
90 776
    }
91
92
    /**
93
     * @param ClassMethod $methodDefintion
94
     */
95 25
    public function addMethod(ClassMethod $methodDefintion)
96
    {
97 25
        $this->methods[$methodDefintion->getName()] = $methodDefintion;
98 25
    }
99
100
    /**
101
     * @param Node\Stmt\Property $property
102
     */
103 4
    public function addProperty(Node\Stmt\Property $property)
104
    {
105 4
        foreach ($property->props as $propertyDefinition) {
106 4
            $this->properties[$propertyDefinition->name] = $propertyDefinition;
107 4
        }
108
        
109 4
        $this->propertyStatements[] = $property;
110 4
    }
111
112
    /**
113
     * @param Node\Stmt\ClassConst $const
114
     */
115 3
    public function addConst(Node\Stmt\ClassConst $const)
116
    {
117 3
        $this->constants[$const->consts[0]->name] = $const;
118 3
    }
119
120
    /**
121
     * @param Context $context
122
     * @return $this
123
     */
124 25
    public function compile(Context $context)
0 ignored issues
show
Complexity introduced by
This operation has 216 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
125
    {
126 25
        if ($this->compiled) {
127
            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...
128
        }
129
130 25
        $context->getEventManager()->fire(
131 25
            Event\StatementBeforeCompile::EVENT_NAME,
132 25
            new Event\StatementBeforeCompile(
133 25
                $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...
134
                $context
135 25
            )
136 25
        );
137
138 25
        $this->compiled = true;
139 25
        $context->setFilepath($this->filepath);
140 25
        $context->setScope($this);
141
142
        // Compile event for properties
143 25
        foreach ($this->properties as $property) {
144 3
            if (!$property->default) {
145 1
                continue;
146
            }
147
148
            // fire expression event for property default
149 2
            $context->getEventManager()->fire(
150 2
                Event\ExpressionBeforeCompile::EVENT_NAME,
151 2
                new Event\ExpressionBeforeCompile(
152 2
                    $property->default,
153
                    $context
154 2
                )
155 2
            );
156 25
        }
157
158
        // Compile event for constants
159 25
        foreach ($this->constants as $const) {
160 1
            $context->getEventManager()->fire(
161 1
                Event\StatementBeforeCompile::EVENT_NAME,
162 1
                new Event\StatementBeforeCompile(
163 1
                    $const,
164
                    $context
165 1
                )
166 1
            );
167 25
        }
168
169
        // Compiler event for property statements
170 25
        foreach ($this->propertyStatements as $prop) {
171 3
            $context->getEventManager()->fire(
172 3
                Event\StatementBeforeCompile::EVENT_NAME,
173 3
                new Event\StatementBeforeCompile(
174 3
                    $prop,
175
                    $context
176 3
                )
177 3
            );
178 25
        }
179
180
        // Compile each method
181 25
        foreach ($this->methods as $method) {
182 24
            $context->clearSymbols();
183
184 24
            if (!$method->isStatic()) {
185 24
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
186 24
                $thisPtr->incGets();
187 24
                $context->addVariable($thisPtr);
188 24
            }
189
190 24
            $method->compile($context);
191
192 24
            $symbols = $context->getSymbols();
193 24
            if (count($symbols) > 0) {
194 24
                foreach ($symbols as $name => $variable) {
195 24
                    if ($variable->isUnused()) {
196 4
                        $context->warning(
197 4
                            'unused-' . $variable->getSymbolType(),
198 4
                            sprintf(
199 4
                                'Unused ' . $variable->getSymbolType() . ' $%s in method %s()',
200 4
                                $variable->getName(),
201 4
                                $method->getName()
202 4
                            )
203 4
                        );
204 4
                    }
205 24
                }
206 24
            }
207 25
        }
208
209 25
        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...
210
    }
211
212
    /**
213
     * @param string $name
214
     * @param boolean|false $inherit
215
     * @return bool
216
     */
217 1
    public function hasMethod($name, $inherit = false)
218
    {
219 1
        if (isset($this->methods[$name])) {
220 1
            return true;
221
        }
222
223 1
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasMethod($name, $inherit)) {
224
            $method = $this->extendsClassDefinition->getMethod($name, $inherit);
225
            return $method && ($method->isPublic() || $method->isProtected());
226
        }
227
228 1
        return false;
229
    }
230
231
    /**
232
     * @param string $name
233
     * @param bool $inherit
234
     * @return bool
235
     */
236 2
    public function hasConst($name, $inherit = false)
237
    {
238 2
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasConst($name, $inherit)) {
239 1
            return true;
240
        }
241
242 2
        return isset($this->constants[$name]);
243
    }
244
245
    /**
246
     * @param $name
247
     * @param boolean|false $inherit
248
     * @return ClassMethod|null
249
     */
250 1
    public function getMethod($name, $inherit = false)
251
    {
252 1
        if (isset($this->methods[$name])) {
253 1
            return $this->methods[$name];
254
        }
255
256
        if ($inherit && $this->extendsClassDefinition) {
257
            return $this->extendsClassDefinition->getMethod($name, $inherit);
258
        }
259
260
        return null;
261
    }
262
263
    /**
264
     * @param $name
265
     * @param bool $inherit
266
     * @return bool
267
     */
268 3
    public function hasProperty($name, $inherit = false)
269
    {
270 3
        if (isset($this->properties[$name])) {
271 1
            return isset($this->properties[$name]);
272
        }
273
274 3
        return $inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasProperty($name, true);
275
    }
276
277
    /**
278
     * @param string $name
279
     * @param bool $inherit
280
     * @return Node\Stmt\Property
281
     */
282
    public function getProperty($name, $inherit = false)
283
    {
284
        assert($this->hasProperty($name, $inherit));
285
286
        if (isset($this->properties[$name])) {
287
            return $this->properties[$name];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->properties[$name]; (PhpParser\Node\Stmt\PropertyProperty) is incompatible with the return type documented by PHPSA\Definition\ClassDefinition::getProperty of type PhpParser\Node\Stmt\Property.

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...
288
        }
289
290
        if ($inherit && $this->extendsClassDefinition) {
291
            return $this->extendsClassDefinition->getProperty($name, true);
292
        }
293
294
        return null;
295
    }
296
297
    /**
298
     * @return string
299
     */
300 25
    public function getFilepath()
301
    {
302 25
        return $this->filepath;
303
    }
304
305
    /**
306
     * @param string $filepath
307
     */
308 25
    public function setFilepath($filepath)
309
    {
310 25
        $this->filepath = $filepath;
311 25
    }
312
313
    /**
314
     * @return bool
315
     */
316
    public function isAbstract()
317
    {
318
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_ABSTRACT);
319
    }
320
321
    /**
322
     * @param null|string $extendsClass
323
     */
324
    public function setExtendsClass($extendsClass)
325
    {
326
        $this->extendsClass = $extendsClass;
327
    }
328
329
    /**
330
     * @return null|ClassDefinition
331
     */
332
    public function getExtendsClassDefinition()
333
    {
334
        return $this->extendsClassDefinition;
335
    }
336
337
    /**
338
     * @param ClassDefinition $extendsClassDefinition
339
     */
340 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...
341
    {
342 1
        $this->extendsClassDefinition = $extendsClassDefinition;
343 1
    }
344
345
    /**
346
     * @param array $interface
347
     */
348
    public function addInterface($interface)
349
    {
350
        $this->interfaces[] = $interface;
351
    }
352
353
    /**
354
     * @return null|string
355
     */
356 25
    public function getExtendsClass()
357
    {
358 25
        return $this->extendsClass;
359
    }
360
}
361