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

AsyncTableGenerator::locateClassloader()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
ccs 3
cts 4
cp 0.75
rs 9.4285
cc 3
eloc 8
nc 3
nop 0
crap 3.1406
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 (in_array($method->name, ['initialize', 'validationDefault'])) {
116 1
                continue;
117 1
            }
118 1
119 1
            if ($this->hasMethodAnnotation(new ReflectionClass($tableClass), $method->name, Ignore::class)) {
120
                continue;
121
            }
122 1
123 1
            $class->addStmt(
124 1
                self::createMethod(
125 1
                    $method->name,
126 1
                    $method->params
127 1
                )
128
            );
129
        }
130 1
131
        $node = $this->factory->namespace($namespace)
132
            ->addStmt($this->factory->use(EntityInterface::class))
133 1
            ->addStmt($this->factory->use($tableClass)->as('BaseTable'))
134
            ->addStmt($this->factory->use(AsyncTable::class))
135 1
            ->addStmt($this->factory->use(AsyncTableInterface::class))
136 1
            ->addStmt($class)
137 1
            ->getNode()
138 1
        ;
139 1
140 1
        $prettyPrinter = new Standard();
141 1
        file_put_contents(
142 1
            $this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php',
143
            $prettyPrinter->prettyPrintFile([
144 1
                $node
145 1
            ]) . PHP_EOL
146 1
        );
147
148
        return $generatedTable;
149
    }
150
151
    protected function createMethod($method, array $params)
152
    {
153
        return $this->factory->method($method)
154
            ->makePublic()
155
            ->addParams($params)
156
            ->addStmt(
157
                new Node\Stmt\Return_(
158
                    new Node\Expr\MethodCall(
159 1
                        new Node\Expr\Variable('this'),
160
                        'callAsyncOrSync',
161 1
                        [
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
                                $this->createMethodArguments($params)
165
                            ),
166 1
                        ]
167
                    )
168 1
                )
169
            )
170
            ;
171
    }
172
173
    /**
174
     * @param array $params
175 1
     * @return array
176
     */
177 1
    protected function createMethodArguments(array $params)
178 1
    {
179
        $arguments = [];
180
        foreach ($params as $param) {
181
            if (!($param instanceof Node\Param)) {
182 1
                continue;
183 1
            }
184
            $arguments[] = new Node\Expr\Variable($param->name);
185
        }
186 1
        return $arguments;
187
    }
188 1
189
    /**
190 1
     * @param Node[] $ast
191 1
     * @return Generator
192 1
     */
193 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 1
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
                yield $stmt;
202
            }
203 1
        }
204
    }
205 1
206 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 1
    {
208 1
        foreach ($stmts as $stmt) {
209 1
            if ($stmt instanceof Node\Stmt\ClassMethod) {
210
                yield $stmt;
211
            }
212
213
            if (!isset($stmt->stmts)) {
214
                continue;
215
            }
216
217
            foreach ($this->iterageStmts($stmt->stmts) as $stmt) {
218
                yield $stmt;
219
            }
220
        }
221
    }
222
223
    protected function extractNamespace(array $ast)
224
    {
225
        foreach ($ast as $node) {
226
            if ($node instanceof Node\Stmt\Namespace_) {
227
                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
    private function hasMethodAnnotation(ReflectionClass $reflectionClass, $method, $class)
241
    {
242
        $methodReflection = $reflectionClass->getMethod($method);
243
        return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class);
244
    }
245
}
246