Completed
Push — master ( 2e4f70...4643e1 )
by Дмитрий
10s
created

ClassDefinition   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 11

Test Coverage

Coverage 79.27%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 310
ccs 88
cts 111
cp 0.7927
rs 8.3673
wmc 45
lcom 3
cbo 11

18 Methods

Rating   Name   Duplication   Size   Complexity  
A addMethod() 0 4 1
A __construct() 0 6 1
A addProperty() 0 6 2
A addConst() 0 4 1
C compile() 0 63 9
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\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 738
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
79
    {
80 738
        $this->name = $name;
81 738
        $this->statement = $statement;
82 738
        $this->type = $type;
83 738
    }
84
85
    /**
86
     * @param ClassMethod $methodDefintion
87
     */
88 20
    public function addMethod(ClassMethod $methodDefintion)
89
    {
90 20
        $this->methods[$methodDefintion->getName()] = $methodDefintion;
91 20
    }
92
93
    /**
94
     * @param Node\Stmt\Property $property
95
     */
96 3
    public function addProperty(Node\Stmt\Property $property)
97
    {
98 3
        foreach ($property->props as $propertyDefinition) {
99 3
            $this->properties[$propertyDefinition->name] = $propertyDefinition;
100 3
        }
101 3
    }
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 19
    public function compile(Context $context)
116
    {
117 19
        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 19
        $context->getEventManager()->fire(
122 19
            Event\StatementBeforeCompile::EVENT_NAME,
123 19
            new Event\StatementBeforeCompile(
124 19
                $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 19
            )
127 19
        );
128
129 19
        $this->compiled = true;
130 19
        $context->setFilepath($this->filepath);
131 19
        $context->setScope($this);
132
133 19
        foreach ($this->properties as $property) {
134 2
            if (!$property->default) {
0 ignored issues
show
Bug introduced by
The property default does not seem to exist in PhpParser\Node\Stmt\Property.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

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