Passed
Push — master ( 13b148...e0ea5e )
by Maxim
02:03
created

CallProcessor::calculateConditions()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
c 0
b 0
f 0
rs 8.8571
cc 5
eloc 9
nc 6
nop 1
1
<?php
2
/**
3
 * This file is a part of "Axessors" library.
4
 *
5
 * @author <[email protected]>
6
 * @license GPL
7
 */
8
9
namespace NoOne4rever\Axessors;
10
11
use NoOne4rever\Axessors\Exceptions\AxessorsError;
12
use NoOne4rever\Axessors\Exceptions\InternalError;
13
use NoOne4rever\Axessors\Exceptions\OopError;
14
use NoOne4rever\Axessors\Exceptions\TypeError;
15
16
/**
17
 * Class CallProcessor
18
 *
19
 * Handles method call.
20
 *
21
 * @package NoOne4rever\Axessors
22
 */
23
class CallProcessor
24
{
25
    /** @var string method name */
26
    private $method;
27
    /** @var mixed|null an object */
28
    private $object;
29
    /** @var mixed call stack */
30
    private $backtrace;
31
    /** @var bool accessor mode */
32
    private $mode;
33
    /** @var string class name */
34
    private $class;
35
    /** @var PropertyData property data */
36
    private $propertyData;
37
    /** @var \ReflectionClass class data */
38
    private $reflection;
39
40
    /**
41
     * CallProcessor constructor.
42
     *
43
     * @param array $backtrace contains call stack
44
     * @param mixed|null $object an object the method is called on (might not be given, if the method is static)
45
     */
46
    public function __construct(array $backtrace, $object = null)
47
    {
48
        $this->object = $object;
49
        $this->class = $backtrace[1]['class'] ?? $backtrace[0]['class'];
50
        $this->backtrace = $backtrace[0];
51
    }
52
53
    /**
54
     * General function of calling the method.
55
     *
56
     * Processes method's name to extract property name and checks if the method is accessible.
57
     *
58
     * @param array $args the arguments of the method called
59
     * @param string $method the name of the method called
60
     * @return mixed return value of the method called
61
     * @throws AxessorsError is thrown when Axessors method not found
62
     * @throws OopError is thrown when the method is not accessible
63
     */
64
    public function call(array $args, string $method)
65
    {
66
        $data = Data::getInstance();
67
        $classData = $data->getClass($this->backtrace['class']);
68
        $this->method = $method;
69
        $this->reflection = $classData->reflection;
70
        while (true) {
71
            foreach ($classData->getAllProperties() as $propertyData) {
72
                foreach ($propertyData->getMethods() as $accessModifier => $methods) {
73
                    foreach ($methods as $method) {
74
                        if ($this->method == $method) {
75
                            if ($this->isAccessible($accessModifier, $classData->reflection)) {
76
                                return $this->run($propertyData, $args);
77
                            } else {
78
                                throw new OopError("method {$this->backtrace['class']}::{$this->method}() is not accessible there");
79
                            }
80
                        }
81
                    }
82
                }
83
            }
84
            $reflection = $classData->reflection->getParentClass();
85
            if ($reflection === false) {
86
                break;
87
            }
88
            try {
89
                $classData = $data->getClass($reflection->name);
90
            } catch (InternalError $error) {
91
                break;
92
            }
93
        }
94
        throw new AxessorsError("method {$this->backtrace['class']}::{$this->method}() not found");
95
    }
96
97
    /**
98
     * Emulates execution of the method.
99
     *
100
     * @param PropertyData $propertyData data of the property
101
     * @param array $args the arguments of the method called
102
     * @return mixed return value of the method called
103
     * @throws AxessorsError if conditions for executing an accessor did not pass
104
     * @throws AxessorsError if Axessors method not found
105
     */
106
    private function run(PropertyData $propertyData, array $args)
107
    {
108
        $prefix = substr($this->method, 0, 3);
109
        $this->propertyData = $propertyData;
110
        if ($prefix == 'get') {
111
            $this->mode = true;
112
            $propertyData->reflection->setAccessible(true);
113
            $value = is_null($this->object) ? $propertyData->reflection->getValue() : $propertyData->reflection->getValue($this->object);
114
            $this->checkType($this->propertyData->getTypeTree(), $value);
115
            $propertyData->reflection->setAccessible(false);
116
            $conditionsSuit = new ConditionsSuit(ConditionsSuit::GETTER_MODE, $this->propertyData, $this->class, $this->method, $this->object);
117
            if ($conditionsSuit->processConditions($value)) {
118
                $value = $this->executeHandlers($value);
119
                return $value;
120
            } else {
121
                throw new AxessorsError("conditions for {$this->backtrace['class']}::{$this->method}() did not pass");
122
            }
123
        } elseif ($prefix == 'set') {
124
            $this->mode = false;
125
            $this->checkType($this->propertyData->getTypeTree(), $args[0]);
126
            $conditionsSuit = new ConditionsSuit(ConditionsSuit::SETTER_MODE, $this->propertyData, $this->class, $this->method, $this->object);
127
            if ($conditionsSuit->processConditions($args[0])) {
128
                $propertyData->reflection->setAccessible(true);
129
                $value = $this->executeHandlers($args[0]);
130
                is_null($this->object) ? $propertyData->reflection->setValue($value) : $propertyData->reflection->setValue($this->object,
131
                    $value);
132
                $propertyData->reflection->setAccessible(false);
133
                return;
134
            } else {
135
                throw new AxessorsError("conditions for {$this->backtrace['class']}::{$this->method}() did not pass");
136
            }
137
        } else {
138
            $this->method = str_replace(ucfirst($this->propertyData->getName()), 'PROPERTY', $this->method);
139
            foreach ($this->propertyData->getTypeTree() as $type => $subType) {
140
                $type = is_int($type) ? $subType : $type;
141
                $reflection = new \ReflectionClass($type);
142
                foreach ($reflection->getMethods() as $method) {
143
                    if (preg_match('{^m_(in|out)_}',
144
                            $method->name) && $method->isStatic() && $method->isPublic() && !$method->isAbstract()) {
145
                        if ($method->name == "m_in_$this->method") {
146
                            $propertyData->reflection->setAccessible(true);
147
                            $value = $propertyData->reflection->getValue($this->object);
148
                            $this->checkType($this->propertyData->getTypeTree(), $value);
149
                            // add support for static properties
150
                            $propertyData->reflection->setValue(
151
                                $this->object, call_user_func(
152
                                    [$type, "m_in_$this->method"], $propertyData->reflection->getValue(
153
                                    $this->object
154
                                ), $args
155
                                )
156
                            );
157
                            $propertyData->reflection->setAccessible(false);
158
                            return;
159
                        } elseif ($method->name == "m_out_$this->method") {
160
                            $propertyData->reflection->setAccessible(true);
161
                            $value = $propertyData->reflection->getValue($this->object);
162
                            $this->checkType($this->propertyData->getTypeTree(), $value);
163
                            $result = call_user_func([$type, "m_out_$this->method"], $value, $args);
164
                            $propertyData->reflection->setAccessible(false);
165
                            return $result;
166
                        }
167
                    }
168
                }
169
            }
170
            throw new AxessorsError("method {$this->backtrace['class']}::{$this->method}() not found");
171
        }
172
    }
173
174
    /**
175
     * Checks if the type of new property's value is correct.
176
     *
177
     * @param array $typeTree all possible types
178
     * @param $var mixed new value of the property
179
     * @throws TypeError if the type of new property's value does not match the type defined in Axessors comment
180
     */
181
    private function checkType(array $typeTree, $var): void
182
    {
183
        foreach ($typeTree as $type => $subType) {
184
            if (is_int($type)) {
185
                if ($var instanceof $subType || ((new \ReflectionClass($subType))->hasMethod('is') && call_user_func([
186
                            $subType,
187
                            'is'
188
                        ], $var))) {
189
                    return;
190
                }
191
            } else {
192
                if (is_iterable($var) && ($var instanceof $type || ((new \ReflectionClass($type))->hasMethod('is') && call_user_func([
193
                                $type,
194
                                'is'
195
                            ], $var)))) {
196
                    foreach ($var as $subVar) {
197
                        $this->checkType($subType, $subVar);
198
                    }
199
                    return;
200
                }
201
            }
202
        }
203
        throw new TypeError("not a valid type of {$this->backtrace['class']}::\${$this->propertyData->getName()}");
204
    }
205
206
    /**
207
     * Executes handlers defined in the Axessors comment.
208
     *
209
     * @param $value mixed value of the property
210
     * @return mixed new value of the property
211
     * @throws OopError if the property does not have one of the handlers defined in the Axessors comment
212
     */
213
    private function executeHandlers($value)
214
    {
215
        $handlers = $this->mode ? $this->propertyData->getOutputHandlers() : $this->propertyData->getInputHandlers();
216
        foreach ($handlers as $handler) {
217
            if (strpos($handler, '`') !== false) {
218
                $handler = str_replace('\\`', '`', substr($handler, 1, strlen($handler) - 2));
219
                if (is_null($this->object)) {
220
                    $value = call_user_func("{$this->reflection->name}::__axessorsExecute", $handler, $value, false);
221
                } else {
222
                    $value = $this->object->__axessorsExecute($handler, $value, false);
223
                }
224
            } else {
225
                foreach ($this->propertyData->getTypeTree() as $type => $subType) {
226
                    $reflection = new \ReflectionClass('\Axessors\Types\\' . is_int($type) ? $subType : $type);
227
                    foreach ($reflection->getMethods() as $method) {
228
                        $isAccessible = $method->isPublic() && $method->isStatic() && !$method->isAbstract();
229
                        $isThat = call_user_func([$reflection->name, 'is'], $value);
230
                        if ($isAccessible && $isThat && "h_$handler" == $method->name) {
231
                            $value = call_user_func([$reflection->name, $method->name], $value, false);
232
                            continue 3;
233
                        } elseif ($isAccessible && "h_$handler" == $method->name && !$isThat) {
234
                            continue 3;
235
                        }
236
                    }
237
                }
238
                throw new OopError("property {$this->backtrace['class']}::\${$this->propertyData->getName()} does not have handler \"$handler\"");
239
            }
240
        }
241
        return $value;
242
    }
243
244
    /**
245
     * Checks if the method is accessible.
246
     *
247
     * @param string $accessModifier access modifier
248
     * @param \ReflectionClass $reflection class data
249
     * @return bool result of the checkout
250
     * @throws InternalError if not a valid it's impossible to check accessibility.
251
     */
252
    private function isAccessible(string $accessModifier, \ReflectionClass $reflection): bool
253
    {
254
        if ($accessModifier == 'public') {
255
            return true;
256
        }
257
        $isThis = $reflection->name == $this->backtrace['class'];
258
        $inThisFile = $this->backtrace['file'] == $reflection->getFileName() && $this->in($reflection);
259
        if ($accessModifier == 'private') {
260
            return $isThis && $inThisFile;
261
        }
262
        $isThisBranch = is_subclass_of($this->class, $reflection->name) || is_subclass_of($reflection->name,
263
                $this->class);
264
        $reflection = new \ReflectionClass($this->class);
265
        $inBranchFile = $this->backtrace['file'] == $reflection->getFileName() && $this->in($reflection);
266
        if ($accessModifier == 'protected') {
267
            return ($isThis && $inThisFile) || ($isThis && $inBranchFile) || ($isThisBranch && $inBranchFile);
268
        }
269
        throw new InternalError('not a valid access modifier given');
270
    }
271
272
    /**
273
     * Checks if the method called in right place and it is accessible there.
274
     *
275
     * @param \ReflectionClass $reflection class data
276
     * @return bool result of the checkout
277
     */
278
    private function in(\ReflectionClass $reflection): bool
279
    {
280
        return $reflection->getStartLine() <= $this->backtrace['line'] && $reflection->getEndLine() >= $this->backtrace['line'];
281
    }
282
}
283