Completed
Push — travis ( f9e5b1...d873c4 )
by Akihito
01:30
created

CodeGenMethod::getTemplateCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 9.264
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\Aop;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use PhpParser\Builder\Method;
9
use PhpParser\Builder\Param;
10
use PhpParser\BuilderFactory;
11
use PhpParser\Comment\Doc;
12
use PhpParser\Node;
13
use PhpParser\Node\NullableType;
14
use PhpParser\Node\Stmt\Class_;
15
use PhpParser\Node\Stmt\ClassMethod;
16
use PhpParser\NodeTraverser;
17
use PhpParser\Parser;
18
use PhpParser\PrettyPrinter\Standard;
19
use Ray\Aop\Annotation\AbstractAssisted;
20
21
final class CodeGenMethod
22
{
23
    /**
24
     * @var \PhpParser\Parser
25
     */
26
    private $parser;
27
28
    /**
29
     * @var \PhpParser\BuilderFactory
30
     */
31
    private $factory;
32
33
    /**
34
     * @var \PhpParser\PrettyPrinter\Standard
35
     */
36
    private $printer;
37
38
    private $reader;
39
40
    /**
41
     * @var AbstractAssisted
42
     */
43
    private $assisted;
44
45
    public function __construct(
46
        Parser $parser,
47
        BuilderFactory $factory,
48
        Standard $printer
49
    ) {
50
        $this->parser = $parser;
51
        $this->factory = $factory;
52
        $this->printer = $printer;
53
        $this->reader = new AnnotationReader;
54
    }
55
56
    public function getMethods(\ReflectionClass $class, BindInterface $bind) : array
57
    {
58
        $bindingMethods = array_keys($bind->getBindings());
59
        $stmts = [];
60
        $methods = $class->getMethods();
61
        foreach ($methods as $method) {
62
            $this->assisted = $this->reader->getMethodAnnotation($method, AbstractAssisted::class);
63
            $isBindingMethod = in_array($method->name, $bindingMethods, true);
64
            /* @var $method \ReflectionMethod */
65
            if ($isBindingMethod && $method->isPublic()) {
66
                $stmts[] = $this->getMethod($method);
67
            }
68
        }
69
70
        return $stmts;
71
    }
72
73
    /**
74
     * Return method statement
75
     *
76
     * @return \PhpParser\Node\Stmt\ClassMethod
77
     */
78
    private function getMethod(\ReflectionMethod $method)
79
    {
80
        $methodStmt = $this->factory->method($method->name);
81
        $params = $method->getParameters();
82
        foreach ($params as $param) {
83
            $methodStmt = $this->getMethodStatement($param, $methodStmt);
84
        }
85
        $returnType = $method->getReturnType();
86
        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...
87
            $this->setReturnType($returnType, $methodStmt);
88
        }
89
        $methodInsideStatements = $this->getMethodInsideStatement($method);
90
        $methodStmt->addStmts($methodInsideStatements);
91
92
        return $this->addMethodDocComment($methodStmt, $method);
93
    }
94
95
    /**
96
     * Return parameter reflection
97
     */
98
    private function getMethodStatement(\ReflectionParameter $param, Method $methodStmt) : Method
99
    {
100
        /* @var $paramStmt Param */
101
        $paramStmt = $this->factory->param($param->name);
102
        /* @var $param \ReflectionParameter */
103
        $this->setParameterType($param, $paramStmt);
104
        $this->setDefault($param, $paramStmt);
105
        $methodStmt->addParam($paramStmt);
106
107
        return $methodStmt;
108
    }
109
110
    private function addMethodDocComment(Method $methodStmt, \ReflectionMethod $method) : ClassMethod
111
    {
112
        $node = $methodStmt->getNode();
113
        $docComment = $method->getDocComment();
114
        if ($docComment) {
115
            $node->setAttribute('comments', [new Doc($docComment)]);
116
        }
117
118
        return $node;
119
    }
120
121
    /**
122
     * @return \PhpParser\Node[]
123
     */
