Completed
Pull Request — master (#5)
by Cees-Jan
11:35 queued 08:19
created

AsyncTableGenerator::hasMethodAnnotation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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