AsyncTableGenerator::locateClassloader()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 13
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 9.8333
cc 3
nc 3
nop 0
crap 12
1
<?php declare(strict_types=1);
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\Builder\Use_;
10
use PhpParser\BuilderFactory;
11
use PhpParser\Node;
12
use PhpParser\Parser;
13
use PhpParser\ParserFactory;
14
use PhpParser\PrettyPrinter\Standard;
15
use ReflectionClass;
16
use RuntimeException;
17
use WyriHaximus\React\Cake\Orm\Annotations\Ignore;
18
19
final class AsyncTableGenerator
20
{
21
    const NAMESPACE_PREFIX = 'WyriHaximus\GeneratedAsyncCakeTable';
22
23
    /**
24
     * @var string
25
     */
26
    private $storageLocation;
27
28
    /**
29
     * @var BuilderFactory
30
     */
31
    private $factory;
32
33
    /**
34
     * @var Parser
35
     */
36
    private $parser;
37
38
    /**
39
     * @var ClassLoader
40
     */
41
    private $classLoader;
42
43
    /**
44
     * @var AnnotationReader
45
     */
46
    private $annotationReader;
47
48
    /**
49
     * @param string $storageLocation
50 1
     */
51
    public function __construct($storageLocation)
52 1
    {
53 1
        $this->storageLocation = $storageLocation;
54 1
        $this->factory = new BuilderFactory();
55 1
        $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
56 1
        $this->classLoader = $this->locateClassloader();
57 1
        $this->annotationReader = new AnnotationReader();
58
    }
59 1
60
    /**
61
     * @param  string         $tableClass
62 1
     * @param  bool           $force
63 1
     * @return GeneratedTable
64
     */
65 1
    public function generate($tableClass, $force = false)
66 1
    {
67
        $fileName = $this->classLoader->findFile($tableClass);
68
        $contents = file_get_contents($fileName);
69
        $ast = $this->parser->parse($contents);
70
        $namespace = static::NAMESPACE_PREFIX;
71
72
        $hashedClass = $this->extractNamespace($ast) . '_C' . md5($tableClass) . '_F' . md5($contents);
0 ignored issues
show
Bug introduced by
It seems like $ast defined by $this->parser->parse($contents) on line 69 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...
73
74
        $generatedTable = new GeneratedTable($namespace, $hashedClass);
75
76
        if (!$force && file_exists($this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php')) {
77
            return $generatedTable;
78 1
        }
79
80 1
        $class = $this->factory->class($hashedClass)
81 1
            ->extend('BaseTable')
82 1
            ->implement('AsyncTableInterface')
83 1
        ;
84
85 1
        $class->addStmt(
86
            new Node\Stmt\TraitUse([
87 1
                new Node\Name('AsyncTable'),
88
            ])
89 1
        );
90
91
        $class->addStmt(
92
            self::createMethod(
93 1
                'save',
94 1
                [
95 1
                    new Node\Param('entity', null, 'EntityInterface'),
96
                    new Node\Param('options', new Node\Expr\Array_()),
97
                ]
98 1
            )
99 1
        );
100 1
101
        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 69 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...
102
            if (in_array($method->name, ['initialize', 'validationDefault'], true)) {
103
                continue;
104 1
            }
105 1
106 1
            if ($this->hasMethodAnnotation(new ReflectionClass($tableClass), $method->name, Ignore::class)) {
107
                continue;
108 1
            }
109 1
110
            $class->addStmt(
111
                self::createMethod(
112
                    $method->name,
113
                    $method->params
114 1
                )
115 1
            );
116
        }
117
118
        $uses = iterator_to_array($this->extractClassImports($ast));
0 ignored issues
show
Bug introduced by
It seems like $ast defined by $this->parser->parse($contents) on line 69 can also be of type null; however, WyriHaximus\React\Cake\O...::extractClassImports() 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...
119 1
        $uses[] = $this->factory->use(EntityInterface::class);
120
        $uses[] = $this->factory->use($tableClass)->as('BaseTable');
121
        $uses[] = $this->factory->use(AsyncTable::class);
122
        $uses[] = $this->factory->use(AsyncTable::class);
123 1
        $uses[] = $this->factory->use(AsyncTableInterface::class);
124 1
125 1
        $node = $this->factory->namespace($namespace)
126 1
            ->addStmts($this->removeDuplicatedUses($uses))
127
            ->addStmt($class)
128
            ->getNode()
129
        ;
130
131 1
        $fileName = $this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php';
132 1
        $prettyPrinter = new Standard();
133 1
        $fileContents = $prettyPrinter->prettyPrintFile([$node,]) . PHP_EOL;
134 1
        file_put_contents(
135 1
            $fileName,
136 1
            $fileContents
137 1
        );
138
139
        do {
140 1
            usleep(500);
141 1
        } while (file_get_contents($fileName) !== $fileContents);
142 1
143 1
        $command = 'PHP_CS_FIXER_IGNORE_ENV=1 ' .
144 1
            dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor/bin/php-cs-fixer fix ' .
145 1
            $this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php' .
146
            ' --config=' .
147
            dirname(__DIR__) .
148 1
            DIRECTORY_SEPARATOR .
149
            '.php_cs ' .
150
            ' --allow-risky=yes -q -v --stop-on-violation --using-cache=no' .
151 1
            ' 2>&1';
152
153 1
        exec($command);
154 1
155 1
        return $generatedTable;
156 1
    }
157 1
158 1
    protected function removeDuplicatedUses(array $rawUses)
159 1
    {
160 1
        $uses = [];
161
        /** @var Node\Stmt\Use_ $use */
162 1
        foreach ($rawUses as $use) {
163 1
            if ($use instanceof Use_) {
164 1
                $use = $use->getNode();
165
            }
166
167
            $uses[$use->uses[0]->type . '_____' . $use->uses[0]->name->toString() . '_____' . $use->uses[0]->alias] = $use;
168
        }
169
170
        return $uses;
171
    }
172
173
    protected function createMethod($method, array $params)
174
    {
175
        return $this->factory->method($method)
176
            ->makePublic()
177 1
            ->addParams($params)
178
            ->addStmt(
179 1
                new Node\Stmt\Return_(
180 1
                    new Node\Expr\MethodCall(
181 1
                        new Node\Expr\Variable('this'),
182
                        'callAsyncOrSync',
183
                        [
184 1
                            new Node\Scalar\String_($method),
185
                            new Node\Expr\Array_(
186 1
                                $this->createMethodArguments($params)
187
                            ),
188
                        ]
189
                    )
190
                )
191
            )
192
            ;
193 1
    }
194
195 1
    /**
196 1
     * @param  array $params
197
     * @return array
198
     */
199
    protected function createMethodArguments(array $params)
200 1
    {
201 1
        $arguments = [];
202
        foreach ($params as $param) {
203
            if (!($param instanceof Node\Param)) {
204 1
                continue;
205
            }
206 1
            $arguments[] = new Node\Expr\Variable($param->name);
207
        }
208 1
209 1
        return $arguments;
210 1
    }
211
212
    /**
213 1
     * @param  Node[]    $ast
214 1
     * @return Generator
215
     */
216 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...
217 1
    {
218 1
        foreach ($ast as $node) {
219
            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...
220
                continue;
221 1
            }
222
223 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...
224
                yield $stmt;
225 1
            }
226 1
        }
