Completed
Push — namespace ( 973949 )
by Akihito
02:15
created

CodeGenMethod::setParameterType()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 8.8333
c 0
b 0
f 0
cc 7
nc 17
nop 2
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
        $this->factory = $factory;
57
        $this->printer = $printer;
58
        $this->reader = new AnnotationReader;
59
    }
60
61
    public function getMethods(\ReflectionClass $class, BindInterface $bind) : array
62
    {
63
        $bindingMethods = array_keys($bind->getBindings());
64
        $stmts = [];
65
        $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
        }
74
75
        return $stmts;
76
    }
77
78
    /**
79
     * Return method statement
80
     *
81
     * @return \PhpParser\Node\Stmt\ClassMethod
82
     */
83
    private function getMethod(\ReflectionMethod $method)
84
    {
85
        $methodStmt = $this->factory->method($method->name);
86
        $params = $method->getParameters();
87
        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
        return $this->addMethodDocComment($methodStmt, $method);
98
    }
99
100
    /**
101
     * Return parameter reflection
102
     */
103
    private function getMethodStatement(\ReflectionParameter $param, Method $methodStmt) : Method
104
    {
105
        /* @var $paramStmt Param */
106
        $paramStmt = $this->factory->param($param->name);
107
        /* @var $param \ReflectionParameter */
108
        $this->setParameterType($param, $paramStmt);
109
        $this->setDefault($param, $paramStmt);
110
        $methodStmt->addParam($paramStmt);
111
112
        return $methodStmt;
113
    }
114
115
    private function addMethodDocComment(Method $methodStmt, \ReflectionMethod $method) : ClassMethod
116
    {
117
        $node = $methodStmt->getNode();
118
        $docComment = $method->getDocComment();
119
        if ($docComment) {
120
            $node->setAttribute('comments', [new Doc($docComment)]);
121
        }
122
123
        return $node;
124
    }
125
126
    /**
127
     * @return \PhpParser\Node[]
128
     */
129
    private function getMethodInsideStatement(\ReflectionMethod $method) : array
130
    {
131
        $traverser = new NodeTraverser;
132
        $traverser->addVisitor(new AopTemplateConverter($method));
133
        $stmts = $this->getTemplateMethodNodeStmts();
134
135
        // traverse
136
        return $traverser->traverse($stmts);
137
    }
138
139
    private function setDefault(\ReflectionParameter $param, Param $paramStmt)
140
    {
141
        if ($param->isDefaultValueAvailable()) {
142
            $paramStmt->setDefault($param->getDefaultValue());
143
144
            return;
145
        }
146
        if ($this->assisted instanceof AbstractAssisted && in_array($param->name, $this->assisted->values, true)) {
147
            $paramStmt->setDefault(null);
148
        }
149
    }
150
151
    private function setParameterType(\ReflectionParameter $param, Param $paramStmt)
152
    {
153
        $type = $param->getType();
154
        if ($type == null) {
155
            return;
156
        }
157
        if ($param->isVariadic()) {
158
            $paramStmt->makeVariadic();
159
        }
160
        $paramString = (string) $param;
161
        $isNullableType = is_int(strpos($paramString, '<required>')) && is_int(strpos($paramString, 'or NULL'));
162
        $destType = $isNullableType ? new NullableType((string) $type) : (string) $type;
163
        if (is_string($destType) && class_exists($destType)) {
164
            $destType = '\\' . $destType;
165
        }
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
    }
168
169
    private function setReturnType(\ReflectionType $returnType, Method $methodStmt)
170
    {
171
        $type = $returnType->allowsNull() ? new NullableType((string) $returnType) : (string) $returnType;
172
        $methodStmt->setReturnType($type);
173
    }
174
175
    /**
176
     * @return Node[]
177
     */
178
    private function getTemplateMethodNodeStmts() : array
179
    {
180
        $code = $this->getTemplateCode();
181
        $node = $this->parser->parse($code)[0];
182
        if (! $node instanceof Class_) {
183
            throw new \LogicException; // @codeCoverageIgnore
184
        }
185
        $methodNode = $node->getMethods()[0];
186
        if ($methodNode->stmts === null) {
187
            throw new \LogicException; // @codeCoverageIgnore
188
        }
189
190
        return $methodNode->stmts;
191
    }
192
193
    /**
194
     * Return CodeGenTemplate string
195
     *
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
     * @see http://stackoverflow.com/questions/8343399/calling-a-function-with-explicit-parameters-vs-call-user-func-array
200
     * @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
    private function getTemplateCode() : string
204
    {
205
        return <<<'EOT'
206
<?php
207
class AopTemplate extends \Ray\Aop\FakeMock implements Ray\Aop\WeavedInterface
208
{
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