Completed
Pull Request — master (#5)
by Cees-Jan
09:16
created

AsyncTableGenerator   C

Complexity

Total Complexity 25

Size/Duplication

Total Lines 224
Duplicated Lines 12.5 %

Coupling/Cohesion

Components 1
Dependencies 19

Test Coverage

Coverage 94.51%

Importance

Changes 0
Metric Value
wmc 25
lcom 1
cbo 19
dl 28
loc 224
ccs 86
cts 91
cp 0.9451
rs 6.875
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A locateClassloader() 0 13 3
B generate() 0 68 4
A createMethod() 0 21 1
A createMethodArguments() 0 11 3
A extractMethods() 12 12 4
B iterageStmts() 16 16 5
A extractNamespace() 0 10 3
A hasMethodAnnotation() 0 5 1

How to fix   Duplicated Code   

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:

1
<?php
2
3
namespace WyriHaximus\React\Cake\Orm;
4
5
use Cake\Datasource\EntityInterface;
6
use Composer\Autoload\ClassLoader;
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Generator;
9
use PhpParser\BuilderFactory;
10
use PhpParser\Node;
11
use PhpParser\Parser;
12
use PhpParser\ParserFactory;
13
use PhpParser\PrettyPrinter\Standard;
14
use ReflectionClass;
15
use RuntimeException;
16
use WyriHaximus\React\Cake\Orm\Annotations\Ignore;
17
18
final class AsyncTableGenerator
19
{
20
    const NAMESPACE_PREFIX = 'WyriHaximus\GeneratedAsyncCakeTable\\';
21
22
    /**
23
     * @var string
24
     */
25
    private $storageLocation;
26
27
    /**
28
     * @var BuilderFactory
29
     */
30
    private $factory;
31
32
    /**
33
     * @var Parser
34
     */
35
    private $parser;
36
37
    /**
38
     * @var ClassLoader
39
     */
40
    private $classLoader;
41
42 1
    /**
43
     * @var AnnotationReader
44 1
     */
45 1
    private $annotationReader;
46 1
47 1
    /**
48 1
     * @param string $storageLocation
49
     */
50 1
    public function __construct($storageLocation)
51
    {
52
        $this->storageLocation = $storageLocation;
53 1
        $this->factory = new BuilderFactory();
54 1
        $this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
55
        $this->classLoader = $this->locateClassloader();
56 1
        $this->annotationReader = new AnnotationReader();
57 1
    }
58
59
    private function locateClassloader()
60
    {
61
        foreach ([
62
                     dirname(__DIR__) . DS . 'vendor' . DS . 'autoload.php',
63
                     dirname(dirname(dirname(__DIR__))) . DS . 'autoload.php',
64
                 ] as $path) {
65
            if (file_exists($path)) {
66
                return require $path;
67
            }
68 1
        }
69
70 1
        throw new RuntimeException('Unable to locate class loader');
71 1
    }
72 1
73 1
    /**
74
     * @param $tableClass
75 1
     * @return GeneratedTable
76
     */
77 1
    public function generate($tableClass)
78
    {
79 1
        $fileName = $this->classLoader->findFile($tableClass);
80
        $contents = file_get_contents($fileName);
81
        $ast = $this->parser->parse($contents);
82
        $namespace = static::NAMESPACE_PREFIX . $this->extractNamespace($ast);
0 ignored issues
show
Bug introduced by
It seems like $ast defined by $this->parser->parse($contents) on line 81 can also be of type null; however, WyriHaximus\React\Cake\O...tor::extractNamespace() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
83 1
84 1
        $hashedClass = 'C' . md5($tableClass) . '_F' . md5($contents);
85 1
86
        $generatedTable = new GeneratedTable($namespace, $hashedClass);
87
88 1
        if (file_exists($this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php')) {
89 1
            return $generatedTable;
90 1
        }
91
92
        $class = $this->factory->class($hashedClass)
93
            ->extend('BaseTable')
94 1
            ->implement('AsyncTableInterface')
95 1
        ;
96 1
97
        $class->addStmt(
98 1
            new Node\Stmt\TraitUse([
99 1
                new Node\Name('AsyncTable')
100
            ])
101
        );
102
103
        $class->addStmt(
104 1
            self::createMethod(
105 1
                'save',
106 1
                [
107 1
                    new Node\Param('entity', null, 'EntityInterface'),
108 1
                    new Node\Param('options', new Node\Expr\Array_()),
109
                ]
110
            )
111
        );
112
113 1
        foreach ($this->extractMethods($ast) as $method) {
0 ignored issues
show
Bug introduced by
It seems like $ast defined by $this->parser->parse($contents) on line 81 can also be of type null; however, WyriHaximus\React\Cake\O...rator::extractMethods() does only seem to accept array<integer,object<PhpParser\Node>>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
114 1
            if ($this->hasMethodAnnotation(new ReflectionClass($tableClass), $method->name, Ignore::class)) {
115 1
                continue;
116 1
            }
117 1
118 1
            $class->addStmt(
119 1
                self::createMethod(
120
                    $method->name,
121
                    $method->params
122 1
                )
123 1
            );
124 1
        }
125 1
126 1
        $node = $this->factory->namespace($namespace)
127 1
            ->addStmt($this->factory->use(EntityInterface::class))
128
            ->addStmt($this->factory->use($tableClass)->as('BaseTable'))
129
            ->addStmt($this->factory->use(AsyncTable::class))
130 1
            ->addStmt($this->factory->use(AsyncTableInterface::class))
131
            ->addStmt($class)
132
            ->getNode()
133 1
        ;
134
135 1
        $prettyPrinter = new Standard();
136 1
        file_put_contents(
137 1
            $this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php',
138 1
            $prettyPrinter->prettyPrintFile([
139 1
                $node
140 1
            ]) . PHP_EOL
141 1
        );
142 1
143
        return $generatedTable;
144 1
    }
145 1
146 1
    protected function createMethod($method, array $params)
147
    {
148
        return $this->factory->method($method)
149
            ->makePublic()
150
            ->addParams($params)
151
            ->addStmt(
152
                new Node\Stmt\Return_(
153
                    new Node\Expr\MethodCall(
154
                        new Node\Expr\Variable('this'),
155
                        'callAsyncOrSync',
156
                        [
0 ignored issues
show
Documentation introduced by
array(new \PhpParser\Nod...hodArguments($params))) is of type array<integer,object<Php...\Node\\Expr\\Array_>"}>, but the function expects a array<integer,object<PhpParser\Node\Arg>>.

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...
157
                            new Node\Scalar\String_($method),
158
                            new Node\Expr\Array_(
159 1
                                $this->createMethodArguments($params)
160
                            ),
161 1
                        ]
162 1
                    )
163 1
                )
164
            )
165
            ;
166 1
    }
167
168 1
    /**
169
     * @param array $params
170
     * @return array
171
     */
172
    protected function createMethodArguments(array $params)
173
    {
174
        $arguments = [];
175 1
        foreach ($params as $param) {
176
            if (!($param instanceof Node\Param)) {
177 1
                continue;
178 1
            }
179
            $arguments[] = new Node\Expr\Variable($param->name);
180
        }
181
        return $arguments;
182 1
    }
183 1
184
    /**
185
     * @param Node[] $ast
186 1
     * @return Generator
187
     */
188 1 View Code Duplication
    protected function extractMethods(array $ast)
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...
189
    {
190 1
        foreach ($ast as $node) {
191 1
            if (!isset($node->stmts)) {
0 ignored issues
show
Bug introduced by
Accessing stmts on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
192 1
                continue;
193
            }
194
195 1
            foreach ($this->iterageStmts($node->stmts) as $stmt) {
0 ignored issues
show
Bug introduced by
Accessing stmts on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
196 1
                yield $stmt;
197
            }
198
        }
199 1
    }
200 1
201 View Code Duplication
    protected function iterageStmts(array $stmts)
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...
202
    {
203 1
        foreach ($stmts as $stmt) {
204
            if ($stmt instanceof Node\Stmt\ClassMethod) {
205 1
                yield $stmt;
206
            }
207 1
208 1
            if (!isset($stmt->stmts)) {
209 1
                continue;
210
            }
211
212
            foreach ($this->iterageStmts($stmt->stmts) as $stmt) {
213
                yield $stmt;
214
            }
215
        }
216
    }
217
218
    protected function extractNamespace(array $ast)
219
    {
220
        foreach ($ast as $node) {
221
            if ($node instanceof Node\Stmt\Namespace_) {
222
                return (string)$node->name;
223
            }
224
        }
225
226
        return 'N' . uniqid('', true);
227
    }
228
229
    /**
230
     * @param ReflectionClass $reflectionClass
231
     * @param string $method
232
     * @param string $class
233
     * @return bool
234
     */
235
    private function hasMethodAnnotation(ReflectionClass $reflectionClass, $method, $class)
236
    {
237
        $methodReflection = $reflectionClass->getMethod($method);
238
        return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class);
239
    }
240
241
}
242