Passed
Push — master ( c6a215...6472b7 )
by Jean
01:59
created

DeferredCallChain::exceptionTrownFromMagicCall()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 15
rs 8.8333
cc 7
nc 18
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       JClaveau\VisibilityViolator\VisibilityViolator;
15
use       BadMethodCallException;
16
17
/**
18
 * This class stores an arbitrary stack of calls (methods or array entries access)
19
 * that will be callable on any future variable.
20
 */
21
class DeferredCallChain implements \JsonSerializable, \ArrayAccess
22
{
23
    use \JClaveau\Traits\Fluent\New_;
24
    use FunctionCallTrait;
25
    use ArrayAccessTrait;
26
    use ExportTrait;
27
    
28
    /** @var array $stack The stack of deferred calls */
29
    protected $stack = [];
30
31
    /** @var mixed $expectedTarget The stack of deferred calls */
32
    protected $expectedTarget;
33
34
    /**
35
     * Constructor 
36
     * 
37
     * @param string $class_type_interface_or_instance The expected target class/type/interface/instance
38
     */
39
    public function __construct($class_type_interface_or_instance=null)
40
    {
41
        if ($class_type_interface_or_instance) {
42
            $this->expectedTarget = $class_type_interface_or_instance;
43
        }
44
    }
45
46
    /**
47
     * Stores any call in the the stack.
48
     *
49
     * @param  string $method
50
     * @param  array  $arguments
51
     *
52
     * @return $this
53
     */
54
    public final function __call($method, array $arguments)
55
    {
56
        $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
57
        
58
        $this->stack[] = [
59
            'method'    => $method,
60
            'arguments' => $arguments,
61
            'file'      => isset($caller['file']) ? $caller['file'] : null,
62
            'line'      => isset($caller['line']) ? $caller['line'] : null,
63
        ];
64
65
        return $this;
66
    }
67
68
    /**
69
     * ArrayAccess interface
70
     *
71
     * @param string $key The entry to acces
72
     */
73
    public function &offsetGet($key)
74
    {
75
        $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
76
        
77
        $this->stack[] = [
78
            'entry' => $key,
79
            'file'  => isset($caller['file']) ? $caller['file'] : null,
80
            'line'  => isset($caller['line']) ? $caller['line'] : null,
81
        ];
82
83
        return $this;
84
    }
85
86
    /**
87
     * Checks that the provided target matches the type/class/interface
88
     * given during construction.
89
     * 
90
     * @param  mixed $target
91
     * @return mixed $target Checked
92
     */
93
    protected function checkTarget($target)
94
    {
95
        if (is_object($this->expectedTarget)) {
96
            if ($target) {
97
                throw new TargetAlreadyDefinedException($this, $this->expectedTarget, $target);
98
            }
99
            
100
            $out = $this->expectedTarget;
101
        }
102
        elseif (is_string($this->expectedTarget)) {
103
            if (class_exists($this->expectedTarget)) {
104
                if (! $target instanceof $this->expectedTarget) {
105
                    throw new BadTargetClassException($this, $this->expectedTarget, $target);
106
                }
107
            }
108
            elseif (interface_exists($this->expectedTarget)) {
109
                if (! $target instanceof $this->expectedTarget) {
110
                    throw new BadTargetInterfaceException($this, $this->expectedTarget, $target);
111
                }
112
            }
113
            elseif (type_exists($this->expectedTarget)) {
114
                if (gettype($target) != $this->expectedTarget) {
115
                    throw new BadTargetTypeException($this, $this->expectedTarget, $target);
116
                }
117
            }
118
            else {
119
                throw new UndefinedTargetClassException($this, $this->expectedTarget);
120
            }
121
            
122
            $out = $target;
123
        }
124
        else {
125
            $out = $target;
126
        }
127
        
128
        return $out;
129
    }
130
131
    /**
132
     * Calling a method coded inside a magic __call can produce a 
133
     * BadMethodCallException and thus not be a callable.
134
     * 
135
     * @param mixed  $current_chained_subject
136
     * @param string $method_name
137
     * @param array  $arguments
138
     * 
139
     * @return bool $is_called
140
     */
141
    protected function checkMethodIsReallyCallable(
142
        $method_type,
143
        &$current_chained_subject, 
144
        $method_name,
145
        $arguments
146
    ) {
147
        $is_called = true;
148
        try {
149
            if ($method_type == '->') {
150
                $callable = [$current_chained_subject, $method_name];
151
            }
152
            elseif ($method_type == '::') {
153
                if (is_object($current_chained_subject)) {
154
                    $class = get_class($current_chained_subject);
155
                }
156
                elseif (is_string($current_chained_subject)) {
157
                    $class = $current_chained_subject;
158
                }
159
                
160
                $callable = $class .'::'. $method_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $class does not seem to be defined for all execution paths leading up to this point.
Loading history...
161
            }
162
            
163
            $current_chained_subject = call_user_func_array(
164
                $callable, 
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $callable does not seem to be defined for all execution paths leading up to this point.
Loading history...
165
                $arguments
166
            );
167
        }
168
        catch (\BadMethodCallException $e) {
169
            if ($this->exceptionTrownFromMagicCall(
170
                $e->getTrace(),
171
                $current_chained_subject,
172
                $method_name
173
            )) {
174
                $is_called = false;
175
            }
176
            else {
177
                throw $e;
178
            }
179
        }
180
        
181
        return $is_called;
182
    }
183
184
    /**
185
     * Checks if the exception having $trace is thrown from à __call
186
     * magic method.
187
     * 
188
     * @param  array  $trace
189
     * @param  object $current_chained_subject
190
     * @param  string $method_name
191
     * 
192
     * @return bool Whether or not the exception having the $trace has been
193
     *              thrown from a __call() method.
194
     */
195
    protected function exceptionTrownFromMagicCall(
196
        $trace, 
197
        $current_chained_subject,
198
        $method_name
199
    ) {
200
        // Before PHP 7, there is a raw for the non existing method called
201
        $call_user_func_array_position = PHP_VERSION_ID < 70000 ? 2 : 1;
202
        
203
        return  
204
                ($trace[0]['function'] == '__call' || $trace[0]['function'] == '__callStatic')
205
            &&  $trace[0]['class']    == get_class($current_chained_subject)
206
            &&  $trace[0]['args'][0]  == $method_name
207
            && (
208
                    $trace[$call_user_func_array_position]['file'] == __FILE__
209
                &&  $trace[$call_user_func_array_position]['function'] == 'call_user_func_array'
210
            )
211
            ;
212
    }
213
214
    /**
215
     * Invoking the instance produces the call of the stack
216
     *
217
     * @param  mixed $target The target to apply the callchain on
218
     * @return mixed The value returned once the call chain is called uppon $target
219
     */
220
    public function __invoke($target=null)
221
    {
222
        $out = $this->checkTarget($target);
223
        
224
        foreach ($this->stack as $i => $call) {
225
            $is_called = false;
226
            try {
227
                if (isset($call['method'])) {
228
                    if (is_callable([$out, $call['method']])) {
229
                        $is_called = $this->checkMethodIsReallyCallable(
230
                            '->',
231
                            $out,
232
                            $call['method'],
233
                            $call['arguments']
234
                        );
235
                    }
236
                    
237
                    if (! $is_called && (
238
                                (is_string($out) && is_callable($out .'::'.$call['method']))
239
                            ||  (is_object($out) && is_callable(get_class($out) .'::'.$call['method']))
240
                        )
241
                    ) {
242
                        $is_called = $this->checkMethodIsReallyCallable(
243
                            '::',
244
                            $out,
245
                            $call['method'],
246
                            $call['arguments']
247
                        );
248
                    }
249
                    
250
                    if (! $is_called && is_callable($call['method'])) {
251
                        $arguments = $this->prepareArgs($call['arguments'], $out);
252
                        $out = call_user_func_array($call['method'], $arguments);
253
                        $is_called = true;
254
                    }
255
                    
256
                    if (! $is_called) {
257
                        throw new \BadMethodCallException(
258
                            $call['method'] . "() is neither a method of " . get_class($out)
259
                            . " nor a function"
260
                        );
261
                    }
262
                }
263
                else {
264
                    $out = $out[ $call['entry'] ];
265
                }
266
            }
267
            catch (\Exception $e) {
268
                
269
                $callchain_description = $this->toString([
270
                    'target' => $target,
271
                    'limit'  => $i,
272
                ]);
273
                
274
                VisibilityViolator::setHiddenProperty(
275
                    $e,
276
                    'message',
277
                    $e->getMessage()
278
                    . "\nWhen applying $callchain_description called in "
279
                    . $call['file'] . ':' . $call['line']
280
                );
281
                
282
                // Throw $e with the good stack (usage exception)
283
                throw $e;
284
            }
285
        }
286
287
        return $out;
288
    }
289
290
    /**/
291
}
292