124
    private function getMethodInsideStatement(\ReflectionMethod $method) : array
125
    {
126
        $traverser = new NodeTraverser;
127
        $traverser->addVisitor(new AopTemplateConverter($method));
128
        $stmts = $this->getTemplateMethodNodeStmts();
129
130
        // traverse
131
        return $traverser->traverse($stmts);
132
    }
133
134
    private function setDefault(\ReflectionParameter $param, Param $paramStmt)
135
    {
136
        if ($param->isDefaultValueAvailable()) {
137
            $paramStmt->setDefault($param->getDefaultValue());
138
139
            return;
140
        }
141
        if ($this->assisted instanceof AbstractAssisted && in_array($param->name, $this->assisted->values, true)) {
142
            $paramStmt->setDefault(null);
143
        }
144
    }
145
146
    private function setParameterType(\ReflectionParameter $param, Param $paramStmt)
147
    {
148
        $type = $param->getType();
149
        if ($type == null) {
150
            return;
151
        }
152
        if ($param->isVariadic()) {
153
            $paramStmt->makeVariadic();
154
        }
155
        $paramString = (string) $param;
156
        $isNullableType = is_int(strpos($paramString, '<required>')) && is_int(strpos($paramString, 'or NULL'));
157
        $destType = $isNullableType ? new NullableType((string) $type) : (string) $type;
158
        $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...
159
    }
160
161
    private function setReturnType(\ReflectionType $returnType, Method $methodStmt)
162
    {
163
        $type = $returnType->allowsNull() ? new NullableType((string) $returnType) : (string) $returnType;
164
        $methodStmt->setReturnType($type);
165
    }
166
167
    /**
168
     * @return Node[]
169
     */
170
    private function getTemplateMethodNodeStmts() : array
171
    {
172
        $code = $this->getTemplateCode();
173
        $node = $this->parser->parse($code)[0];
174
        if (! $node instanceof Class_) {
175
            throw new \LogicException; // @codeCoverageIgnore
176
        }
177
        $methodNode = $node->getMethods()[0];
178
        if ($methodNode->stmts === null) {
179
            throw new \LogicException; // @codeCoverageIgnore
180
        }
181
182
        return $methodNode->stmts;
183
    }
184
185
    /**
186
     * Return CodeGenTemplate string
187
     *
188
     * Compiler takes only the statements in the method. Then create new inherit code with interceptors.
189
     *
190
     * @see http://paul-m-jones.com/archives/182
191
     * @see http://stackoverflow.com/questions/8343399/calling-a-function-with-explicit-parameters-vs-call-user-func-array
192
     * @see http://stackoverflow.com/questions/1796100/what-is-faster-many-ifs-or-else-if
193
     * @see http://stackoverflow.com/questions/2401478/why-is-faster-than-in-php
194
     */
195
    private function getTemplateCode() : string
196
    {
197
        return <<<'EOT'
198
<?php
199
class AopTemplate extends \Ray\Aop\FakeMock implements Ray\Aop\WeavedInterface
200
{
201
    /**
202
     * @var array
203
     *
204
     * [$methodName => [$interceptorA[]][]
205
     */
206
    public $bindings;
207
208
    /**
209
     * @var bool
210
     */
211
    private $isIntercepting = true;
212
213
    /**
214
     * Method Template
215
     *
216
     * @param mixed $a
217
     */
218
    public function templateMethod($a, $b)
219
    {
220
        if ($this->isIntercepting === false) {
221
            $this->isIntercepting = true;
222
223
            return parent::templateMethod($a, $b);
224
        }
225
226
        $this->isIntercepting = false;
227
        // invoke interceptor
228
        $result = (new \Ray\Aop\ReflectiveMethodInvocation($this, __FUNCTION__, [$a, $b], $this->bindings[__FUNCTION__]))->proceed();
229
        $this->isIntercepting = true;
230
231
        return $result;
232
    }
233
}
234
EOT;
235
    }
236
}
237