Passed
Push — master ( c26772...f9347c )
by Jean
02:16
created

DeferredCallChain::exceptionTrownFromMagicCall()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 18
rs 8.4444
cc 8
nc 32
nop 3
1
<?php
2
/**
3
 * DeferredCallChain
4
 *
5
 * @package php-deferred-callchain
6
 * @author  Jean Claveau
7
 */
8
namespace JClaveau\Async;
9
use       JClaveau\Async\Exceptions\BadTargetClassException;
10
use       JClaveau\Async\Exceptions\BadTargetTypeException;
11
use       JClaveau\Async\Exceptions\UndefinedTargetClassException;
12
use       JClaveau\Async\Exceptions\BadTargetInterfaceException;
13
use       JClaveau\Async\Exceptions\TargetAlreadyDefinedException;
14
use       BadMethodCallException;
15
16
/**
17
 * This class stores an arbitrary stack of calls (methods or array entries access)
18
 * that will be callable on any future variable.
19
 */
20
class DeferredCallChain implements \JsonSerializable, \ArrayAccess
21
{
22
    use \JClaveau\Traits\Fluent\New_;
23
    use FunctionCallTrait;
24
    
25
    /** @var array $stack The stack of deferred calls */
26
    protected $stack = [];
27
28
    /** @var mixed $expectedTarget The stack of deferred calls */
29
    protected $expectedTarget;
30
31
    /**
32
     * Constructor 
33
     * 
34
     * @param string $key The entry to acces
35
     */
36
    public function __construct($class_type_or_instance=null)
37
    {
38
        if ($class_type_or_instance) {
39
            $this->expectedTarget = $class_type_or_instance;
40
        }
41
    }
42
43
    /**
44
     * ArrayAccess interface
45
     *
46
     * @param string $key The entry to acces
47
     */
48
    public function &offsetGet($key)
49
    {
50
        $this->stack[] = [
51
            'entry' => $key,
52
        ];
53
54
        return $this;
55
    }
56
57
    /**
58
     * Stores any call in the the stack.
59
     *
60
     * @param  string $method
61
     * @param  array  $arguments
62
     *
63
     * @return $this
64
     */
65
    public final function __call($method, array $arguments)
66
    {
67
        $this->stack[] = [
68
            'method'    => $method,
69
            'arguments' => $arguments,
70
        ];
71
72
        return $this;
73
    }
74
75
    /**
76
     * For implementing JsonSerializable interface.
77
     *
78
     * @see https://secure.php.net/manual/en/jsonserializable.jsonserialize.php
79
     */
80
    public function jsonSerialize()
81
    {
82
        return $this->stack;
83
    }
84
85
    /**
86
     * Outputs the PHP code producing the current call chain while it's casted
87
     * as a string.
88
     *
89
     * @return string The PHP code corresponding to this call chain
90
     */
91
    public function __toString()
92
    {
93
        $string = '(new ' . get_called_class();
94
        if (is_string($this->expectedTarget)) {
95
            $string .= '(' . var_export($this->expectedTarget, true) . ')';
96
        }
97
        elseif (is_object($this->expectedTarget)) {
98
            $string .= '( ' . get_class($this->expectedTarget) . '#' . spl_object_id($this->expectedTarget) . ' )';
99
        }
100
        $string .= ')';
101
102
        foreach ($this->stack as $i => $call) {
103
            if (isset($call['method'])) {
104
                $string .= '->';
105
                $string .= $call['method'].'(';
106
                $string .= implode(', ', array_map(function($argument) {
107
                    return var_export($argument, true);
108
                }, $call['arguments']));
109
                $string .= ')';
110
            }
111
            else {
112
                $string .= '[' . var_export($call['entry'], true) . ']';
113
            }
114
        }
115
116
        return $string;
117
    }
118
119
    /**
120
     * Checks that the provided target matches the type/class/interface
121
     * given during construction.
122
     * 
123
     * @param  mixed $target
124
     * @return mixed $target Checked
125
     */
126
    protected function checkTarget($target)
127
    {
128
        if (is_object($this->expectedTarget)) {
129
            if ($target) {
130
                throw new TargetAlreadyDefinedException($this, $this->expectedTarget, $target);
131
            }
132
            
133
            $out = $this->expectedTarget;
134
        }
135
        elseif (is_string($this->expectedTarget)) {
136
            if (class_exists($this->expectedTarget)) {
137
                if (! $target instanceof $this->expectedTarget) {
138
                    throw new BadTargetClassException($this, $this->expectedTarget, $target);
139
                }
140
            }
141
            elseif (interface_exists($this->expectedTarget)) {
142
                if (! $target instanceof $this->expectedTarget) {
143
                    throw new BadTargetInterfaceException($this, $this->expectedTarget, $target);
144
                }
145
            }
146
            elseif (type_exists($this->expectedTarget)) {
147
                if (gettype($target) != $this->expectedTarget) {
148
                    throw new BadTargetTypeException($this, $this->expectedTarget, $target);
149
                }
150
            }
151
            else {
152
                throw new UndefinedTargetClassException($this, $this->expectedTarget);
153
            }
154
            
155
            $out = $target;
156
        }
157
        else {
158
            $out = $target;
159
        }
160
        
161
        return $out;
162
    }
163
164
    /**
165
     * Calling a method coded inside a magic __call can produce a 
166
     * BadMethodCallException and thus not be a callable.
167
     * 
168
     * @param mixed  $current_chained_subject
169
     * @param string $method_name
170
     * @param array  $arguments
171
     * 
172
     * @return bool $is_called
173
     */
174
    protected function checkMethodIsReallyCallable(
175
        &$current_chained_subject, 
176
        $method_name,
177
        $arguments
178
    ) {
179
        $is_called = true;
180
        try {
181
            $current_chained_subject = call_user_func_array(
182
                [$current_chained_subject, $method_name], 
183
                $arguments
184
            );
185
        }
186
        catch (\BadMethodCallException $e) {
187
            if ($this->exceptionTrownFromMagicCall(
188
                $e->getTrace(),
189
                $current_chained_subject,
190
                $method_name
191
            )) {
192
                $is_called = false;
193
            }
194
            else {
195
                throw $e;
196
            }
197
        }
198
        
199
        return $is_called;
200
    }
201
202
    /**
203
     * Checks if the exception having $trace is thrown from à __call
204
     * magic method.
205
     * 
206
     * @return bool $is_called
207
     */
208
    protected function exceptionTrownFromMagicCall(
209
        $trace, 
210
        $current_chained_subject,
211
        $method_name
212
    ) {
213
        $call_user_func_array_position = PHP_VERSION_ID < 70000 ? 2 : 1;
214
        
215
        return  $trace[0]['function'] == '__call'
216
            &&  $trace[0]['class'] == get_class($current_chained_subject)
217
            && (
218
                    $trace[$call_user_func_array_position]['file'] == __FILE__
219
                &&  $trace[$call_user_func_array_position]['function'] == 'call_user_func_array'
220
            )
221
            && (
222
                // The magic method call doesn't exist in the stack with PHP 7
223
                    PHP_VERSION_ID < 70000
224
                &&  $trace[1]['function'] == $method_name
225
                &&  $trace[1]['class'] == get_class($current_chained_subject)
226
            )
227
            ;
228
    }
229
230
    /**
231
     * Invoking the instance produces the call of the stack
232
     *
233
     * @param  mixed $target The target to apply the callchain on
234
     * @return mixde The value returned once the call chain is called uppon $target
0 ignored issues
show
Bug introduced by
The type JClaveau\Async\mixde was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
235
     */
236
    public function __invoke($target=null)
237
    {
238
        $out = $this->checkTarget($target);
239
        
240
        foreach ($this->stack as $i => $call) {
241
            $is_called = false;
242
            try {
243
                if (isset($call['method'])) {
244
                    if (is_callable([$out, $call['method']])) {
245
                        $is_called = $this->checkMethodIsReallyCallable(
246
                            $out,
247
                            $call['method'],
248
                            $call['arguments']
249
                        );
250
                    }
251
                    
252
                    if (! $is_called && is_callable($call['method'])) {
253
                        $arguments = $this->prepareArgs($call['arguments'], $out);
254
                        $out = call_user_func_array($call['method'], $arguments);
255
                        $is_called = true;
256
                    }
257
                    
258
                    if (! $is_called) {
259
                        throw new \BadMethodCallException(
260
                            $call['method'] . "() is neither a method of " . get_class($out)
261
                            . " nor a function"
262
                        );
263
                    }
264
                }
265
                else {
266
                    $out = $out[ $call['entry'] ];
267
                }
268
            }
269
            catch (\Exception $e) {
270
                // Throw $e with the good stack (usage exception)
271
                throw $e;
272
            }
273
        }
274
275
        return $out;
276
    }
277
278
    /**
279
     * Unused part of the ArrayAccess interface
280
     *
281
     * @param  $offset
282
     * @param  $value
283
     * @throws \BadMethodCallException
284
     */
285
    public function offsetSet($offset, $value)
286
    {
287
        throw new BadMethodCallException(
288
            "not implemented"
289
        );
290
    }
291
292
    /**
293
     * Unused part of the ArrayAccess interface
294
     *
295
     * @param  $offset
296
     * @throws \BadMethodCallException
297
     */
298
    public function offsetExists($offset)
299
    {
300
        throw new BadMethodCallException(
301
            "not implemented"
302
        );
303
    }
304
305
    /**
306
     * Unused part of the ArrayAccess interface
307
     *
308
     * @param  $offset
309
     * @throws \BadMethodCallException
310
     */
311
    public function offsetUnset($offset)
312
    {
313
        throw new BadMethodCallException(
314
            "not implemented"
315
        );
316
    }
317
318
    /**/
319
}
320