227 1
    }
228
229 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...
230
    {
231
        foreach ($stmts as $stmt) {
232
            if ($stmt instanceof Node\Stmt\ClassMethod) {
233
                yield $stmt;
234
            }
235
236
            if (!isset($stmt->stmts)) {
237
                continue;
238
            }
239
240 1
            foreach ($this->iterageStmts($stmt->stmts) as $stmt) {
241
                yield $stmt;
242 1
            }
243 1
        }
244
    }
245
246
    protected function extractNamespace(array $ast)
247
    {
248
        foreach ($ast as $node) {
249
            if ($node instanceof Node\Stmt\Namespace_) {
250
                return str_replace('\\', '_', (string)$node->name);
251
            }
252
        }
253
254
        return 'N' . uniqid('', true);
255
    }
256
257
    protected function extractClassImports(array $ast)
258
    {
259
        foreach ($ast as $node) {
260
            if ($node instanceof Node\Stmt\Namespace_) {
261
                foreach ($node->stmts as $stmt) {
262
                    if ($stmt instanceof Node\Stmt\Use_) {
263
                        yield $stmt;
264
                    }
265
                }
266
            }
267
        }
268
    }
269
270
    private function locateClassloader()
271
    {
272
        foreach ([
273
                     dirname(__DIR__) . DS . 'vendor' . DS . 'autoload.php',
274
                     dirname(dirname(dirname(__DIR__))) . DS . 'autoload.php',
275
                 ] as $path) {
276
            if (file_exists($path)) {
277
                return require $path;
278
            }
279
        }
280
281
        throw new RuntimeException('Unable to locate class loader');
282
    }
283
284
    /**
285
     * @param  ReflectionClass $reflectionClass
286
     * @param  string          $method
287
     * @param  string          $class
288
     * @return bool
289
     */
290
    private function hasMethodAnnotation(ReflectionClass $reflectionClass, $method, $class)
291
    {
292
        $methodReflection = $reflectionClass->getMethod($method);
293
294
        return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class);
295
    }
296
}
297