Passed
Push — master ( 604248...b10a5e )
by Valentin
02:42
created

ProxyPrinterTest   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 421
Duplicated Lines 27.55 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 11
dl 116
loc 421
rs 9.1199
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A testDeclaration() 0 27 1
A testSameNamespace() 0 17 1
A testDifferentNamespace() 17 17 1
A testUseStmtsInSameNamespace() 18 18 1
A testUseStmtsInDifferentNamespace() 19 19 1
A fetchUseStatements() 0 15 3
A fetchExternalDependencies() 0 16 4
A testTraits() 7 7 1
A testConstants() 7 7 1
A testProperties() 0 31 3
A testHasConstructor() 0 32 1
A testNotContainParentConstructor() 15 15 1
A testContainParentConstructor() 15 15 1
A testPromiseMethods() 18 18 2
A testInheritedProperties() 0 34 5
B testInheritedMethods() 0 52 9
A getDeclaration() 0 4 1
A extractor() 0 6 1
A makeProxyObject() 0 9 1
A make() 0 4 1
A proxyCreator() 0 7 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ProxyPrinterTest 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 ProxyPrinterTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace Cycle\ORM\Promise\Tests;
5
6
use Cycle\ORM\ORMInterface;
7
use Cycle\ORM\Promise\Declaration\Declaration;
8
use Cycle\ORM\Promise\Declaration\Extractor;
9
use Cycle\ORM\Promise\Declaration\Structure;
10
use Cycle\ORM\Promise\PromiseInterface;
11
use Cycle\ORM\Promise\PromiseResolver;
12
use Cycle\ORM\Promise\ProxyPrinter;
13
use Cycle\ORM\Promise\Tests\Fixtures;
14
use Cycle\ORM\Promise\Utils;
15
use PhpParser\PrettyPrinter\Standard;
16
use PhpParser\PrettyPrinterAbstract;
17
use PHPUnit\Framework\TestCase;
18
use Spiral\Core\Container;
19
20
class ProxyPrinterTest extends TestCase
21
{
22
    public function testDeclaration(): void
23
    {
24
        $class = Fixtures\Entity::class;
25
        $as = 'EntityProxy' . __LINE__;
26
27
        $declaration = new Declaration(new \ReflectionClass($class), $as);
28
        $output = $this->make($declaration);
29
        $output = ltrim($output, '<?php');
30
31
        $this->assertFalse(class_exists($declaration->class->getFullName()));
32
33
        eval($output);
34
35
        $this->assertStringNotContainsString('abstract', $output);
36
        $this->assertStringContainsString(sprintf(
37
            'class %s extends %s implements %s',
38
            $as,
39
            Utils::shortName($class),
40
            Utils::shortName(PromiseInterface::class)
41
        ), $output);
42
43
        $proxy = $this->makeProxyObject($class, $declaration->class->getFullName());
44
45
        $this->assertInstanceOf($declaration->class->getFullName(), $proxy);
46
        $this->assertInstanceOf($class, $proxy);
47
        $this->assertInstanceOf(PromiseInterface::class, $proxy);
48
    }
49
50
    /**
51
     * @throws \ReflectionException
52
     */
53
    public function testSameNamespace(): void
54
    {
55
        $class = Fixtures\Entity::class;
56
        $as = 'EntityProxy' . __LINE__;
57
58
        $declaration = new Declaration(new \ReflectionClass($class), $as);
59
        $output = $this->make($declaration);
60
        $output = ltrim($output, '<?php');
61
62
        $this->assertFalse(class_exists($declaration->class->getFullName()));
63
64
        eval($output);
65
66
        $origReflection = new \ReflectionClass($class);
67
        $proxyReflection = new \ReflectionClass($declaration->class->getFullName());
68
        $this->assertSame($origReflection->getNamespaceName(), $proxyReflection->getNamespaceName());
69
    }
70
71
    /**
72
     * @throws \ReflectionException
73
     */
74 View Code Duplication
    public function testDifferentNamespace(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
75
    {
76
        $class = Fixtures\Entity::class;
77
        $as = "\EntityProxy" . __LINE__;
78
79
        $declaration = new Declaration(new \ReflectionClass($class), $as);
80
        $output = $this->make($declaration);
81
        $output = ltrim($output, '<?php');
82
83
        $this->assertFalse(class_exists($declaration->class->getFullName()));
84
85
        eval($output);
86
87
        $proxyReflection = new \ReflectionClass($declaration->class->getFullName());
88
        $this->assertSame('', (string)$proxyReflection->getNamespaceName());
89
        $this->assertStringNotContainsString('namespace ', $output);
90
    }
91
92
    /**
93
     * @throws \ReflectionException
94
     */
95 View Code Duplication
    public function testUseStmtsInSameNamespace(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
96
    {
97
        $class = Fixtures\Entity::class;
98
        $as = 'EntityProxy' . __LINE__;
99
100
        $declaration = new Declaration(new \ReflectionClass($class), $as);
101
        $output = $this->make($declaration);
102
        $output = ltrim($output, '<?php');
103
104
        $this->assertFalse(class_exists($declaration->class->getFullName()));
105
106
        eval($output);
107
108
        $this->assertSame($this->fetchUseStatements($output), $this->fetchExternalDependencies($declaration->class->getFullName(), [
109
            PromiseResolver::class,
110
            PromiseInterface::class
111
        ]));
112
    }
113
114
    /**
115
     * @throws \ReflectionException
116
     */
117 View Code Duplication
    public function testUseStmtsInDifferentNamespace(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
118
    {
119
        $class = Fixtures\Entity::class;
120
        $as = "\EntityProxy" . __LINE__;
121
122
        $declaration = new Declaration(new \ReflectionClass($class), $as);
123
        $output = $this->make($declaration);
124
        $output = ltrim($output, '<?php');
125
126
        $this->assertFalse(class_exists($declaration->class->getFullName()));
127
128
        eval($output);
129
130
        $this->assertSame($this->fetchUseStatements($output), $this->fetchExternalDependencies($declaration->class->getFullName(), [
131
            PromiseResolver::class,
132
            PromiseInterface::class,
133
            $class
134
        ]));
135
    }
136
137
    private function fetchUseStatements(string $code): array
138
    {
139
        $uses = [];
140
        foreach (explode("\n", $code) as $line) {
141
            if (mb_stripos($line, 'use') !== 0) {
142
                continue;
143
            }
144
145
            $uses[] = trim(mb_substr($line, 4), " ;\r\n");
146
        }
147
148
        sort($uses);
149
150
        return $uses;
151
    }
152
153
    /**
154
     * @param string $class
155
     * @param array  $types
156
     *
157
     * @return array
158
     * @throws \ReflectionException
159
     */
160
    private function fetchExternalDependencies(string $class, array $types = []): array
161
    {
162
        $reflection = new \ReflectionClass($class);
163
164
        foreach ($reflection->getConstructor()->getParameters() as $parameter) {
165
            if (!$parameter->hasType() || $parameter->getType()->isBuiltin()) {
166
                continue;
167
            }
168
169
            $types[] = $parameter->getType()->getName();
170
        }
171
172
        sort($types);
173
174
        return $types;
175
    }
176
177 View Code Duplication
    public function testTraits(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
178
    {
179
        $this->assertStringNotContainsString(' use ',
180
            $this->make(new Declaration(new \ReflectionClass(Fixtures\EntityWithoutTrait::class), 'EntityProxy' . __LINE__)));
181
        $this->assertStringNotContainsString(' use ',
182
            $this->make(new Declaration(new \ReflectionClass(Fixtures\EntityWithTrait::class), 'EntityProxy' . __LINE__)));
183
    }
184
185 View Code Duplication
    public function testConstants(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
186
    {
187
        $this->assertStringNotContainsString(' const ',
188
            $this->make(new Declaration(new \ReflectionClass(Fixtures\EntityWithoutConstants::class), 'EntityProxy' . __LINE__)));
189
        $this->assertStringNotContainsString(' const ',
190
            $this->make(new Declaration(new \ReflectionClass(Fixtures\EntityWithConstants::class), 'EntityProxy' . __LINE__)));
191
    }
192
193
    public function testProperties(): void
194
    {
195
        $class = Fixtures\Entity::class;
196
        $as = 'EntityProxy' . __LINE__;
197
198
        $declaration = new Declaration(new \ReflectionClass($class), $as);
199
        $output = $this->make($declaration);
200
        $output = ltrim($output, '<?php');
201
202
        $this->assertFalse(class_exists($declaration->class->getFullName()));
203
204
        eval($output);
205
206
        $reflection = new \ReflectionClass($declaration->class->getFullName());
207
208
        /** @var \ReflectionProperty[] $properties */
209
        $properties = [];
210
        foreach ($reflection->getProperties() as $property) {
211
            if ($property->getDeclaringClass()->getName() !== $declaration->class->getFullName()) {
0 ignored issues
show
introduced by
Consider using $property->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
212
                continue;
213
            }
214
215
            $properties[] = $property;
216
        }
217
218
        $this->assertCount(1, $properties);
0 ignored issues
show
Documentation introduced by
$properties is of type array<integer,object<ReflectionProperty>>, but the function expects a object<Countable>|object...nit\Framework\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
219
        $property = $properties[0];
220
        $this->assertTrue($property->isPrivate());
221
        $this->assertFalse($property->isStatic());
222
        $this->assertStringContainsString('@var PromiseResolver|' . Utils::shortName($class), $property->getDocComment());
223
    }
224
225
    /**
226
     * @throws \ReflectionException
227
     */
228
    public function testHasConstructor(): void
229
    {
230
        $class = Fixtures\EntityWithoutConstructor::class;
231
        $as = 'EntityProxy' . __LINE__;
232
233
        $declaration = new Declaration(new \ReflectionClass($class), $as);
234
        $output = $this->make($declaration);
235
        $output = ltrim($output, '<?php');
236
237
        $this->assertFalse(class_exists($declaration->class->getFullName()));
238
239
        eval($output);
240
241
        $reflection = new \ReflectionClass($declaration->class->getFullName());
242
        $constructor = $reflection->getConstructor();
243
        $this->assertNotNull($constructor);
244
245
        $class = Fixtures\EntityWithConstructor::class;
246
        $as = 'EntityProxy' . __LINE__;
247
248
        $declaration = new Declaration(new \ReflectionClass($class), $as);
249
        $output = $this->make($declaration);
250
        $output = ltrim($output, '<?php');
251
252
        $this->assertFalse(class_exists($declaration->class->getFullName()));
253
254
        eval($output);
255
256
        $reflection = new \ReflectionClass($declaration->class->getFullName());
257
        $constructor = $reflection->getConstructor();
258
        $this->assertNotNull($constructor);
259
    }
260
261 View Code Duplication
    public function testNotContainParentConstructor(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
262
    {
263
        $class = Fixtures\EntityWithoutConstructor::class;
264
        $as = 'EntityProxy' . __LINE__;
265
266
        $declaration = new Declaration(new \ReflectionClass($class), $as);
267
        $output = $this->make($declaration);
268
        $output = ltrim($output, '<?php');
269
270
        $this->assertFalse(class_exists($declaration->class->getFullName()));
271
272
        eval($output);
273
274
        $this->assertStringNotContainsString('parent::__construct();', $output);
275
    }
276
277 View Code Duplication
    public function testContainParentConstructor(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
278
    {
279
        $class = Fixtures\EntityWithConstructor::class;
280
        $as = 'EntityProxy' . __LINE__;
281
282
        $declaration = new Declaration(new \ReflectionClass($class), $as);
283
        $output = $this->make($declaration);
284
        $output = ltrim($output, '<?php');
285
286
        $this->assertFalse(class_exists($declaration->class->getFullName()));
287
288
        eval($output);
289
290
        $this->assertStringContainsString('parent::__construct();', $output);
291
    }
292
293 View Code Duplication
    public function testPromiseMethods(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
294
    {
295
        $class = Fixtures\Entity::class;
296
        $as = 'EntityProxy' . __LINE__;
297
298
        $declaration = new Declaration(new \ReflectionClass($class), $as);
299
        $output = $this->make($declaration);
300
        $output = ltrim($output, '<?php');
301
302
        $this->assertFalse(class_exists($declaration->class->getFullName()));
303
304
        eval($output);
305
306
        $i = new \ReflectionClass(PromiseInterface::class);
307
        foreach ($i->getMethods() as $method) {
308
            $this->assertStringContainsString("public function {$method->getName()}()", $output);
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
309
        }
310
    }
311
312
    public function testInheritedProperties(): void
313
    {
314
        $class = Fixtures\ChildEntity::class;
315
        $as = 'EntityProxy' . __LINE__;
316
317
        $reflection = new \ReflectionClass($class);
318
319
        $declaration = new Declaration($reflection, $as);
320
        $output = $this->make($declaration);
321
        $output = ltrim($output, '<?php');
322
323
        $this->assertFalse(class_exists($declaration->class->getFullName()));
324
325
        eval($output);
326
327
        $sourceProperties = [];
328
        foreach ($reflection->getProperties() as $property) {
329
            $sourceProperties[] = $property->getName();
330
        }
331
332
        $properties = [];
333
        foreach ($this->getDeclaration($class)->properties as $property) {
334
            $properties[] = $property;
335
        }
336
337
        foreach ($sourceProperties as $property) {
338
            $this->assertArrayNotHasKey($property, $properties, "Proxied class contains not expected `{$property}` property");
339
            $this->assertStringNotContainsString(" $property;", $output);
340
        }
341
342
        foreach ($properties as $property) {
343
            $this->assertArrayNotHasKey($property, $sourceProperties, "Origin class contains not expected `{$property}` property");
344
        }
345
    }
346
347
    public function testInheritedMethods(): void
348
    {
349
        $class = Fixtures\ChildEntity::class;
350
        $as = 'EntityProxy' . __LINE__;
351
352
        $reflection = new \ReflectionClass($class);
353
354
        $declaration = new Declaration($reflection, $as);
355
        $output = $this->make($declaration);
356
        $output = ltrim($output, '<?php');
357
358
        $this->assertFalse(class_exists($declaration->class->getFullName()));
359
360
        eval($output);
361
362
        $sourceMethods = [];
363
364
        //There're only public and protected methods inside
365
        foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
366
            $sourceMethods[$method->getName()] = $method->isPublic() ? 'public' : 'protected';
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
367
        }
368
369
        /** @var \PhpParser\Node\Stmt\ClassMethod[] $methods */
370
        $methods = [];
371
        foreach ($this->getDeclaration($class)->methods as $method) {
372
            $methods[$method->name->name] = $method;
373
        }
374
375
        foreach ($sourceMethods as $name => $accessor) {
376
            $this->assertArrayHasKey($name, $methods, "Proxy class does not contain expected `{$name}` method");
377
378
            if ($accessor === 'public') {
379
                $this->assertTrue($methods[$name]->isPublic(), "Proxied method `{$name}` expected to be public");
380
                $this->assertStringContainsString("public function {$name}()", $output);
381
            } else {
382
                $this->assertTrue($methods[$name]->isProtected(), "Proxied method `{$name}` expected to be protected");
383
                $this->assertStringContainsString("protected function {$name}()", $output);
384
            }
385
        }
386
387
        foreach ($methods as $name => $method) {
388
            $this->assertArrayHasKey($name, $sourceMethods, "Origin class does not contain expected `{$name}` method");
389
390
            if ($method->isPublic()) {
391
                $this->assertEquals('public', $sourceMethods[$name], "Proxied method `{$name}` expected to be public");
392
            } elseif ($method->isProtected()) {
393
                $this->assertEquals('protected', $sourceMethods[$name], "Proxied method `{$name}` expected to be public");
394
            } else {
395
                throw new \UnexpectedValueException("\"{$method->name->toString()}\" method not found");
396
            }
397
        }
398
    }
399
400
    private function getDeclaration(string $class): Structure
401
    {
402
        return $this->extractor()->extract($class);
403
    }
404
405
    private function extractor(): Extractor
406
    {
407
        $container = new Container();
408
409
        return $container->get(Extractor::class);
410
    }
411
412
    /**
413
     * @param string $className
414
     * @param string $proxyFullName
415
     *
416
     * @return object
417
     */
418
    private function makeProxyObject(string $className, string $proxyFullName)
419
    {
420
        $orm = \Mockery::mock(ORMInterface::class);
421
422
        $container = new Container();
423
        $container->bind(ORMInterface::class, $orm);
424
425
        return $container->make($proxyFullName, ['role' => $className, 'scope' => []]);
426
    }
427
428
    private function make(Declaration $declaration): string
429
    {
430
        return $this->proxyCreator()->make($declaration);
431
    }
432
433
    private function proxyCreator(): ProxyPrinter
434
    {
435
        $container = new Container();
436
        $container->bind(PrettyPrinterAbstract::class, Standard::class);
437
438
        return $container->get(ProxyPrinter::class);
439
    }
440
}