Completed
Push — master ( 4a17b9...26a191 )
by Jean
01:58
created

DeferredCallChain::offsetGet()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 11
rs 10
cc 3
nc 4
nop 1
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
        &$current_chained_subject, 
143
        $method_name,
144
        $arguments
145
    ) {
146
        $is_called = true;
147
        try {
148
            $current_chained_subject = call_user_func_array(
149
                [$current_chained_subject, $method_name], 
150
                $arguments
151
            );
152
        }
153
        catch (\BadMethodCallException $e) {
154
            if ($this->exceptionTrownFromMagicCall(
155
                $e->getTrace(),
156
                $current_chained_subject,
157
                $method_name
158
            )) {
159
                $is_called = false;
160
            }
161
            else {
162
                throw $e;
163
            }
164
        }
165
        
166
        return $is_called;
167
    }
168
169
    /**
170
     * Checks if the exception having $trace is thrown from à __call
171
     * magic method.
172
     * 
173
     * @param  array  $trace
174
     * @param  object $current_chained_subject
175
     * @param  string $method_name
176
     * 
177
     * @return bool Whether or not the exception having the $trace has been
178
     *              thrown from a __call() method.
179
     */
180
    protected function exceptionTrownFromMagicCall(
181
        $trace, 
182
        $current_chained_subject,
183
        $method_name
184
    ) {
185
        // Before PHP 7, there is a raw for the non existing method called
186
        $call_user_func_array_position = PHP_VERSION_ID < 70000 ? 2 : 1;
187
        
188
        return  
189
                $trace[0]['function'] == '__call'
190
            &&  $trace[0]['class']    == get_class($current_chained_subject)
191
            &&  $trace[0]['args'][0]  == $method_name
192
            && (
193
                    $trace[$call_user_func_array_position]['file'] == __FILE__
194
                &&  $trace[$call_user_func_array_position]['function'] == 'call_user_func_array'
195
            )
196
            ;
197
    }
198
199
    /**
200
     * Invoking the instance produces the call of the stack
201
     *
202
     * @param  mixed $target The target to apply the callchain on
203
     * @return mixed The value returned once the call chain is called uppon $target
204
     */
205
    public function __invoke($target=null)
206
    {
207
        $out = $this->checkTarget($target);
208
        
209
        foreach ($this->stack as $i => $call) {
210
            $is_called = false;
211
            try {
212
                if (isset($call['method'])) {
213
                    if (is_callable([$out, $call['method']])) {
214
                        $is_called = $this->checkMethodIsReallyCallable(
215
                            $out,
216
                            $call['method'],
217
                            $call['arguments']
218
                        );
219
                    }
220
                    
221
                    if (! $is_called && is_callable($call['method'])) {
222
                        $arguments = $this->prepareArgs($call['arguments'], $out);
223
                        $out = call_user_func_array($call['method'], $arguments);
224
                        $is_called = true;
225
                    }
226
                    
227
                    if (! $is_called) {
228
                        throw new \BadMethodCallException(
229
                            $call['method'] . "() is neither a method of " . get_class($out)
230
                            . " nor a function"
231
                        );
232
                    }
233
                }
234
                else {
235
                    $out = $out[ $call['entry'] ];
236
                }
237
            }
238
            catch (\Exception $e) {
239
                
240
                $callchain_description = $this->toString([
241
                    'target' => $target,
242
                    'limit'  => $i,
243
                ]);
244
                
245
                VisibilityViolator::setHiddenProperty(
246
                    $e,
247
                    'message',
248
                    $e->getMessage()
249
                    . "\nWhen applying $callchain_description called in "
250
                    . $call['file'] . ':' . $call['line']
251
                );
252
                
253
                // Throw $e with the good stack (usage exception)
254
                throw $e;
255
            }
256
        }
257
258
        return $out;
259
    }
260
261
    /**/
262
}
263