Completed
Pull Request — 2.x (#99)
by Akihito
03:04 queued 01:50
created

CodeGenMethod::getTemplateMethodNodeStmts()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\Aop;
6
7
use function class_exists;
8
use Doctrine\Common\Annotations\AnnotationReader;
9
use function is_string;
10
use PhpParser\Builder\Method;
11
use PhpParser\Builder\Param;
12
use PhpParser\BuilderFactory;
13
use PhpParser\Comment\Doc;
14
use PhpParser\Node;
15
use PhpParser\Node\NullableType;
16
use PhpParser\Node\Stmt\Class_;
17
use PhpParser\Node\Stmt\ClassMethod;
18
use PhpParser\NodeTraverser;
19
use PhpParser\Parser;
20
use PhpParser\PrettyPrinter\Standard;
21
use Ray\Aop\Annotation\AbstractAssisted;
22
23
final class CodeGenMethod
24
{
25
    /**
26
     * @var \PhpParser\Parser
27
     */
28
    private $parser;
29
30
    /**
31
     * @var \PhpParser\BuilderFactory
32
     */
33
    private $factory;
34
35
    /**
36
     * @var \PhpParser\PrettyPrinter\Standard
37
     */
38
    private $printer;
39
40
    private $reader;
41
42
    /**
43
     * @var AbstractAssisted
44
     */
45
    private $assisted;
46
47
    /**
48
     * @throws \Doctrine\Common\Annotations\AnnotationException
49
     */
50
    public function __construct(
51
        Parser $parser,
52
        BuilderFactory $factory,
53
        Standard $printer
54
    ) {
55
        $this->parser = $parser;
56 33
        $this->factory = $factory;
57
        $this->printer = $printer;
58
        $this->reader = new AnnotationReader;
59
    }
60
61 33
    public function getMethods(\ReflectionClass $class, BindInterface $bind) : array
62 33
    {
63 33
        $bindingMethods = array_keys($bind->getBindings());
64 33
        $stmts = [];
65 33
        $methods = $class->getMethods();
66
        foreach ($methods as $method) {
67
            $this->assisted = $this->reader->getMethodAnnotation($method, AbstractAssisted::class);
68
            $isBindingMethod = in_array($method->name, $bindingMethods, true);
69
            /* @var $method \ReflectionMethod */
70
            if ($isBindingMethod && $method->isPublic()) {
71
                $stmts[] = $this->getMethod($method);
72
            }
73 17
        }
74
75 17
        return $stmts;
76 17
    }
77 17
78 17
    /**
79 16
     * Return method statement
80 16
     *
81
     * @return \PhpParser\Node\Stmt\ClassMethod
82 16
     */
83 16
    private function getMethod(\ReflectionMethod $method)
84
    {
85
        $methodStmt = $this->factory->method($method->name);
86
        $params = $method->getParameters();
87 17
        foreach ($params as $param) {
88
            $methodStmt = $this->getMethodStatement($param, $methodStmt);
89
        }
90
        $returnType = $method->getReturnType();
91
        if ($returnType instanceof \ReflectionType) {
0 ignored issues
show
Bug introduced by
The class ReflectionType does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
92
            $this->setReturnType($returnType, $methodStmt);
93
        }
94
        $methodInsideStatements = $this->getMethodInsideStatement($method);
95
        $methodStmt->addStmts($methodInsideStatements);
96
97 15
        return $this->addMethodDocComment($methodStmt, $method);
98
    }
99 15
100 15
    /**
101 15
     * Return parameter reflection
102 13
     */
103
    private function getMethodStatement(\ReflectionParameter $param, Method $methodStmt) : Method
104 15
    {
105 15
        /* @var $paramStmt Param */
106 5
        $paramStmt = $this->factory->param($param->name);
107
        /* @var $param \ReflectionParameter */
108 15
        $this->setParameterType($param, $paramStmt);
109 15
        $this->setDefault($param, $paramStmt);
110
        $methodStmt->addParam($paramStmt);
111 15
112
        return $methodStmt;
113
    }
114
115
    private function addMethodDocComment(Method $methodStmt, \ReflectionMethod $method) : ClassMethod
116
    {
117 13
        $node = $methodStmt->getNode();
118
        $docComment = $method->getDocComment();
119
        if ($docComment) {
120 13
            $node->setAttribute('comments', [new Doc($docComment)]);
121
        }
122 13
123 13
        return $node;
124 13
    }
125
126 13
    /**
127
     * @return \PhpParser\Node[]
128
     */
129 15
    private function getMethodInsideStatement(\ReflectionMethod $method) : array
130
    {
131 15
        $traverser = new NodeTraverser;
132 15
        $traverser->addVisitor(new AopTemplateConverter($method));
133 15
        $stmts = $this->getTemplateMethodNodeStmts();
134 6
135
        // traverse
136
        return $traverser->traverse($stmts);
137 15
    }
138
139
    private function setDefault(\ReflectionParameter $param, Param $paramStmt)
140
    {
141
        if ($param->isDefaultValueAvailable()) {
142
            $paramStmt->setDefault($param->getDefaultValue());
143 15
144
            return;
145 15
        }
146 15
        if ($this->assisted instanceof AbstractAssisted && in_array($param->name, $this->assisted->values, true)) {
147 15
            $paramStmt->setDefault(null);
148
        }
149
    }
150 15
151
    private function setParameterType(\ReflectionParameter $param, Param $paramStmt)
152 15
    {
153
        $type = $param->getType();
154
        if ($type == null) {
155 13
            return;
156
        }
157 13
        if ($param->isVariadic()) {
158 3
            $paramStmt->makeVariadic();
159
        }
160 3
        $paramString = (string) $param;
161
        $isNullableType = is_int(strpos($paramString, '<required>')) && is_int(strpos($paramString, 'or NULL'));
162 13
        $destType = $isNullableType ? new NullableType((string) $type) : (string) $type;
163 1
        if (is_string($destType) && class_exists($destType)) {
164
            $destType = '\\' . $destType;
165 13
        }
166
        $paramStmt->setTypeHint($destType);
0 ignored issues
show
Deprecated Code introduced by
The method PhpParser\Builder\Param::setTypeHint() has been deprecated with message: Use setType() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
167 13
    }
168
169 13
    private function setReturnType(\ReflectionType $returnType, Method $methodStmt)
170 13
    {
171 8
        $type = $returnType->allowsNull() ? new NullableType((string) $returnType) : (string) $returnType;
172
        $methodStmt->setReturnType($type);
173 7
    }
174 1
175
    /**
176 7
     * @return Node[]
177 7
     */
178 7
    private function getTemplateMethodNodeStmts() : array
179 7
    {
180 7
        $code = $this->getTemplateCode();
181
        $node = $this->parser->parse($code)[0];
182 5
        if (! $node instanceof Class_) {
183
            throw new \LogicException; // @codeCoverageIgnore
184 5
        }
185 5
        $methodNode = $node->getMethods()[0];
186 5
        if ($methodNode->stmts === null) {
187
            throw new \LogicException; // @codeCoverageIgnore
188
        }
189
190
        return $methodNode->stmts;
191 15
    }
192
193 15
    /**
194 15
     * Return CodeGenTemplate string
195 15
     *
196
     * Compiler takes only the statements in the method. Then create new inherit code with interceptors.
197
     *
198
     * @see http://paul-m-jones.com/archives/182
199 15
     * @see http://stackoverflow.com/questions/8343399/calling-a-function-with-explicit-parameters-vs-call-user-func-array
200 15
     * @see http://stackoverflow.com/questions/1796100/what-is-faster-many-ifs-or-else-if
201
     * @see http://stackoverflow.com/questions/2401478/why-is-faster-than-in-php
202
     */
203 15
    private function getTemplateCode() : string
204 15
    {
205
        return <<<'EOT'
206
<?php
207
class AopTemplate extends \Ray\Aop\FakeMock implements Ray\Aop\WeavedInterface
208 15
{
209
    /**
210
     * @var array
211
     *
212
     * [$methodName => [$interceptorA[]][]
213
     */
214
    public $bindings;
215
216
    /**
217
     * @var bool
218
     */
219
    private $isIntercepting = true;
220
221
    /**
222
     * Method Template
223
     *
224
     * @param mixed $a
225
     */
226
    public function templateMethod($a, $b)
227
    {
228
        if ($this->isIntercepting === false) {
229
            $this->isIntercepting = true;
230
231
            return parent::templateMethod($a, $b);
232
        }
233
234
        $this->isIntercepting = false;
235
        // invoke interceptor
236
        $result = (new \Ray\Aop\ReflectiveMethodInvocation($this, __FUNCTION__, [$a, $b], $this->bindings[__FUNCTION__]))->proceed();
237
        $this->isIntercepting = true;
238
239
        return $result;
240
    }
241
}
242
EOT;
243
    }
244
}
245