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

AsyncTableGenerator::createMethodArguments()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
crap 3.0416
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 string $tableClass
75 1
     * @param bool $force
76
     * @return GeneratedTable
77 1
     */
78
    public function generate($tableClass, $force = false)
79 1
    {
80
        $fileName = $this->classLoader->findFile($tableClass);
81
        $contents = file_get_contents($fileName);
82
        $ast = $this->parser->parse($contents);
83 1
        $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 82 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...
84 1
85 1
        $hashedClass = 'C' . md5($tableClass) . '_F' . md5($contents);
86
87
        $generatedTable = new GeneratedTable($namespace, $hashedClass);
88 1
89 1
        if (!$force && file_exists($this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php')) {
90 1
            return $generatedTable;
91
        }
92
93
        $class = $this->factory->class($hashedClass)
94 1
            ->extend('BaseTable')
95 1
            ->implement('AsyncTableInterface')
96 1
        ;
97
98 1
        $class->addStmt(
99 1
            new Node\Stmt\TraitUse([
100
                new Node\Name('AsyncTable')
101
            ])
102
        );
103
104 1
        $class->addStmt(
105 1
            self::createMethod(
106 1
                'save',
107 1
                [
108 1
                    new Node\Param('entity', null, 'EntityInterface'),
109
                    new Node\Param('options', new Node\Expr\Array_()),
110
                ]
111
            )
112
        );
113 1
114 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 82 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...
115 1
            if ($this->hasMethodAnnotation(new ReflectionClass($tableClass), $method->name, Ignore::class)) {
116 1
                continue;
117 1
            }
118 1
119 1
            $class->addStmt(
120
                self::createMethod(
121
                    $method->name,
122 1
                    $method->params
123 1
                )
124 1
            );
125 1
        }
126 1
127 1
        $node = $this->factory->namespace($namespace)
128
            ->addStmt($this->factory->use(EntityInterface::class))
129
            ->addStmt($this->factory->use($tableClass)->as('BaseTable'))
130 1
            ->addStmt($this->factory->use(AsyncTable::class))
131
            ->addStmt($this->factory->use(AsyncTableInterface::class))
132
            ->addStmt($class)
133 1
            ->getNode()
134
        ;
135 1
136 1
        $prettyPrinter = new Standard();
137 1
        file_put_contents(
138 1
            $this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php',
139 1
            $prettyPrinter->prettyPrintFile([
140 1
                $node
141 1
            ]) . PHP_EOL
142 1
        );
143
144 1
        return $generatedTable;
145 1
    }
146 1
147
    protected function createMethod($method, array $params)
148
    {
149
        return $this->factory->method($method)
150
            ->makePublic()
151
            ->addParams($params)
152
            ->addStmt(
153
                new Node\Stmt\Return_(
154
                    new Node\Expr\MethodCall(
155
                        new Node\Expr\Variable('this'),
156
                        'callAsyncOrSync',
157
                        [
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...
158
                            new Node\Scalar\String_($method),
159 1
                            new Node\Expr\Array_(
160
                                $this->createMethodArguments($params)
161 1
                            ),
162 1
                        ]
163 1
                    )
164
                )
165
            )
166 1
            ;
167
    }
168 1
169
    /**
170
     * @param array $params
171
     * @return array
172
     */
173
    protected function createMethodArguments(array $params)
174
    {
175 1
        $arguments = [];
176
        foreach ($params as $param) {
177 1
            if (!($param instanceof Node\Param)) {
178 1
                continue;
179
            }
180
            $arguments[] = new Node\Expr\Variable($param->name);
181
        }
182 1
        return $arguments;
183 1
    }
184
185
    /**
186 1
     * @param Node[] $ast
187
     * @return Generator
188 1
     */
189 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...
190 1
    {
191 1
        foreach ($ast as $node) {
192 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...
193
                continue;
194
            }
195 1
196 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...
197
                yield $stmt;
198
            }
199 1
        }
200 1
    }
201
202 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...
203 1
    {
204
        foreach ($stmts as $stmt) {
205 1
            if ($stmt instanceof Node\Stmt\ClassMethod) {
206
                yield $stmt;
207 1
            }
208 1
209 1
            if (!isset($stmt->stmts)) {
210
                continue;
211
            }
212
213
            foreach ($this->iterageStmts($stmt->stmts) as $stmt) {
214
                yield $stmt;
215
            }
216
        }
217
    }
218
219
    protected function extractNamespace(array $ast)
220
    {
221
        foreach ($ast as $node) {
222
            if ($node instanceof Node\Stmt\Namespace_) {
223
                return (string)$node->name;
224
            }
225
        }
226
227
        return 'N' . uniqid('', true);
228
    }
229
230
    /**
231
     * @param ReflectionClass $reflectionClass
232
     * @param string $method
233
     * @param string $class
234
     * @return bool
235
     */
236
    private function hasMethodAnnotation(ReflectionClass $reflectionClass, $method, $class)
237
    {
238
        $methodReflection = $reflectionClass->getMethod($method);
239
        return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class);
240
    }
241
242
}
243